From 0d7517fbb2cb7c58a2cc6ea4e8b0f4cd2b6296c9 Mon Sep 17 00:00:00 2001 From: archive Date: Mon, 9 Oct 2006 00:00:00 +0000 Subject: [PATCH] as released 2006-10-09 --- EULA.Development Kit.rtf | 58 + ReadMe.rtf | Bin 0 -> 3879 bytes examples/01 - Basic Mod/ReadMe.txt | 53 + examples/01 - Basic Mod/mymod/description.txt | 1 + examples/01 - Basic Mod/mymod/game00.pk4 | Bin 0 -> 1687836 bytes examples/02 - Feedback/ReadMe.txt | 27 + examples/03 - New Entities/ReadMe.txt | 40 + .../healthzone/game_zone.cpp | 1223 ++ .../03 - New Entities/healthzone/game_zone.h | 282 + .../healthzone/healthzone.pk4 | Bin 0 -> 4094 bytes src/2005MayaImport.vcproj | 3722 ++++ src/2005game.vcproj | 6529 ++++++ src/2005idlib.vcproj | 1124 + src/MayaImport/Maya4.5/maya.h | 47 + src/MayaImport/Maya6.0/maya.h | 47 + src/MayaImport/exporter.h | 434 + src/MayaImport/maya5.0/maya.h | 47 + src/MayaImport/maya_main.cpp | 3152 +++ src/MayaImport/maya_main.h | 18 + src/MayaImport/mayaimport.def | 4 + src/PREY.sln | 38 + src/Prey/ai_Navigator.cpp | 268 + src/Prey/ai_Navigator.h | 41 + src/Prey/ai_centurion.cpp | 529 + src/Prey/ai_centurion.h | 84 + src/Prey/ai_crawler.cpp | 115 + src/Prey/ai_crawler.h | 24 + src/Prey/ai_creaturex.cpp | 1669 ++ src/Prey/ai_creaturex.h | 139 + src/Prey/ai_droid.cpp | 660 + src/Prey/ai_droid.h | 73 + src/Prey/ai_gasbag_simple.cpp | 801 + src/Prey/ai_gasbag_simple.h | 90 + src/Prey/ai_harvester_simple.cpp | 838 + src/Prey/ai_harvester_simple.h | 79 + src/Prey/ai_hunter_simple.cpp | 2139 ++ src/Prey/ai_hunter_simple.h | 128 + src/Prey/ai_inspector.cpp | 136 + src/Prey/ai_inspector.h | 28 + src/Prey/ai_jetpack_harvester_simple.cpp | 662 + src/Prey/ai_jetpack_harvester_simple.h | 66 + src/Prey/ai_keeper_simple.cpp | 694 + src/Prey/ai_keeper_simple.h | 98 + src/Prey/ai_mutate.cpp | 63 + src/Prey/ai_mutate.h | 15 + src/Prey/ai_mutilatedhuman.cpp | 390 + src/Prey/ai_mutilatedhuman.h | 23 + src/Prey/ai_passageway.cpp | 144 + src/Prey/ai_passageway.h | 49 + src/Prey/ai_possessedTommy.cpp | 120 + src/Prey/ai_possessedTommy.h | 30 + src/Prey/ai_reaction.cpp | 639 + src/Prey/ai_reaction.h | 261 + src/Prey/ai_spawncase.cpp | 227 + src/Prey/ai_spawncase.h | 34 + src/Prey/ai_speech.cpp | 403 + src/Prey/ai_speech.h | 102 + src/Prey/ai_sphereboss.cpp | 533 + src/Prey/ai_sphereboss.h | 45 + src/Prey/anim_baseanim.cpp | 104 + src/Prey/anim_baseanim.h | 29 + src/Prey/force_converge.cpp | 173 + src/Prey/force_converge.h | 44 + src/Prey/game_afs.cpp | 100 + src/Prey/game_afs.h | 49 + src/Prey/game_alarm.cpp | 76 + src/Prey/game_alarm.h | 21 + src/Prey/game_anim.cpp | 244 + src/Prey/game_anim.h | 23 + src/Prey/game_animBlend.cpp | 276 + src/Prey/game_animBlend.h | 6 + src/Prey/game_animDriven.cpp | 287 + src/Prey/game_animDriven.h | 40 + src/Prey/game_animatedentity.cpp | 424 + src/Prey/game_animatedentity.h | 74 + src/Prey/game_animatedgui.cpp | 208 + src/Prey/game_animatedgui.h | 34 + src/Prey/game_animator.cpp | 251 + src/Prey/game_animator.h | 32 + src/Prey/game_arcadegame.cpp | 766 + src/Prey/game_arcadegame.h | 188 + src/Prey/game_barrel.cpp | 181 + src/Prey/game_barrel.h | 30 + src/Prey/game_bindController.cpp | 290 + src/Prey/game_bindController.h | 68 + src/Prey/game_blackjack.cpp | 508 + src/Prey/game_blackjack.h | 70 + src/Prey/game_cards.cpp | 130 + src/Prey/game_cards.h | 63 + src/Prey/game_cilia.cpp | 300 + src/Prey/game_cilia.h | 36 + src/Prey/game_console.cpp | 822 + src/Prey/game_console.h | 148 + src/Prey/game_damagetester.cpp | 180 + src/Prey/game_damagetester.h | 39 + src/Prey/game_dda.cpp | 681 + src/Prey/game_dda.h | 128 + src/Prey/game_deathwraith.cpp | 559 + src/Prey/game_deathwraith.h | 48 + src/Prey/game_debrisspawner.cpp | 439 + src/Prey/game_debrisspawner.h | 47 + src/Prey/game_dock.cpp | 70 + src/Prey/game_dock.h | 49 + src/Prey/game_dockedgun.cpp | 16 + src/Prey/game_dockedgun.h | 19 + src/Prey/game_door.cpp | 586 + src/Prey/game_door.h | 69 + src/Prey/game_eggspawner.cpp | 281 + src/Prey/game_eggspawner.h | 53 + src/Prey/game_energynode.cpp | 126 + src/Prey/game_energynode.h | 35 + src/Prey/game_entityfx.cpp | 637 + src/Prey/game_entityfx.h | 62 + src/Prey/game_entityspawner.cpp | 117 + src/Prey/game_entityspawner.h | 25 + src/Prey/game_events.cpp | 12 + src/Prey/game_events.h | 18 + src/Prey/game_fixedpod.cpp | 209 + src/Prey/game_fixedpod.h | 31 + src/Prey/game_forcefield.cpp | 625 + src/Prey/game_forcefield.h | 94 + src/Prey/game_fxinfo.cpp | 343 + src/Prey/game_fxinfo.h | 175 + src/Prey/game_gibbable.cpp | 146 + src/Prey/game_gibbable.h | 31 + src/Prey/game_gravityswitch.cpp | 204 + src/Prey/game_gravityswitch.h | 35 + src/Prey/game_guihand.cpp | 52 + src/Prey/game_guihand.h | 33 + src/Prey/game_gun.cpp | 295 + src/Prey/game_gun.h | 46 + src/Prey/game_hand.cpp | 1155 + src/Prey/game_hand.h | 171 + src/Prey/game_handcontrol.cpp | 110 + src/Prey/game_handcontrol.h | 34 + src/Prey/game_healthbasin.cpp | 246 + src/Prey/game_healthbasin.h | 54 + src/Prey/game_healthspore.cpp | 121 + src/Prey/game_healthspore.h | 23 + src/Prey/game_inventory.cpp | 579 + src/Prey/game_inventory.h | 54 + src/Prey/game_itemautomatic.cpp | 227 + src/Prey/game_itemautomatic.h | 24 + src/Prey/game_itemcabinet.cpp | 417 + src/Prey/game_itemcabinet.h | 51 + src/Prey/game_jukebox.cpp | 265 + src/Prey/game_jukebox.h | 47 + src/Prey/game_jumpzone.cpp | 129 + src/Prey/game_jumpzone.h | 28 + src/Prey/game_light.cpp | 103 + src/Prey/game_light.h | 25 + src/Prey/game_lightfixture.cpp | 210 + src/Prey/game_lightfixture.h | 37 + src/Prey/game_mine.cpp | 432 + src/Prey/game_mine.h | 82 + src/Prey/game_misc.cpp | 404 + src/Prey/game_misc.h | 100 + src/Prey/game_modeldoor.cpp | 1076 + src/Prey/game_modeldoor.h | 139 + src/Prey/game_modeltoggle.cpp | 316 + src/Prey/game_modeltoggle.h | 62 + src/Prey/game_monster_ai.cpp | 2156 ++ src/Prey/game_monster_ai.h | 262 + src/Prey/game_monster_ai_events.cpp | 1302 ++ src/Prey/game_mountedgun.cpp | 1048 + src/Prey/game_mountedgun.h | 158 + src/Prey/game_moveable.cpp | 492 + src/Prey/game_moveable.h | 56 + src/Prey/game_mover.cpp | 241 + src/Prey/game_mover.h | 73 + src/Prey/game_note.cpp | 29 + src/Prey/game_note.h | 13 + src/Prey/game_organtrigger.cpp | 190 + src/Prey/game_organtrigger.h | 41 + src/Prey/game_player.cpp | 7825 +++++++ src/Prey/game_player.h | 771 + src/Prey/game_playerview.cpp | 748 + src/Prey/game_playerview.h | 108 + src/Prey/game_pod.cpp | 157 + src/Prey/game_pod.h | 34 + src/Prey/game_podspawner.cpp | 141 + src/Prey/game_podspawner.h | 31 + src/Prey/game_poker.cpp | 807 + src/Prey/game_poker.h | 123 + src/Prey/game_portal.cpp | 1164 + src/Prey/game_portal.h | 125 + src/Prey/game_portalframe.cpp | 20 + src/Prey/game_portalframe.h | 14 + src/Prey/game_proxdoor.cpp | 1163 + src/Prey/game_proxdoor.h | 204 + src/Prey/game_rail.cpp | 85 + src/Prey/game_rail.h | 41 + src/Prey/game_railshuttle.cpp | 620 + src/Prey/game_railshuttle.h | 106 + src/Prey/game_renderentity.cpp | 141 + src/Prey/game_renderentity.h | 31 + src/Prey/game_safeDeathVolume.cpp | 136 + src/Prey/game_safeDeathVolume.h | 43 + src/Prey/game_securityeye.cpp | 801 + src/Prey/game_securityeye.h | 134 + src/Prey/game_shuttle.cpp | 1057 + src/Prey/game_shuttle.h | 152 + src/Prey/game_shuttledock.cpp | 454 + src/Prey/game_shuttledock.h | 67 + src/Prey/game_shuttletransport.cpp | 244 + src/Prey/game_shuttletransport.h | 51 + src/Prey/game_skybox.cpp | 91 + src/Prey/game_skybox.h | 14 + src/Prey/game_slots.cpp | 417 + src/Prey/game_slots.h | 86 + src/Prey/game_sphere.cpp | 151 + src/Prey/game_sphere.h | 31 + src/Prey/game_spherepart.cpp | 319 + src/Prey/game_spherepart.h | 71 + src/Prey/game_spring.cpp | 191 + src/Prey/game_spring.h | 45 + src/Prey/game_sunCorona.cpp | 123 + src/Prey/game_sunCorona.h | 24 + src/Prey/game_talon.cpp | 1777 ++ src/Prey/game_talon.h | 194 + src/Prey/game_targetproxy.cpp | 282 + src/Prey/game_targetproxy.h | 63 + src/Prey/game_targets.cpp | 849 + src/Prey/game_targets.h | 235 + src/Prey/game_trackmover.cpp | 172 + src/Prey/game_trackmover.h | 33 + src/Prey/game_trigger.cpp | 1482 ++ src/Prey/game_trigger.h | 258 + src/Prey/game_tripwire.cpp | 336 + src/Prey/game_tripwire.h | 50 + src/Prey/game_utils.cpp | 749 + src/Prey/game_utils.h | 318 + src/Prey/game_vehicle.cpp | 1922 ++ src/Prey/game_vehicle.h | 375 + src/Prey/game_vomiter.cpp | 333 + src/Prey/game_vomiter.h | 61 + src/Prey/game_weaponHandState.cpp | 282 + src/Prey/game_weaponHandState.h | 41 + src/Prey/game_woundmanager.cpp | 314 + src/Prey/game_woundmanager.h | 34 + src/Prey/game_wraith.cpp | 974 + src/Prey/game_wraith.h | 128 + src/Prey/game_zone.cpp | 1182 + src/Prey/game_zone.h | 266 + src/Prey/particles_particles.cpp | 368 + src/Prey/particles_particles.h | 13 + src/Prey/physics_delta.cpp | 155 + src/Prey/physics_delta.h | 34 + src/Prey/physics_preyai.cpp | 573 + src/Prey/physics_preyai.h | 45 + src/Prey/physics_preyparametric.cpp | 302 + src/Prey/physics_preyparametric.h | 54 + src/Prey/physics_simple.cpp | 50 + src/Prey/physics_simple.h | 15 + src/Prey/physics_vehicle.cpp | 234 + src/Prey/physics_vehicle.h | 37 + src/Prey/prey_animator.cpp | 607 + src/Prey/prey_animator.h | 50 + src/Prey/prey_baseweapons.cpp | 2525 +++ src/Prey/prey_baseweapons.h | 365 + src/Prey/prey_beam.cpp | 1150 + src/Prey/prey_beam.h | 206 + src/Prey/prey_bonecontroller.cpp | 247 + src/Prey/prey_bonecontroller.h | 57 + src/Prey/prey_camerainterpolator.cpp | 672 + src/Prey/prey_camerainterpolator.h | 134 + src/Prey/prey_firecontroller.cpp | 548 + src/Prey/prey_firecontroller.h | 160 + src/Prey/prey_game.cpp | 1438 ++ src/Prey/prey_game.h | 116 + src/Prey/prey_items.cpp | 639 + src/Prey/prey_items.h | 79 + src/Prey/prey_liquid.cpp | 105 + src/Prey/prey_liquid.h | 26 + src/Prey/prey_local.cpp | 3 + src/Prey/prey_local.h | 115 + src/Prey/prey_projectile.cpp | 1547 ++ src/Prey/prey_projectile.h | 176 + src/Prey/prey_projectileautocannon.cpp | 21 + src/Prey/prey_projectileautocannon.h | 8 + src/Prey/prey_projectilebounce.cpp | 34 + src/Prey/prey_projectilebounce.h | 11 + src/Prey/prey_projectilebug.cpp | 135 + src/Prey/prey_projectilebug.h | 20 + src/Prey/prey_projectilebugtrigger.cpp | 40 + src/Prey/prey_projectilebugtrigger.h | 13 + src/Prey/prey_projectilecocoon.cpp | 105 + src/Prey/prey_projectilecocoon.h | 19 + src/Prey/prey_projectilecrawlergrenade.cpp | 557 + src/Prey/prey_projectilecrawlergrenade.h | 86 + src/Prey/prey_projectilefreezer.cpp | 115 + src/Prey/prey_projectilefreezer.h | 35 + src/Prey/prey_projectilegasbagpod.cpp | 89 + src/Prey/prey_projectilegasbagpod.h | 20 + src/Prey/prey_projectilehiderweapon.cpp | 136 + src/Prey/prey_projectilehiderweapon.h | 45 + src/Prey/prey_projectilemine.cpp | 505 + src/Prey/prey_projectilemine.h | 70 + src/Prey/prey_projectilerifle.cpp | 226 + src/Prey/prey_projectilerifle.h | 41 + src/Prey/prey_projectilerocketlauncher.cpp | 323 + src/Prey/prey_projectilerocketlauncher.h | 70 + src/Prey/prey_projectileshuttle.cpp | 22 + src/Prey/prey_projectileshuttle.h | 8 + src/Prey/prey_projectilesoulcannon.cpp | 143 + src/Prey/prey_projectilesoulcannon.h | 30 + src/Prey/prey_projectilespiritarrow.cpp | 105 + src/Prey/prey_projectilespiritarrow.h | 20 + src/Prey/prey_projectiletracking.cpp | 330 + src/Prey/prey_projectiletracking.h | 48 + src/Prey/prey_projectiletrigger.cpp | 56 + src/Prey/prey_projectiletrigger.h | 10 + src/Prey/prey_projectilewrench.cpp | 43 + src/Prey/prey_projectilewrench.h | 11 + src/Prey/prey_script_thread.cpp | 114 + src/Prey/prey_script_thread.h | 27 + src/Prey/prey_sound.cpp | 258 + src/Prey/prey_sound.h | 38 + src/Prey/prey_soundleadincontroller.cpp | 264 + src/Prey/prey_soundleadincontroller.h | 53 + src/Prey/prey_spiritbridge.cpp | 70 + src/Prey/prey_spiritbridge.h | 17 + src/Prey/prey_spiritproxy.cpp | 1094 + src/Prey/prey_spiritproxy.h | 200 + src/Prey/prey_spiritsecret.cpp | 72 + src/Prey/prey_spiritsecret.h | 15 + src/Prey/prey_vehiclefirecontroller.cpp | 225 + src/Prey/prey_vehiclefirecontroller.h | 47 + src/Prey/prey_weapon.cpp | 4 + src/Prey/prey_weapon.h | 5 + src/Prey/prey_weaponautocannon.cpp | 363 + src/Prey/prey_weaponautocannon.h | 67 + src/Prey/prey_weaponcrawlergrenade.cpp | 51 + src/Prey/prey_weaponcrawlergrenade.h | 21 + src/Prey/prey_weaponfirecontroller.cpp | 574 + src/Prey/prey_weaponfirecontroller.h | 138 + src/Prey/prey_weaponhider.cpp | 95 + src/Prey/prey_weaponhider.h | 42 + src/Prey/prey_weaponrifle.cpp | 466 + src/Prey/prey_weaponrifle.h | 91 + src/Prey/prey_weaponrocketlauncher.cpp | 29 + src/Prey/prey_weaponrocketlauncher.h | 97 + src/Prey/prey_weaponsoulstripper.cpp | 1427 ++ src/Prey/prey_weaponsoulstripper.h | 182 + src/Prey/prey_weaponspiritbow.cpp | 378 + src/Prey/prey_weaponspiritbow.h | 63 + src/Prey/sys_debugger.cpp | 1102 + src/Prey/sys_debugger.h | 187 + src/Prey/sys_preycmds.cpp | 1121 + src/Prey/sys_preycmds.h | 6 + src/cm/CollisionModel.h | 146 + src/cm/CollisionModel_contacts.cpp | 48 + src/cm/CollisionModel_contents.cpp | 614 + src/cm/CollisionModel_debug.cpp | 498 + src/cm/CollisionModel_files.cpp | 598 + src/cm/CollisionModel_load.cpp | 3955 ++++ src/cm/CollisionModel_local.h | 527 + src/cm/CollisionModel_rotate.cpp | 1672 ++ src/cm/CollisionModel_trace.cpp | 229 + src/cm/CollisionModel_translate.cpp | 1100 + src/framework/BuildDefines.h | 163 + src/framework/BuildVersion.h | 4 + src/framework/CVarSystem.h | 288 + src/framework/CmdSystem.h | 177 + src/framework/Common.h | 215 + src/framework/DeclPDA.h | 142 + src/framework/File.h | 218 + src/framework/FileSystem.h | 274 + src/framework/UsercmdGen.h | 159 + src/framework/async/NetworkSystem.h | 40 + src/framework/declAF.h | 197 + src/framework/declEntityDef.h | 26 + src/framework/declFX.h | 104 + src/framework/declManager.h | 338 + src/framework/declParticle.h | 201 + src/framework/declPreyBeam.h | 53 + src/framework/declSkin.h | 40 + src/framework/declTable.h | 41 + src/framework/dotnetwarnings.h | 20 + src/framework/licensee.h | 219 + src/game/AF.cpp | 1363 ++ src/game/AF.h | 99 + src/game/AFEntity.cpp | 3071 +++ src/game/AFEntity.h | 477 + src/game/Actor.cpp | 4136 ++++ src/game/Actor.h | 442 + src/game/BrittleFracture.cpp | 1360 ++ src/game/BrittleFracture.h | 116 + src/game/Camera.cpp | 747 + src/game/Camera.h | 109 + src/game/Entity.cpp | 6221 ++++++ src/game/Entity.h | 789 + src/game/EntityAdditions.cpp | 1670 ++ src/game/Fx.cpp | 826 + src/game/Fx.h | 106 + src/game/Game.def | 2 + src/game/Game.h | 342 + src/game/GameEdit.cpp | 1118 + src/game/GameEdit.h | 94 + src/game/Game_local.cpp | 5394 +++++ src/game/Game_local.h | 1110 + src/game/Game_network.cpp | 2084 ++ src/game/IK.cpp | 1156 + src/game/IK.h | 165 + src/game/Item.cpp | 1064 + src/game/Item.h | 155 + src/game/Light.cpp | 1132 + src/game/Light.h | 117 + src/game/Misc.cpp | 3198 +++ src/game/Misc.h | 751 + src/game/Moveable.cpp | 539 + src/game/Moveable.h | 99 + src/game/Mover.cpp | 4842 +++++ src/game/Mover.h | 562 + src/game/MultiplayerGame.cpp | 4559 ++++ src/game/MultiplayerGame.h | 394 + src/game/Player.cpp | 7487 +++++++ src/game/Player.h | 709 + src/game/PlayerIcon.cpp | 284 + src/game/PlayerIcon.h | 46 + src/game/PlayerView.cpp | 710 + src/game/PlayerView.h | 110 + src/game/Pvs.cpp | 1551 ++ src/game/Pvs.h | 110 + src/game/SecurityCamera.cpp | 563 + src/game/SecurityCamera.h | 72 + src/game/SmokeParticles.cpp | 486 + src/game/SmokeParticles.h | 77 + src/game/Sound.cpp | 280 + src/game/Sound.h | 51 + src/game/Target.cpp | 1048 + src/game/Target.h | 414 + src/game/Weapon.cpp | 3181 +++ src/game/Weapon.h | 388 + src/game/WorldSpawn.cpp | 125 + src/game/WorldSpawn.h | 30 + src/game/ai/AAS.cpp | 250 + src/game/ai/AAS.h | 118 + src/game/ai/AAS_NearPoint.cpp | 223 + src/game/ai/AAS_debug.cpp | 568 + src/game/ai/AAS_local.h | 224 + src/game/ai/AAS_pathing.cpp | 696 + src/game/ai/AAS_routing.cpp | 1324 ++ src/game/ai/AI.cpp | 5454 +++++ src/game/ai/AI.h | 781 + src/game/ai/AI_events.cpp | 2815 +++ src/game/ai/AI_pathing.cpp | 1528 ++ src/game/anim/Anim.cpp | 1259 ++ src/game/anim/Anim.h | 709 + src/game/anim/Anim_Blend.cpp | 5166 +++++ src/game/anim/Anim_Import.cpp | 554 + src/game/anim/Anim_Testmodel.cpp | 935 + src/game/anim/Anim_Testmodel.h | 70 + src/game/gamesys/Callbacks.cpp | 2600 +++ src/game/gamesys/Class.cpp | 1115 + src/game/gamesys/Class.h | 341 + src/game/gamesys/DebugGraph.cpp | 67 + src/game/gamesys/DebugGraph.h | 15 + src/game/gamesys/Event.cpp | 903 + src/game/gamesys/Event.h | 196 + src/game/gamesys/NoGameTypeInfo.h | 58 + src/game/gamesys/SaveGame.cpp | 1663 ++ src/game/gamesys/SaveGame.h | 167 + src/game/gamesys/SysCmds.cpp | 2841 +++ src/game/gamesys/SysCmds.h | 9 + src/game/gamesys/SysCvar.cpp | 401 + src/game/gamesys/SysCvar.h | 294 + src/game/gamesys/TypeInfo.cpp | 1418 ++ src/game/gamesys/TypeInfo.h | 27 + src/game/physics/Clip.cpp | 2108 ++ src/game/physics/Clip.h | 397 + src/game/physics/Force.cpp | 68 + src/game/physics/Force.h | 41 + src/game/physics/Force_Constant.cpp | 110 + src/game/physics/Force_Constant.h | 46 + src/game/physics/Force_Drag.cpp | 130 + src/game/physics/Force_Drag.h | 49 + src/game/physics/Force_Field.cpp | 243 + src/game/physics/Force_Field.h | 69 + src/game/physics/Force_Spring.cpp | 183 + src/game/physics/Force_Spring.h | 55 + src/game/physics/Physics.cpp | 55 + src/game/physics/Physics.h | 162 + src/game/physics/Physics_AF.cpp | 8167 +++++++ src/game/physics/Physics_AF.h | 1036 + src/game/physics/Physics_Actor.cpp | 671 + src/game/physics/Physics_Actor.h | 133 + src/game/physics/Physics_Base.cpp | 844 + src/game/physics/Physics_Base.h | 145 + src/game/physics/Physics_Monster.cpp | 809 + src/game/physics/Physics_Monster.h | 129 + src/game/physics/Physics_Parametric.cpp | 1291 ++ src/game/physics/Physics_Parametric.h | 160 + src/game/physics/Physics_Player.cpp | 2116 ++ src/game/physics/Physics_Player.h | 220 + src/game/physics/Physics_PreyPlayer.cpp | 1172 + src/game/physics/Physics_PreyPlayer.h | 122 + src/game/physics/Physics_RigidBody.cpp | 1601 ++ src/game/physics/Physics_RigidBody.h | 179 + src/game/physics/Physics_Static.cpp | 836 + src/game/physics/Physics_Static.h | 136 + src/game/physics/Physics_StaticMulti.cpp | 1025 + src/game/physics/Physics_StaticMulti.h | 129 + src/game/physics/Push.cpp | 1484 ++ src/game/physics/Push.h | 91 + src/game/projectile.cpp | 2165 ++ src/game/projectile.h | 222 + src/game/script/Script_Compiler.cpp | 2628 +++ src/game/script/Script_Compiler.h | 253 + src/game/script/Script_Interpreter.cpp | 2016 ++ src/game/script/Script_Interpreter.h | 251 + src/game/script/Script_Program.cpp | 2264 ++ src/game/script/Script_Program.h | 732 + src/game/script/Script_Thread.cpp | 2171 ++ src/game/script/Script_Thread.h | 346 + src/game/trigger.cpp | 1167 + src/game/trigger.h | 264 + src/idLib/Base64.cpp | 233 + src/idLib/Base64.h | 84 + src/idLib/BitMsg.cpp | 1358 ++ src/idLib/BitMsg.h | 753 + src/idLib/CmdArgs.cpp | 173 + src/idLib/CmdArgs.h | 48 + src/idLib/Dict.cpp | 709 + src/idLib/Dict.h | 280 + src/idLib/Heap.cpp | 1821 ++ src/idLib/Heap.h | 864 + src/idLib/LangDict.cpp | 293 + src/idLib/LangDict.h | 52 + src/idLib/Lexer.cpp | 1772 ++ src/idLib/Lexer.h | 282 + src/idLib/Lib.cpp | 544 + src/idLib/Lib.h | 248 + src/idLib/Parser.cpp | 3227 +++ src/idLib/Parser.h | 258 + src/idLib/Str.cpp | 1718 ++ src/idLib/Str.h | 1006 + src/idLib/Timer.cpp | 135 + src/idLib/Timer.h | 204 + src/idLib/Token.cpp | 154 + src/idLib/Token.h | 140 + src/idLib/bv/Bounds.cpp | 410 + src/idLib/bv/Bounds.h | 385 + src/idLib/bv/Box.cpp | 822 + src/idLib/bv/Box.h | 272 + src/idLib/bv/Frustum.cpp | 2827 +++ src/idLib/bv/Frustum.h | 240 + src/idLib/bv/Frustum_gcc.cpp | 116 + src/idLib/bv/Sphere.cpp | 130 + src/idLib/bv/Sphere.h | 250 + src/idLib/containers/BTree.h | 495 + src/idLib/containers/BinSearch.h | 113 + src/idLib/containers/HashIndex.cpp | 130 + src/idLib/containers/HashIndex.h | 380 + src/idLib/containers/HashTable.h | 379 + src/idLib/containers/Hierarchy.h | 337 + src/idLib/containers/LinkList.h | 318 + src/idLib/containers/List.h | 926 + src/idLib/containers/PlaneSet.h | 56 + src/idLib/containers/PreyStack.h | 64 + src/idLib/containers/Queue.h | 63 + src/idLib/containers/Stack.h | 61 + src/idLib/containers/StaticList.h | 523 + src/idLib/containers/StrList.h | 179 + src/idLib/containers/StrPool.h | 209 + src/idLib/containers/VectorSet.h | 245 + src/idLib/geometry/DrawVert.cpp | 19 + src/idLib/geometry/DrawVert.h | 82 + src/idLib/geometry/JointTransform.cpp | 62 + src/idLib/geometry/JointTransform.h | 295 + src/idLib/geometry/Surface.cpp | 905 + src/idLib/geometry/Surface.h | 208 + src/idLib/geometry/Surface_Patch.cpp | 666 + src/idLib/geometry/Surface_Patch.h | 121 + src/idLib/geometry/Surface_Polytope.cpp | 311 + src/idLib/geometry/Surface_Polytope.h | 45 + src/idLib/geometry/Surface_SweptSpline.cpp | 198 + src/idLib/geometry/Surface_SweptSpline.h | 68 + src/idLib/geometry/TraceModel.cpp | 1469 ++ src/idLib/geometry/TraceModel.h | 164 + src/idLib/geometry/Winding.cpp | 1576 ++ src/idLib/geometry/Winding.h | 376 + src/idLib/geometry/Winding2D.cpp | 729 + src/idLib/geometry/Winding2D.h | 144 + src/idLib/hashing/CRC16.cpp | 120 + src/idLib/hashing/CRC16.h | 21 + src/idLib/hashing/CRC32.cpp | 167 + src/idLib/hashing/CRC32.h | 19 + src/idLib/hashing/CRC8.cpp | 116 + src/idLib/hashing/CRC8.h | 21 + src/idLib/hashing/Honeyman.cpp | 118 + src/idLib/hashing/Honeyman.h | 22 + src/idLib/hashing/MD4.cpp | 259 + src/idLib/hashing/MD4.h | 16 + src/idLib/hashing/MD5.cpp | 272 + src/idLib/hashing/MD5.h | 16 + src/idLib/mapfile.cpp | 942 + src/idLib/mapfile.h | 212 + src/idLib/math/Angles.cpp | 215 + src/idLib/math/Angles.h | 237 + src/idLib/math/Complex.cpp | 16 + src/idLib/math/Complex.h | 323 + src/idLib/math/Curve.h | 2506 +++ src/idLib/math/Extrapolate.h | 216 + src/idLib/math/Interpolate.h | 392 + src/idLib/math/Lcp.cpp | 1619 ++ src/idLib/math/Lcp.h | 52 + src/idLib/math/Math.cpp | 106 + src/idLib/math/Math.h | 914 + src/idLib/math/Matrix.cpp | 8077 +++++++ src/idLib/math/Matrix.h | 3271 +++ src/idLib/math/Ode.cpp | 330 + src/idLib/math/Ode.h | 121 + src/idLib/math/Plane.cpp | 129 + src/idLib/math/Plane.h | 366 + src/idLib/math/Pluecker.cpp | 61 + src/idLib/math/Pluecker.h | 343 + src/idLib/math/Polynomial.cpp | 217 + src/idLib/math/Polynomial.h | 604 + src/idLib/math/Quat.cpp | 227 + src/idLib/math/Quat.h | 379 + src/idLib/math/Random.h | 133 + src/idLib/math/Rotation.cpp | 132 + src/idLib/math/Rotation.h | 186 + src/idLib/math/Simd.cpp | 4200 ++++ src/idLib/math/Simd.h | 199 + src/idLib/math/Simd_3DNow.cpp | 272 + src/idLib/math/Simd_3DNow.h | 25 + src/idLib/math/Simd_AltiVec.cpp | 11218 ++++++++++ src/idLib/math/Simd_AltiVec.h | 225 + src/idLib/math/Simd_MMX.cpp | 322 + src/idLib/math/Simd_MMX.h | 26 + src/idLib/math/Simd_SSE.cpp | 18129 ++++++++++++++++ src/idLib/math/Simd_SSE.h | 118 + src/idLib/math/Simd_SSE2.cpp | 2196 ++ src/idLib/math/Simd_SSE2.h | 47 + src/idLib/math/Simd_SSE3.cpp | 734 + src/idLib/math/Simd_SSE3.h | 31 + src/idLib/math/Simd_generic.cpp | 3270 +++ src/idLib/math/Simd_generic.h | 129 + src/idLib/math/Vector.cpp | 416 + src/idLib/math/Vector.h | 1993 ++ src/idLib/math/prey_interpolate.h | 346 + src/idLib/math/prey_math.cpp | 248 + src/idLib/math/prey_math.h | 56 + src/idLib/precompiled.cpp | 2 + src/idLib/precompiled.h | 214 + src/preyengine/profiler.h | 151 + src/renderer/Cinematic.h | 104 + src/renderer/Material.h | 753 + src/renderer/Model.h | 324 + src/renderer/ModelManager.h | 74 + src/renderer/Model_local.h | 442 + src/renderer/RenderSystem.h | 263 + src/renderer/RenderWorld.h | 492 + src/renderer/glext.h | 5922 +++++ src/renderer/qgl.h | 542 + src/renderer/qgl_linked.h | 346 + src/sound/sound.h | 446 + src/sys/linux/qgl_enforce.h | 1451 ++ src/sys/scons/SConscript.game | 111 + src/sys/scons/SConscript.idlib | 83 + src/sys/scons/scons_utils.py | 182 + src/sys/sys_public.h | 625 + src/tools/compilers/aas/AASFile.h | 327 + src/tools/compilers/aas/AASFileManager.h | 25 + src/ui/ListGUI.h | 36 + src/ui/UserInterface.h | 152 + 668 files changed, 382447 insertions(+) create mode 100644 EULA.Development Kit.rtf create mode 100644 ReadMe.rtf create mode 100644 examples/01 - Basic Mod/ReadMe.txt create mode 100644 examples/01 - Basic Mod/mymod/description.txt create mode 100644 examples/01 - Basic Mod/mymod/game00.pk4 create mode 100644 examples/02 - Feedback/ReadMe.txt create mode 100644 examples/03 - New Entities/ReadMe.txt create mode 100644 examples/03 - New Entities/healthzone/game_zone.cpp create mode 100644 examples/03 - New Entities/healthzone/game_zone.h create mode 100644 examples/03 - New Entities/healthzone/healthzone.pk4 create mode 100644 src/2005MayaImport.vcproj create mode 100644 src/2005game.vcproj create mode 100644 src/2005idlib.vcproj create mode 100644 src/MayaImport/Maya4.5/maya.h create mode 100644 src/MayaImport/Maya6.0/maya.h create mode 100644 src/MayaImport/exporter.h create mode 100644 src/MayaImport/maya5.0/maya.h create mode 100644 src/MayaImport/maya_main.cpp create mode 100644 src/MayaImport/maya_main.h create mode 100644 src/MayaImport/mayaimport.def create mode 100644 src/PREY.sln create mode 100644 src/Prey/ai_Navigator.cpp create mode 100644 src/Prey/ai_Navigator.h create mode 100644 src/Prey/ai_centurion.cpp create mode 100644 src/Prey/ai_centurion.h create mode 100644 src/Prey/ai_crawler.cpp create mode 100644 src/Prey/ai_crawler.h create mode 100644 src/Prey/ai_creaturex.cpp create mode 100644 src/Prey/ai_creaturex.h create mode 100644 src/Prey/ai_droid.cpp create mode 100644 src/Prey/ai_droid.h create mode 100644 src/Prey/ai_gasbag_simple.cpp create mode 100644 src/Prey/ai_gasbag_simple.h create mode 100644 src/Prey/ai_harvester_simple.cpp create mode 100644 src/Prey/ai_harvester_simple.h create mode 100644 src/Prey/ai_hunter_simple.cpp create mode 100644 src/Prey/ai_hunter_simple.h create mode 100644 src/Prey/ai_inspector.cpp create mode 100644 src/Prey/ai_inspector.h create mode 100644 src/Prey/ai_jetpack_harvester_simple.cpp create mode 100644 src/Prey/ai_jetpack_harvester_simple.h create mode 100644 src/Prey/ai_keeper_simple.cpp create mode 100644 src/Prey/ai_keeper_simple.h create mode 100644 src/Prey/ai_mutate.cpp create mode 100644 src/Prey/ai_mutate.h create mode 100644 src/Prey/ai_mutilatedhuman.cpp create mode 100644 src/Prey/ai_mutilatedhuman.h create mode 100644 src/Prey/ai_passageway.cpp create mode 100644 src/Prey/ai_passageway.h create mode 100644 src/Prey/ai_possessedTommy.cpp create mode 100644 src/Prey/ai_possessedTommy.h create mode 100644 src/Prey/ai_reaction.cpp create mode 100644 src/Prey/ai_reaction.h create mode 100644 src/Prey/ai_spawncase.cpp create mode 100644 src/Prey/ai_spawncase.h create mode 100644 src/Prey/ai_speech.cpp create mode 100644 src/Prey/ai_speech.h create mode 100644 src/Prey/ai_sphereboss.cpp create mode 100644 src/Prey/ai_sphereboss.h create mode 100644 src/Prey/anim_baseanim.cpp create mode 100644 src/Prey/anim_baseanim.h create mode 100644 src/Prey/force_converge.cpp create mode 100644 src/Prey/force_converge.h create mode 100644 src/Prey/game_afs.cpp create mode 100644 src/Prey/game_afs.h create mode 100644 src/Prey/game_alarm.cpp create mode 100644 src/Prey/game_alarm.h create mode 100644 src/Prey/game_anim.cpp create mode 100644 src/Prey/game_anim.h create mode 100644 src/Prey/game_animBlend.cpp create mode 100644 src/Prey/game_animBlend.h create mode 100644 src/Prey/game_animDriven.cpp create mode 100644 src/Prey/game_animDriven.h create mode 100644 src/Prey/game_animatedentity.cpp create mode 100644 src/Prey/game_animatedentity.h create mode 100644 src/Prey/game_animatedgui.cpp create mode 100644 src/Prey/game_animatedgui.h create mode 100644 src/Prey/game_animator.cpp create mode 100644 src/Prey/game_animator.h create mode 100644 src/Prey/game_arcadegame.cpp create mode 100644 src/Prey/game_arcadegame.h create mode 100644 src/Prey/game_barrel.cpp create mode 100644 src/Prey/game_barrel.h create mode 100644 src/Prey/game_bindController.cpp create mode 100644 src/Prey/game_bindController.h create mode 100644 src/Prey/game_blackjack.cpp create mode 100644 src/Prey/game_blackjack.h create mode 100644 src/Prey/game_cards.cpp create mode 100644 src/Prey/game_cards.h create mode 100644 src/Prey/game_cilia.cpp create mode 100644 src/Prey/game_cilia.h create mode 100644 src/Prey/game_console.cpp create mode 100644 src/Prey/game_console.h create mode 100644 src/Prey/game_damagetester.cpp create mode 100644 src/Prey/game_damagetester.h create mode 100644 src/Prey/game_dda.cpp create mode 100644 src/Prey/game_dda.h create mode 100644 src/Prey/game_deathwraith.cpp create mode 100644 src/Prey/game_deathwraith.h create mode 100644 src/Prey/game_debrisspawner.cpp create mode 100644 src/Prey/game_debrisspawner.h create mode 100644 src/Prey/game_dock.cpp create mode 100644 src/Prey/game_dock.h create mode 100644 src/Prey/game_dockedgun.cpp create mode 100644 src/Prey/game_dockedgun.h create mode 100644 src/Prey/game_door.cpp create mode 100644 src/Prey/game_door.h create mode 100644 src/Prey/game_eggspawner.cpp create mode 100644 src/Prey/game_eggspawner.h create mode 100644 src/Prey/game_energynode.cpp create mode 100644 src/Prey/game_energynode.h create mode 100644 src/Prey/game_entityfx.cpp create mode 100644 src/Prey/game_entityfx.h create mode 100644 src/Prey/game_entityspawner.cpp create mode 100644 src/Prey/game_entityspawner.h create mode 100644 src/Prey/game_events.cpp create mode 100644 src/Prey/game_events.h create mode 100644 src/Prey/game_fixedpod.cpp create mode 100644 src/Prey/game_fixedpod.h create mode 100644 src/Prey/game_forcefield.cpp create mode 100644 src/Prey/game_forcefield.h create mode 100644 src/Prey/game_fxinfo.cpp create mode 100644 src/Prey/game_fxinfo.h create mode 100644 src/Prey/game_gibbable.cpp create mode 100644 src/Prey/game_gibbable.h create mode 100644 src/Prey/game_gravityswitch.cpp create mode 100644 src/Prey/game_gravityswitch.h create mode 100644 src/Prey/game_guihand.cpp create mode 100644 src/Prey/game_guihand.h create mode 100644 src/Prey/game_gun.cpp create mode 100644 src/Prey/game_gun.h create mode 100644 src/Prey/game_hand.cpp create mode 100644 src/Prey/game_hand.h create mode 100644 src/Prey/game_handcontrol.cpp create mode 100644 src/Prey/game_handcontrol.h create mode 100644 src/Prey/game_healthbasin.cpp create mode 100644 src/Prey/game_healthbasin.h create mode 100644 src/Prey/game_healthspore.cpp create mode 100644 src/Prey/game_healthspore.h create mode 100644 src/Prey/game_inventory.cpp create mode 100644 src/Prey/game_inventory.h create mode 100644 src/Prey/game_itemautomatic.cpp create mode 100644 src/Prey/game_itemautomatic.h create mode 100644 src/Prey/game_itemcabinet.cpp create mode 100644 src/Prey/game_itemcabinet.h create mode 100644 src/Prey/game_jukebox.cpp create mode 100644 src/Prey/game_jukebox.h create mode 100644 src/Prey/game_jumpzone.cpp create mode 100644 src/Prey/game_jumpzone.h create mode 100644 src/Prey/game_light.cpp create mode 100644 src/Prey/game_light.h create mode 100644 src/Prey/game_lightfixture.cpp create mode 100644 src/Prey/game_lightfixture.h create mode 100644 src/Prey/game_mine.cpp create mode 100644 src/Prey/game_mine.h create mode 100644 src/Prey/game_misc.cpp create mode 100644 src/Prey/game_misc.h create mode 100644 src/Prey/game_modeldoor.cpp create mode 100644 src/Prey/game_modeldoor.h create mode 100644 src/Prey/game_modeltoggle.cpp create mode 100644 src/Prey/game_modeltoggle.h create mode 100644 src/Prey/game_monster_ai.cpp create mode 100644 src/Prey/game_monster_ai.h create mode 100644 src/Prey/game_monster_ai_events.cpp create mode 100644 src/Prey/game_mountedgun.cpp create mode 100644 src/Prey/game_mountedgun.h create mode 100644 src/Prey/game_moveable.cpp create mode 100644 src/Prey/game_moveable.h create mode 100644 src/Prey/game_mover.cpp create mode 100644 src/Prey/game_mover.h create mode 100644 src/Prey/game_note.cpp create mode 100644 src/Prey/game_note.h create mode 100644 src/Prey/game_organtrigger.cpp create mode 100644 src/Prey/game_organtrigger.h create mode 100644 src/Prey/game_player.cpp create mode 100644 src/Prey/game_player.h create mode 100644 src/Prey/game_playerview.cpp create mode 100644 src/Prey/game_playerview.h create mode 100644 src/Prey/game_pod.cpp create mode 100644 src/Prey/game_pod.h create mode 100644 src/Prey/game_podspawner.cpp create mode 100644 src/Prey/game_podspawner.h create mode 100644 src/Prey/game_poker.cpp create mode 100644 src/Prey/game_poker.h create mode 100644 src/Prey/game_portal.cpp create mode 100644 src/Prey/game_portal.h create mode 100644 src/Prey/game_portalframe.cpp create mode 100644 src/Prey/game_portalframe.h create mode 100644 src/Prey/game_proxdoor.cpp create mode 100644 src/Prey/game_proxdoor.h create mode 100644 src/Prey/game_rail.cpp create mode 100644 src/Prey/game_rail.h create mode 100644 src/Prey/game_railshuttle.cpp create mode 100644 src/Prey/game_railshuttle.h create mode 100644 src/Prey/game_renderentity.cpp create mode 100644 src/Prey/game_renderentity.h create mode 100644 src/Prey/game_safeDeathVolume.cpp create mode 100644 src/Prey/game_safeDeathVolume.h create mode 100644 src/Prey/game_securityeye.cpp create mode 100644 src/Prey/game_securityeye.h create mode 100644 src/Prey/game_shuttle.cpp create mode 100644 src/Prey/game_shuttle.h create mode 100644 src/Prey/game_shuttledock.cpp create mode 100644 src/Prey/game_shuttledock.h create mode 100644 src/Prey/game_shuttletransport.cpp create mode 100644 src/Prey/game_shuttletransport.h create mode 100644 src/Prey/game_skybox.cpp create mode 100644 src/Prey/game_skybox.h create mode 100644 src/Prey/game_slots.cpp create mode 100644 src/Prey/game_slots.h create mode 100644 src/Prey/game_sphere.cpp create mode 100644 src/Prey/game_sphere.h create mode 100644 src/Prey/game_spherepart.cpp create mode 100644 src/Prey/game_spherepart.h create mode 100644 src/Prey/game_spring.cpp create mode 100644 src/Prey/game_spring.h create mode 100644 src/Prey/game_sunCorona.cpp create mode 100644 src/Prey/game_sunCorona.h create mode 100644 src/Prey/game_talon.cpp create mode 100644 src/Prey/game_talon.h create mode 100644 src/Prey/game_targetproxy.cpp create mode 100644 src/Prey/game_targetproxy.h create mode 100644 src/Prey/game_targets.cpp create mode 100644 src/Prey/game_targets.h create mode 100644 src/Prey/game_trackmover.cpp create mode 100644 src/Prey/game_trackmover.h create mode 100644 src/Prey/game_trigger.cpp create mode 100644 src/Prey/game_trigger.h create mode 100644 src/Prey/game_tripwire.cpp create mode 100644 src/Prey/game_tripwire.h create mode 100644 src/Prey/game_utils.cpp create mode 100644 src/Prey/game_utils.h create mode 100644 src/Prey/game_vehicle.cpp create mode 100644 src/Prey/game_vehicle.h create mode 100644 src/Prey/game_vomiter.cpp create mode 100644 src/Prey/game_vomiter.h create mode 100644 src/Prey/game_weaponHandState.cpp create mode 100644 src/Prey/game_weaponHandState.h create mode 100644 src/Prey/game_woundmanager.cpp create mode 100644 src/Prey/game_woundmanager.h create mode 100644 src/Prey/game_wraith.cpp create mode 100644 src/Prey/game_wraith.h create mode 100644 src/Prey/game_zone.cpp create mode 100644 src/Prey/game_zone.h create mode 100644 src/Prey/particles_particles.cpp create mode 100644 src/Prey/particles_particles.h create mode 100644 src/Prey/physics_delta.cpp create mode 100644 src/Prey/physics_delta.h create mode 100644 src/Prey/physics_preyai.cpp create mode 100644 src/Prey/physics_preyai.h create mode 100644 src/Prey/physics_preyparametric.cpp create mode 100644 src/Prey/physics_preyparametric.h create mode 100644 src/Prey/physics_simple.cpp create mode 100644 src/Prey/physics_simple.h create mode 100644 src/Prey/physics_vehicle.cpp create mode 100644 src/Prey/physics_vehicle.h create mode 100644 src/Prey/prey_animator.cpp create mode 100644 src/Prey/prey_animator.h create mode 100644 src/Prey/prey_baseweapons.cpp create mode 100644 src/Prey/prey_baseweapons.h create mode 100644 src/Prey/prey_beam.cpp create mode 100644 src/Prey/prey_beam.h create mode 100644 src/Prey/prey_bonecontroller.cpp create mode 100644 src/Prey/prey_bonecontroller.h create mode 100644 src/Prey/prey_camerainterpolator.cpp create mode 100644 src/Prey/prey_camerainterpolator.h create mode 100644 src/Prey/prey_firecontroller.cpp create mode 100644 src/Prey/prey_firecontroller.h create mode 100644 src/Prey/prey_game.cpp create mode 100644 src/Prey/prey_game.h create mode 100644 src/Prey/prey_items.cpp create mode 100644 src/Prey/prey_items.h create mode 100644 src/Prey/prey_liquid.cpp create mode 100644 src/Prey/prey_liquid.h create mode 100644 src/Prey/prey_local.cpp create mode 100644 src/Prey/prey_local.h create mode 100644 src/Prey/prey_projectile.cpp create mode 100644 src/Prey/prey_projectile.h create mode 100644 src/Prey/prey_projectileautocannon.cpp create mode 100644 src/Prey/prey_projectileautocannon.h create mode 100644 src/Prey/prey_projectilebounce.cpp create mode 100644 src/Prey/prey_projectilebounce.h create mode 100644 src/Prey/prey_projectilebug.cpp create mode 100644 src/Prey/prey_projectilebug.h create mode 100644 src/Prey/prey_projectilebugtrigger.cpp create mode 100644 src/Prey/prey_projectilebugtrigger.h create mode 100644 src/Prey/prey_projectilecocoon.cpp create mode 100644 src/Prey/prey_projectilecocoon.h create mode 100644 src/Prey/prey_projectilecrawlergrenade.cpp create mode 100644 src/Prey/prey_projectilecrawlergrenade.h create mode 100644 src/Prey/prey_projectilefreezer.cpp create mode 100644 src/Prey/prey_projectilefreezer.h create mode 100644 src/Prey/prey_projectilegasbagpod.cpp create mode 100644 src/Prey/prey_projectilegasbagpod.h create mode 100644 src/Prey/prey_projectilehiderweapon.cpp create mode 100644 src/Prey/prey_projectilehiderweapon.h create mode 100644 src/Prey/prey_projectilemine.cpp create mode 100644 src/Prey/prey_projectilemine.h create mode 100644 src/Prey/prey_projectilerifle.cpp create mode 100644 src/Prey/prey_projectilerifle.h create mode 100644 src/Prey/prey_projectilerocketlauncher.cpp create mode 100644 src/Prey/prey_projectilerocketlauncher.h create mode 100644 src/Prey/prey_projectileshuttle.cpp create mode 100644 src/Prey/prey_projectileshuttle.h create mode 100644 src/Prey/prey_projectilesoulcannon.cpp create mode 100644 src/Prey/prey_projectilesoulcannon.h create mode 100644 src/Prey/prey_projectilespiritarrow.cpp create mode 100644 src/Prey/prey_projectilespiritarrow.h create mode 100644 src/Prey/prey_projectiletracking.cpp create mode 100644 src/Prey/prey_projectiletracking.h create mode 100644 src/Prey/prey_projectiletrigger.cpp create mode 100644 src/Prey/prey_projectiletrigger.h create mode 100644 src/Prey/prey_projectilewrench.cpp create mode 100644 src/Prey/prey_projectilewrench.h create mode 100644 src/Prey/prey_script_thread.cpp create mode 100644 src/Prey/prey_script_thread.h create mode 100644 src/Prey/prey_sound.cpp create mode 100644 src/Prey/prey_sound.h create mode 100644 src/Prey/prey_soundleadincontroller.cpp create mode 100644 src/Prey/prey_soundleadincontroller.h create mode 100644 src/Prey/prey_spiritbridge.cpp create mode 100644 src/Prey/prey_spiritbridge.h create mode 100644 src/Prey/prey_spiritproxy.cpp create mode 100644 src/Prey/prey_spiritproxy.h create mode 100644 src/Prey/prey_spiritsecret.cpp create mode 100644 src/Prey/prey_spiritsecret.h create mode 100644 src/Prey/prey_vehiclefirecontroller.cpp create mode 100644 src/Prey/prey_vehiclefirecontroller.h create mode 100644 src/Prey/prey_weapon.cpp create mode 100644 src/Prey/prey_weapon.h create mode 100644 src/Prey/prey_weaponautocannon.cpp create mode 100644 src/Prey/prey_weaponautocannon.h create mode 100644 src/Prey/prey_weaponcrawlergrenade.cpp create mode 100644 src/Prey/prey_weaponcrawlergrenade.h create mode 100644 src/Prey/prey_weaponfirecontroller.cpp create mode 100644 src/Prey/prey_weaponfirecontroller.h create mode 100644 src/Prey/prey_weaponhider.cpp create mode 100644 src/Prey/prey_weaponhider.h create mode 100644 src/Prey/prey_weaponrifle.cpp create mode 100644 src/Prey/prey_weaponrifle.h create mode 100644 src/Prey/prey_weaponrocketlauncher.cpp create mode 100644 src/Prey/prey_weaponrocketlauncher.h create mode 100644 src/Prey/prey_weaponsoulstripper.cpp create mode 100644 src/Prey/prey_weaponsoulstripper.h create mode 100644 src/Prey/prey_weaponspiritbow.cpp create mode 100644 src/Prey/prey_weaponspiritbow.h create mode 100644 src/Prey/sys_debugger.cpp create mode 100644 src/Prey/sys_debugger.h create mode 100644 src/Prey/sys_preycmds.cpp create mode 100644 src/Prey/sys_preycmds.h create mode 100644 src/cm/CollisionModel.h create mode 100644 src/cm/CollisionModel_contacts.cpp create mode 100644 src/cm/CollisionModel_contents.cpp create mode 100644 src/cm/CollisionModel_debug.cpp create mode 100644 src/cm/CollisionModel_files.cpp create mode 100644 src/cm/CollisionModel_load.cpp create mode 100644 src/cm/CollisionModel_local.h create mode 100644 src/cm/CollisionModel_rotate.cpp create mode 100644 src/cm/CollisionModel_trace.cpp create mode 100644 src/cm/CollisionModel_translate.cpp create mode 100644 src/framework/BuildDefines.h create mode 100644 src/framework/BuildVersion.h create mode 100644 src/framework/CVarSystem.h create mode 100644 src/framework/CmdSystem.h create mode 100644 src/framework/Common.h create mode 100644 src/framework/DeclPDA.h create mode 100644 src/framework/File.h create mode 100644 src/framework/FileSystem.h create mode 100644 src/framework/UsercmdGen.h create mode 100644 src/framework/async/NetworkSystem.h create mode 100644 src/framework/declAF.h create mode 100644 src/framework/declEntityDef.h create mode 100644 src/framework/declFX.h create mode 100644 src/framework/declManager.h create mode 100644 src/framework/declParticle.h create mode 100644 src/framework/declPreyBeam.h create mode 100644 src/framework/declSkin.h create mode 100644 src/framework/declTable.h create mode 100644 src/framework/dotnetwarnings.h create mode 100644 src/framework/licensee.h create mode 100644 src/game/AF.cpp create mode 100644 src/game/AF.h create mode 100644 src/game/AFEntity.cpp create mode 100644 src/game/AFEntity.h create mode 100644 src/game/Actor.cpp create mode 100644 src/game/Actor.h create mode 100644 src/game/BrittleFracture.cpp create mode 100644 src/game/BrittleFracture.h create mode 100644 src/game/Camera.cpp create mode 100644 src/game/Camera.h create mode 100644 src/game/Entity.cpp create mode 100644 src/game/Entity.h create mode 100644 src/game/EntityAdditions.cpp create mode 100644 src/game/Fx.cpp create mode 100644 src/game/Fx.h create mode 100644 src/game/Game.def create mode 100644 src/game/Game.h create mode 100644 src/game/GameEdit.cpp create mode 100644 src/game/GameEdit.h create mode 100644 src/game/Game_local.cpp create mode 100644 src/game/Game_local.h create mode 100644 src/game/Game_network.cpp create mode 100644 src/game/IK.cpp create mode 100644 src/game/IK.h create mode 100644 src/game/Item.cpp create mode 100644 src/game/Item.h create mode 100644 src/game/Light.cpp create mode 100644 src/game/Light.h create mode 100644 src/game/Misc.cpp create mode 100644 src/game/Misc.h create mode 100644 src/game/Moveable.cpp create mode 100644 src/game/Moveable.h create mode 100644 src/game/Mover.cpp create mode 100644 src/game/Mover.h create mode 100644 src/game/MultiplayerGame.cpp create mode 100644 src/game/MultiplayerGame.h create mode 100644 src/game/Player.cpp create mode 100644 src/game/Player.h create mode 100644 src/game/PlayerIcon.cpp create mode 100644 src/game/PlayerIcon.h create mode 100644 src/game/PlayerView.cpp create mode 100644 src/game/PlayerView.h create mode 100644 src/game/Pvs.cpp create mode 100644 src/game/Pvs.h create mode 100644 src/game/SecurityCamera.cpp create mode 100644 src/game/SecurityCamera.h create mode 100644 src/game/SmokeParticles.cpp create mode 100644 src/game/SmokeParticles.h create mode 100644 src/game/Sound.cpp create mode 100644 src/game/Sound.h create mode 100644 src/game/Target.cpp create mode 100644 src/game/Target.h create mode 100644 src/game/Weapon.cpp create mode 100644 src/game/Weapon.h create mode 100644 src/game/WorldSpawn.cpp create mode 100644 src/game/WorldSpawn.h create mode 100644 src/game/ai/AAS.cpp create mode 100644 src/game/ai/AAS.h create mode 100644 src/game/ai/AAS_NearPoint.cpp create mode 100644 src/game/ai/AAS_debug.cpp create mode 100644 src/game/ai/AAS_local.h create mode 100644 src/game/ai/AAS_pathing.cpp create mode 100644 src/game/ai/AAS_routing.cpp create mode 100644 src/game/ai/AI.cpp create mode 100644 src/game/ai/AI.h create mode 100644 src/game/ai/AI_events.cpp create mode 100644 src/game/ai/AI_pathing.cpp create mode 100644 src/game/anim/Anim.cpp create mode 100644 src/game/anim/Anim.h create mode 100644 src/game/anim/Anim_Blend.cpp create mode 100644 src/game/anim/Anim_Import.cpp create mode 100644 src/game/anim/Anim_Testmodel.cpp create mode 100644 src/game/anim/Anim_Testmodel.h create mode 100644 src/game/gamesys/Callbacks.cpp create mode 100644 src/game/gamesys/Class.cpp create mode 100644 src/game/gamesys/Class.h create mode 100644 src/game/gamesys/DebugGraph.cpp create mode 100644 src/game/gamesys/DebugGraph.h create mode 100644 src/game/gamesys/Event.cpp create mode 100644 src/game/gamesys/Event.h create mode 100644 src/game/gamesys/NoGameTypeInfo.h create mode 100644 src/game/gamesys/SaveGame.cpp create mode 100644 src/game/gamesys/SaveGame.h create mode 100644 src/game/gamesys/SysCmds.cpp create mode 100644 src/game/gamesys/SysCmds.h create mode 100644 src/game/gamesys/SysCvar.cpp create mode 100644 src/game/gamesys/SysCvar.h create mode 100644 src/game/gamesys/TypeInfo.cpp create mode 100644 src/game/gamesys/TypeInfo.h create mode 100644 src/game/physics/Clip.cpp create mode 100644 src/game/physics/Clip.h create mode 100644 src/game/physics/Force.cpp create mode 100644 src/game/physics/Force.h create mode 100644 src/game/physics/Force_Constant.cpp create mode 100644 src/game/physics/Force_Constant.h create mode 100644 src/game/physics/Force_Drag.cpp create mode 100644 src/game/physics/Force_Drag.h create mode 100644 src/game/physics/Force_Field.cpp create mode 100644 src/game/physics/Force_Field.h create mode 100644 src/game/physics/Force_Spring.cpp create mode 100644 src/game/physics/Force_Spring.h create mode 100644 src/game/physics/Physics.cpp create mode 100644 src/game/physics/Physics.h create mode 100644 src/game/physics/Physics_AF.cpp create mode 100644 src/game/physics/Physics_AF.h create mode 100644 src/game/physics/Physics_Actor.cpp create mode 100644 src/game/physics/Physics_Actor.h create mode 100644 src/game/physics/Physics_Base.cpp create mode 100644 src/game/physics/Physics_Base.h create mode 100644 src/game/physics/Physics_Monster.cpp create mode 100644 src/game/physics/Physics_Monster.h create mode 100644 src/game/physics/Physics_Parametric.cpp create mode 100644 src/game/physics/Physics_Parametric.h create mode 100644 src/game/physics/Physics_Player.cpp create mode 100644 src/game/physics/Physics_Player.h create mode 100644 src/game/physics/Physics_PreyPlayer.cpp create mode 100644 src/game/physics/Physics_PreyPlayer.h create mode 100644 src/game/physics/Physics_RigidBody.cpp create mode 100644 src/game/physics/Physics_RigidBody.h create mode 100644 src/game/physics/Physics_Static.cpp create mode 100644 src/game/physics/Physics_Static.h create mode 100644 src/game/physics/Physics_StaticMulti.cpp create mode 100644 src/game/physics/Physics_StaticMulti.h create mode 100644 src/game/physics/Push.cpp create mode 100644 src/game/physics/Push.h create mode 100644 src/game/projectile.cpp create mode 100644 src/game/projectile.h create mode 100644 src/game/script/Script_Compiler.cpp create mode 100644 src/game/script/Script_Compiler.h create mode 100644 src/game/script/Script_Interpreter.cpp create mode 100644 src/game/script/Script_Interpreter.h create mode 100644 src/game/script/Script_Program.cpp create mode 100644 src/game/script/Script_Program.h create mode 100644 src/game/script/Script_Thread.cpp create mode 100644 src/game/script/Script_Thread.h create mode 100644 src/game/trigger.cpp create mode 100644 src/game/trigger.h create mode 100644 src/idLib/Base64.cpp create mode 100644 src/idLib/Base64.h create mode 100644 src/idLib/BitMsg.cpp create mode 100644 src/idLib/BitMsg.h create mode 100644 src/idLib/CmdArgs.cpp create mode 100644 src/idLib/CmdArgs.h create mode 100644 src/idLib/Dict.cpp create mode 100644 src/idLib/Dict.h create mode 100644 src/idLib/Heap.cpp create mode 100644 src/idLib/Heap.h create mode 100644 src/idLib/LangDict.cpp create mode 100644 src/idLib/LangDict.h create mode 100644 src/idLib/Lexer.cpp create mode 100644 src/idLib/Lexer.h create mode 100644 src/idLib/Lib.cpp create mode 100644 src/idLib/Lib.h create mode 100644 src/idLib/Parser.cpp create mode 100644 src/idLib/Parser.h create mode 100644 src/idLib/Str.cpp create mode 100644 src/idLib/Str.h create mode 100644 src/idLib/Timer.cpp create mode 100644 src/idLib/Timer.h create mode 100644 src/idLib/Token.cpp create mode 100644 src/idLib/Token.h create mode 100644 src/idLib/bv/Bounds.cpp create mode 100644 src/idLib/bv/Bounds.h create mode 100644 src/idLib/bv/Box.cpp create mode 100644 src/idLib/bv/Box.h create mode 100644 src/idLib/bv/Frustum.cpp create mode 100644 src/idLib/bv/Frustum.h create mode 100644 src/idLib/bv/Frustum_gcc.cpp create mode 100644 src/idLib/bv/Sphere.cpp create mode 100644 src/idLib/bv/Sphere.h create mode 100644 src/idLib/containers/BTree.h create mode 100644 src/idLib/containers/BinSearch.h create mode 100644 src/idLib/containers/HashIndex.cpp create mode 100644 src/idLib/containers/HashIndex.h create mode 100644 src/idLib/containers/HashTable.h create mode 100644 src/idLib/containers/Hierarchy.h create mode 100644 src/idLib/containers/LinkList.h create mode 100644 src/idLib/containers/List.h create mode 100644 src/idLib/containers/PlaneSet.h create mode 100644 src/idLib/containers/PreyStack.h create mode 100644 src/idLib/containers/Queue.h create mode 100644 src/idLib/containers/Stack.h create mode 100644 src/idLib/containers/StaticList.h create mode 100644 src/idLib/containers/StrList.h create mode 100644 src/idLib/containers/StrPool.h create mode 100644 src/idLib/containers/VectorSet.h create mode 100644 src/idLib/geometry/DrawVert.cpp create mode 100644 src/idLib/geometry/DrawVert.h create mode 100644 src/idLib/geometry/JointTransform.cpp create mode 100644 src/idLib/geometry/JointTransform.h create mode 100644 src/idLib/geometry/Surface.cpp create mode 100644 src/idLib/geometry/Surface.h create mode 100644 src/idLib/geometry/Surface_Patch.cpp create mode 100644 src/idLib/geometry/Surface_Patch.h create mode 100644 src/idLib/geometry/Surface_Polytope.cpp create mode 100644 src/idLib/geometry/Surface_Polytope.h create mode 100644 src/idLib/geometry/Surface_SweptSpline.cpp create mode 100644 src/idLib/geometry/Surface_SweptSpline.h create mode 100644 src/idLib/geometry/TraceModel.cpp create mode 100644 src/idLib/geometry/TraceModel.h create mode 100644 src/idLib/geometry/Winding.cpp create mode 100644 src/idLib/geometry/Winding.h create mode 100644 src/idLib/geometry/Winding2D.cpp create mode 100644 src/idLib/geometry/Winding2D.h create mode 100644 src/idLib/hashing/CRC16.cpp create mode 100644 src/idLib/hashing/CRC16.h create mode 100644 src/idLib/hashing/CRC32.cpp create mode 100644 src/idLib/hashing/CRC32.h create mode 100644 src/idLib/hashing/CRC8.cpp create mode 100644 src/idLib/hashing/CRC8.h create mode 100644 src/idLib/hashing/Honeyman.cpp create mode 100644 src/idLib/hashing/Honeyman.h create mode 100644 src/idLib/hashing/MD4.cpp create mode 100644 src/idLib/hashing/MD4.h create mode 100644 src/idLib/hashing/MD5.cpp create mode 100644 src/idLib/hashing/MD5.h create mode 100644 src/idLib/mapfile.cpp create mode 100644 src/idLib/mapfile.h create mode 100644 src/idLib/math/Angles.cpp create mode 100644 src/idLib/math/Angles.h create mode 100644 src/idLib/math/Complex.cpp create mode 100644 src/idLib/math/Complex.h create mode 100644 src/idLib/math/Curve.h create mode 100644 src/idLib/math/Extrapolate.h create mode 100644 src/idLib/math/Interpolate.h create mode 100644 src/idLib/math/Lcp.cpp create mode 100644 src/idLib/math/Lcp.h create mode 100644 src/idLib/math/Math.cpp create mode 100644 src/idLib/math/Math.h create mode 100644 src/idLib/math/Matrix.cpp create mode 100644 src/idLib/math/Matrix.h create mode 100644 src/idLib/math/Ode.cpp create mode 100644 src/idLib/math/Ode.h create mode 100644 src/idLib/math/Plane.cpp create mode 100644 src/idLib/math/Plane.h create mode 100644 src/idLib/math/Pluecker.cpp create mode 100644 src/idLib/math/Pluecker.h create mode 100644 src/idLib/math/Polynomial.cpp create mode 100644 src/idLib/math/Polynomial.h create mode 100644 src/idLib/math/Quat.cpp create mode 100644 src/idLib/math/Quat.h create mode 100644 src/idLib/math/Random.h create mode 100644 src/idLib/math/Rotation.cpp create mode 100644 src/idLib/math/Rotation.h create mode 100644 src/idLib/math/Simd.cpp create mode 100644 src/idLib/math/Simd.h create mode 100644 src/idLib/math/Simd_3DNow.cpp create mode 100644 src/idLib/math/Simd_3DNow.h create mode 100644 src/idLib/math/Simd_AltiVec.cpp create mode 100644 src/idLib/math/Simd_AltiVec.h create mode 100644 src/idLib/math/Simd_MMX.cpp create mode 100644 src/idLib/math/Simd_MMX.h create mode 100644 src/idLib/math/Simd_SSE.cpp create mode 100644 src/idLib/math/Simd_SSE.h create mode 100644 src/idLib/math/Simd_SSE2.cpp create mode 100644 src/idLib/math/Simd_SSE2.h create mode 100644 src/idLib/math/Simd_SSE3.cpp create mode 100644 src/idLib/math/Simd_SSE3.h create mode 100644 src/idLib/math/Simd_generic.cpp create mode 100644 src/idLib/math/Simd_generic.h create mode 100644 src/idLib/math/Vector.cpp create mode 100644 src/idLib/math/Vector.h create mode 100644 src/idLib/math/prey_interpolate.h create mode 100644 src/idLib/math/prey_math.cpp create mode 100644 src/idLib/math/prey_math.h create mode 100644 src/idLib/precompiled.cpp create mode 100644 src/idLib/precompiled.h create mode 100644 src/preyengine/profiler.h create mode 100644 src/renderer/Cinematic.h create mode 100644 src/renderer/Material.h create mode 100644 src/renderer/Model.h create mode 100644 src/renderer/ModelManager.h create mode 100644 src/renderer/Model_local.h create mode 100644 src/renderer/RenderSystem.h create mode 100644 src/renderer/RenderWorld.h create mode 100644 src/renderer/glext.h create mode 100644 src/renderer/qgl.h create mode 100644 src/renderer/qgl_linked.h create mode 100644 src/sound/sound.h create mode 100644 src/sys/linux/qgl_enforce.h create mode 100644 src/sys/scons/SConscript.game create mode 100644 src/sys/scons/SConscript.idlib create mode 100644 src/sys/scons/scons_utils.py create mode 100644 src/sys/sys_public.h create mode 100644 src/tools/compilers/aas/AASFile.h create mode 100644 src/tools/compilers/aas/AASFileManager.h create mode 100644 src/ui/ListGUI.h create mode 100644 src/ui/UserInterface.h diff --git a/EULA.Development Kit.rtf b/EULA.Development Kit.rtf new file mode 100644 index 0000000..79293f3 --- /dev/null +++ b/EULA.Development Kit.rtf @@ -0,0 +1,58 @@ +{\rtf1\ansi\ansicpg1252\deff0\deflang1033\deflangfe1033{\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\fmodern\fprq1\fcharset0 Courier New;}} +{\*\generator Msftedit 5.41.15.1507;}\viewkind4\uc1\pard\qc\b\f0\fs22 PREY SOFTWARE DEVELOPMENT KIT\par +LIMITED USE LICENSE AGREEMENT\par +\pard\qj\b0\par +\pard\fi720\qj This \i PREY\i0 Software Development Kit Limited Use License Agreement (this "Agreement") is a legal agreement among you, the end-user, and Human Head Studios, Inc. ("Human Head Studios"). \b BY CONTINUING THE DOWNLOAD OR INSTALLATION OF THIS SOFTWARE DEVELOPMENT KIT (THE "SOFTWARE") FOR THE GAME PROGRAM ENTITLED \i PREY\i0 , BY LOADING OR RUNNING THE SOFTWARE, OR BY PLACING OR COPYING THE SOFTWARE ONTO YOUR COMPUTER HARD DRIVE, COMPUTER RAM, OR OTHER STORAGE, YOU ARE AGREEING TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS AGREEMENT. YOU ACKNOWLEDGE AND UNDERSTAND THAT IN ORDER TO OPERATE THE SOFTWARE, YOU MUST HAVE THE FULL VERSION OF THE HUMAN HEAD STUDIOS GAME ENTITLED \i PREY\i0 INSTALLED ON YOUR COMPUTER.\b0\par +\par +\b 1.\tab Grant of License.\b0 Subject to the terms and provisions of this Agreement and so long as you fully comply at all times with this Agreement, Human Head Studios grants to you the non-exclusive and limited right to use the Software only in executable or object code form. The term "Software" includes all elements of the Software, including, without limitation, data files and screen displays. You are not receiving any ownership or proprietary right, title, or interest in or to the Software or the copyrights, trademarks, or other rights related thereto. For purposes of the first sentence of this Section, "use" means loading the Software into RAM and/or onto computer hard drive, as well as installation of the Software on a hard disk or other storage device, and means the uses permitted in Sections 2 and 5 hereinbelow. You agree that the Software will not be downloaded, shipped, transferred, exported or re\_exported into any country in violation of the United States Export Administration Act (or any other law governing such matters) by you or anyone at your direction, and that you will not utilize and will not authorize anyone to utilize the Software in any other manner in violation of any applicable law. The Software shall not be downloaded or otherwise exported or re\_exported into (or to a national or resident of) any country to which the United States has embargoed goods, or to anyone or into any country who/that are prohibited, by applicable law, from receiving such property. In exercising your limited rights hereunder, you shall comply, at all times, with all applicable laws, regulations, ordinances, and statutes. Human Head Studios reserves all rights not granted in this Agreement, including, without limitation, all rights to Human Head Studios' trademarks.\f1\par +\par +\b\f0 2.\tab Permitted New Creations.\b0 Subject to the terms and provisions of this Agreement and so long as you fully comply at all times with this Agreement, Human Head Studios grants to you the non-exclusive and limited right to use the Software to create for the software game \i PREY\i0 your own modifications (the "New Creations") that shall operate only with \i PREY\i0 (but not any demo, test, or other version of \i PREY\i0 ). You may include within the New Creations certain textures and other images (the "Software Images") from the Software. You shall not create any New Creations that infringe against any third-party right or that are libelous, defamatory, obscene, false, misleading, or otherwise illegal or unlawful. You agree that the New Creations will not be downloaded, shipped, transferred, exported, or re\_exported into any country in violation of the United States Export Administration Act (or any other law governing such matters) by you or anyone at your direction, and that you will not utilize and will not authorize anyone to utilize the New Creations in any other manner in violation of any applicable law. The New Creations shall not be downloaded or otherwise exported or re\_exported into (or to a national or resident of) any country to which the United States has embargoed goods or to anyone or into any country who/that are prohibited, by applicable law, from receiving such property. You shall not rent, sell, lease, lend, offer on a pay-per-play basis, or otherwise commercially exploit or commercially distribute the New Creations. You are permitted to distribute, without any cost or charge, the New Creations only to other end-users so long as such distribution is not infringing against any third-party right and otherwise is not illegal or unlawful. As noted below, in the event you commit any breach of this Agreement, your license and this Agreement automatically shall terminate, without notice.\par +\par +\b 3.\tab Prohibitions with Regard to the Software.\b0 You, whether directly or indirectly, shall \b not\b0 do any of the following acts:\par +\par +\pard\fi-720\li1840\qj a.\tab rent the Software;\par +\par +b.\tab sell the Software;\par +\par +c.\tab lease or lend the Software;\par +\par +d.\tab offer the Software on a pay-per-play basis;\par +\par +e.\tab distribute the Software (except as permitted under Section 5 hereinbelow);\par +\par +f.\tab in any other manner and through any medium whatsoever commercially exploit the Software or use the Software for any commercial purpose;\par +\par +g.\tab disassemble, reverse engineer, decompile, modify (except as permitted under Section\~2 hereinabove) or alter the Software;\par +\par +h.\tab translate the Software;\par +\par +i.\tab reproduce or copy the Software (except as permitted under Section 5 hereinbelow);\par +\par +j.\tab publicly display the Software;\par +\par +k.\tab prepare or develop derivative works based upon the Software;\par +\par +l.\tab remove or alter any notices or other markings or legends, such as trademark or copyright notices, affixed on or within the Software; or\par +\par +m.\tab remove, alter, modify, disable, or reduce any of the anti-piracy measures contained in the Software or in \i PREY\i0 , including, without limitation, measures relating to multiplayer play.\par +\pard\qj\par +\pard\fi720\qj\b 4.\tab Prohibition against Cheat Programs.\b0 Any attempt by you, either directly or indirectly, to circumvent or bypass any element of the Software to gain any advantage in multiplayer play of the Software is a material breach of this Agreement. It is a material breach of this Agreement for you, whether directly or indirectly, to create, develop, copy, reproduce, distribute, or otherwise make any use of any software program or any modification to the Software ("Cheat Program") itself that enables or allows the user thereof to obtain an advantage or otherwise exploit another Software player or user when playing the Software against other players or users on a local area network, any other network, or on the Internet. Hacking into the executable of the Software, modification of the Software, or any other use of the Software in connection with the creation, development, or use of any such unauthorized Cheat Program is a material breach of this Agreement. Cheat Programs include, but are not limited to,\b \b0 programs that allow Software players or users to see through walls or other level geometry; programs that allow Software players or users to change their rate of speed outside the allowable limits of the Software; programs that crash either and/or other Software players, users, PC clients, or network servers; programs that automatically target other Software players or users (commonly referred to as "aimbots") that automatically simulate Software player or user input for the purpose of gaining an advantage over other Software players or users; or any other program or modification that functions in a similar capacity or allows any prohibited conduct.\par +\par +In the event you breach this Section or otherwise breach this Agreement, your license and this Agreement automatically shall terminate, without notice, and you shall have no right to play the Software against other players or make any other use of the Software.\par +\b\par +5.\tab Permitted Distribution and Copying.\b0 So long as this Agreement accompanies each copy you make of the Software, and so long as you comply fully at all times with this Agreement, Human Head Studios grants to you the non-exclusive and limited right to copy the Software and to distribute such copies of the Software free of charge for non-commercial purposes that shall include the free-of-charge distribution of copies of the Software as mounted on the covers of magazines; provided, however, you shall \b not \b0 copy or distribute the Software in any infringing manner or in any manner that violates any law or third-party right, and you shall \b not\b0 distribute the Software together with any material that infringes against any third-party right or that is libelous, defamatory, obscene, false, misleading, or otherwise illegal or unlawful. Subject to the terms and conditions of this Agreement, you also may: (i) download one (1) copy of the Software or copy the Software from the CD ROM on which you received your copy of the Software onto your computer RAM; (ii) copy the Software from your computer RAM onto your computer hard drive; and (iii)\~make one (1) "backup" or archival copy of the Software on one (1) hard disk. In exercising your limited rights hereunder, you shall comply at all times with all applicable laws, regulations, ordinances, and statutes. Human Head Studios reserves all rights not granted in this Agreement. \b You shall not distribute the Software commercially unless you first enter into a separate contract with Human Head Studios, on terms and conditions determined in Human Head Studios' sole discretion, and only upon your receipt of a written agreement executed by an authorized officer of Human Head Studios.\b0\par +\par +\b 6.\tab Intellectual Property Rights.\b0 The Software and all copyrights, trademarks, and all other conceivable intellectual property rights related to the Software are owned by Human Head Studios and are protected by United States copyright laws, international treaty provisions, and all applicable law, such as the Lanham Act. You must treat the Software like any other copyrighted material, as required by 17 U.S.C. \'a7 101 \i et seq.\i0 and other applicable law. You agree to use your best efforts to see that any user of the Software licensed hereunder or the New Creations complies with this Agreement. You agree that you are receiving a copy of the Software by limited license only and not by sale and that the "first sale" doctrine of 17\~U.S.C. \'a7 109 does not apply to your receipt or use of the Software. This Section shall survive the cancellation or termination of this Agreement.\par +\par +\b 7.\tab NO HUMAN HEAD STUDIOS WARRANTIES.\b0 \b HUMAN HEAD STUDIOS DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, AND ANY WARRANTY OF NON-INFRINGEMENT, WITH RESPECT TO THE SOFTWARE, THE SOFTWARE IMAGES, AND OTHERWISE. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY. HUMAN HEAD STUDIOS DOES NOT WARRANT THAT THE SOFTWARE OR THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR-FREE OR THAT THE SOFTWARE WILL MEET YOUR SPECIFIC OR SPECIAL REQUIREMENTS. ADDITIONAL STATEMENTS, WHETHER ORAL OR WRITTEN, DO NOT CONSTITUTE WARRANTIES BY HUMAN HEAD STUDIOS AND SHOULD NOT BE RELIED UPON.\b0 This Section shall survive the cancellation or termination of this Agreement.\par +\par +\b 8.\tab Governing Law, Venue, Indemnity, and Liability Limitation.\b0 This Agreement shall be construed in accordance with and governed by the applicable laws of the State of Texas (but excluding conflicts of laws principles) and applicable United States federal law. Except as set forth below, exclusive venue for all litigation regarding this Agreement shall be in Dallas County, Texas, and you agree to submit to the jurisdiction of the federal and state courts in Dallas County, Texas, for any such litigation. You hereby agree to indemnify, defend and hold harmless Human Head Studios and Human Head Studios' officers, employees, directors, agents, licensees (excluding you), sub-licensees (excluding you), successors, and assigns from and against all losses, lawsuits, damages, causes of action, and claims relating to and/or arising from the New Creations or the distribution or other use of the New Creations or relating to and/or arising from your breach of this Agreement. You agree that your unauthorized use of the Software Images or the Software, or any part thereof, immediately and irreparably may damage Human Head Studios such that Human Head Studios could not be adequately compensated solely by a monetary award, and in such event, at Human Head Studios' option, that Human Head Studios shall be entitled to an injunctive order, in addition to all other available remedies, including a monetary award, to prohibit such unauthorized use without the necessity of Human Head Studios posting bond or other security. \b IN ANY CASE, HUMAN HEAD STUDIOS AND HUMAN HEAD STUDIOS' OFFICERS, EMPLOYEES, DIRECTORS, SHAREHOLDERS, REPRESENTATIVES, AGENTS, LICENSEES (EXCLUDING YOU), SUB-LICENSEES (EXCLUDING YOU), SUCCESSORS, AND ASSIGNS SHALL NOT BE LIABLE FOR LOSS OF DATA, LOSS OF PROFITS, LOST SAVINGS, SPECIAL, INCIDENTAL, CONSEQUENTIAL, INDIRECT OR PUNITIVE DAMAGES, OR ANY OTHER DAMAGES ARISING FROM ANY ALLEGED CLAIM FOR BREACH OF WARRANTY, BREACH OF CONTRACT, NEGLIGENCE, STRICT PRODUCT LIABILITY, OR OTHER LEGAL THEORY EVEN IF HUMAN HEAD STUDIOS OR ITS RESPECTIVE AGENT(S) HAVE BEEN ADVISED OF THE POSSIBILITY OF ANY SUCH DAMAGES, OR EVEN IF SUCH DAMAGES ARE FORESEEABLE, OR LIABLE FOR ANY CLAIM BY ANY OTHER PARTY.\b0 Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so the above limitation or exclusion may not apply to you. This Section shall survive the cancellation or termination of this Agreement.\par +\par +\b 9.\tab United States Government Restricted Rights.\b0 To the extent applicable, the United States Government shall have only those rights to use the Software as expressly stated and expressly limited and restricted in this Agreement, as provided in 48 C.F.R. \'a7\'a7 227.7201 through 227.7204, inclusive.\par +\par +\b 10.\tab General Provisions.\b0 Neither this Agreement nor any part or portion hereof shall be assigned or sublicensed by you. Human Head Studios may assign its rights under this Agreement in its sole discretion. Should any provision of this Agreement be held to be void, invalid, unenforceable, or illegal by a court of competent jurisdiction, the validity and enforceability of the other provisions shall not be affected thereby. If any provision is determined to be unenforceable by a court of competent jurisdiction, you agree to a modification of such provision to provide for enforcement of the provision's intent, to the extent permitted by applicable law. Failure of Human Head Studios to enforce any provision of this Agreement shall not constitute or be construed as a waiver of such provision or of the right to enforce such provision. \b IMMEDIATELY UPON YOUR FAILURE TO COMPLY WITH, OR YOUR BREACH OF ANY TERM OR PROVISION OF THIS AGREEMENT, YOUR LICENSE GRANTED HEREIN AND THIS AGREEMENT AUTOMATICALLY SHALL TERMINATE, WITHOUT NOTICE, AND HUMAN HEAD STUDIOS MAY PURSUE ALL RELIEF AND REMEDIES AGAINST YOU THAT ARE AVAILABLE UNDER APPLICABLE LAW AND/OR THIS AGREEMENT.\b0 Immediately upon termination of this Agreement, any and all rights you are granted hereunder shall terminate, you shall have no right to use the Software or the New Creations, in any manner, you immediately shall destroy all copies of the Software and the New Creations in your possession, custody, or control, and all rights granted hereunder shall revert, without notice, to and be vested in Human Head Studios.\par +\par +\b YOU ACKNOWLEDGE THAT YOU HAVE READ THIS AGREEMENT, YOU UNDERSTAND THIS AGREEMENT, AND YOU UNDERSTAND THAT, BY CONTINUING THE DOWNLOAD OR INSTALLATION OF THE SOFTWARE, BY LOADING OR RUNNING THE SOFTWARE, OR BY PLACING OR COPYING THE SOFTWARE ONTO YOUR COMPUTER HARD DRIVE, COMPUTER RAM, OR OTHER STORAGE, YOU AGREE TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS AGREEMENT. YOU FURTHER AGREE THAT, EXCEPT FOR WRITTEN, SEPARATE AGREEMENTS, IF ANY, BETWEEN ID AND YOU, THIS AGREEMENT IS A COMPLETE AND EXCLUSIVE STATEMENT OF THE RIGHTS AND LIABILITIES OF THE PARTIES HERETO RELATING TO THE SUBJECT MATTER HEREOF. THIS AGREEMENT SUPERSEDES ALL PRIOR ORAL AGREEMENTS, PROPOSALS, OR UNDERSTANDINGS, AND ANY OTHER COMMUNICATIONS, IF ANY, BETWEEN ID AND YOU RELATING TO THE SUBJECT MATTER OF THIS AGREEMENT.\b0\par +\par +} + \ No newline at end of file diff --git a/ReadMe.rtf b/ReadMe.rtf new file mode 100644 index 0000000000000000000000000000000000000000..6a49f537ddc3928219485a5257cb1b2a05a96849 GIT binary patch literal 3879 zcmb7H?{6E&5$)#y`5y-PP+JXsq@<`VoKHq2)Jm$vh}9$o_+@!J$J^*`_p(29W)1)E zc{5AWBH$V*V9+ADH#_s@y*I=6>zKRqweiX6OFM1PFRm`umb-2xuY>X1^VQ|$`*jz5 z&YJ;$tkzwcT}tb&?M+OaSM(}6Gkp8u!TlnESU)Qrl4@iC3qac8>Ha%gX<8+OUaH};l-=U z3Bew?6);G;y%6jXOX3;oc*c9sB~cC}WE6Ix*P{V3lB7PElwXX_7i3k@)Uz!p?Ws)l z&}^dDD9;bkdXrHRp2rab!GIeK(t^I=W++)HAOzE_&K62^#ylzwYs94cT)s!=>}TVtnd2(t{rle0!I@cK)BiEo|enkuPM&6Fb>Sr4CH+3 zLm8}4pt2Nnu&&$FF5&Y$GYey(9F>bGQjSgqH=H-C3;nnf?AW0ch9X&MSyK$Zahs`g z12iCs3Bj8mZtk^``l!aAk~kJJZp!APYG zBaZ>jHH2;D3W5W*$iEkdK+0f67Fu$hYt=rLz{)JK+yzeQ75XH4wK1C~pr&kMhF^B{ zTS*!IBo$oqrhvwge&TBF&&@EKof_QnGikEkg8L+65xGOv!k_RFO+Gu3E$X_>z{QOd zAl!HMW7$&kN2+MbPbebauATO6?J!O5TpL3QU8bL15)1fW>JcD>hamX~VJA)22LCKq z+egBivO|+$WfNnxqOWb3b}~o~yu7Li?M2*1(_$O~C12a1HikB&hW@j+yMuS0>iUMF z+xEElGiE#{>znaVo!aqZ%Eh_>PgGx6K5E77(V&-~Fz4^2rSQ%H9xT{74~iFVF(vu>&0sRRI_lJtvydPBMUG(Lsnu2HRy&i1YlmPi;1q}(KvbwfW65kjs`MzK zLo-z)9-e%{Td)XMRseMzBVyz&gb8!hM?Q8OUma#INNrJ-+79HR3i5QhA`8w|Gptd> zpV2Q}5}*pzzf=b%Fl}}u2IGbkzTN|uqmM)9iH8oHfzRo5`D${xL(b_dwVN4VWcVBU z3m5ZnZgDUfN+_*rtM7j$41l`+fL<*oP=aq_13*LY1US{jFp{&sp3$uCt+qFE+~OQO zR-773fXJOKdZE31lBHx(?RCRyNC*pE0^5L5=rH=6g#>Mh@~x4D%wZPKJUeT;jjuibW0W*1MA*l_Q4&kvm&-!qSx^V^u+?j z_k}Hz*o#;&n}3lVY_4(Pw^+x3k%=3ZqpWb!t&JFN1A23-l z7tj^xn5b*lL1XH*5Y)jk%*^WG9&1Wdpm}s!JR`xY6_}T>@E`Vpi)c<*gbTFrB}9O6z@B?Eqm2GM_JF% zveOZAm=FLqbISWo=OA+f6dFtnjIoWB60pB;`*7TlD%%n+60KYAvs@O13xp2 zy6%=SeGZXJD=X8ObYN(x)bx2AJ^{<^(Yha)C-nggzn%P$k!we-e!qvQ6$^|Yss48Q zy`a@PL~scJl~qA<4S0$4nvI3+cFaSySk!K@u7Ft*SzaQam~18eKLbo?DDA_dzW>R! hy|_(gIBMGe>UdosQoVxS>OjOz`1N-Z4!>#t^aq5(Qd9r{ literal 0 HcmV?d00001 diff --git a/examples/01 - Basic Mod/ReadMe.txt b/examples/01 - Basic Mod/ReadMe.txt new file mode 100644 index 0000000..f93445a --- /dev/null +++ b/examples/01 - Basic Mod/ReadMe.txt @@ -0,0 +1,53 @@ +This example demonstrates a simple mod which adds a console command to print some text. + +Fire up the codebase and open up SysCmds.cpp. Around line 2730, you will see some text that looks like this: + + cmdSystem->AddCommand( "centerview", Cmd_CenterView_f, CMD_FL_GAME, "centers the view" ); + cmdSystem->AddCommand( "god", Cmd_God_f, CMD_FL_GAME|CMD_FL_CHEAT, "enables god mode" ); + cmdSystem->AddCommand( "notarget", Cmd_Notarget_f, CMD_FL_GAME|CMD_FL_CHEAT, "disables the player as a target" ); + +Let's turn that into: + + cmdSystem->AddCommand( "centerview", Cmd_CenterView_f, CMD_FL_GAME, "centers the view" ); + //mymod begin + cmdSystem->AddCommand( "myCommand", Cmd_MyCommand_f, CMD_FL_GAME, "my brand new command" ); + //mymod end + cmdSystem->AddCommand( "god", Cmd_God_f, CMD_FL_GAME|CMD_FL_CHEAT, "enables god mode" ); + cmdSystem->AddCommand( "notarget", Cmd_Notarget_f, CMD_FL_GAME|CMD_FL_CHEAT, "disables the player as a target" ); + +Now let's go to line 458, above the void Cmd_God_f( const idCmdArgs &args ) function, and let's add: + +//mymod begin +void Cmd_MyCommand_f( const idCmdArgs &args ) { + gameLocal.Printf("I am a banana!\n"); +} +//mymod end + +Now compile, and you should end up with a ./releasedll/gamex86.dll file. Using a program like WinRAR or WinZip, +take that file and put it in a zip file. Now you will want to create a file called "binary.conf". The contents +of that file should look like this: + +// add flags for supported operating systems in this pak +// one id per line +// name the file binary.conf and place it in the game pak +// 0 win-x86 +// 1 mac-ppc +// 2 linux-x86 +0 + +Add that binary.conf file into the zip file with the gamex86.dll. You should now have a zip file with those two +files in it. Rename the zip file to game00.pk4, and head over to your Prey folder. Under your Prey folder you will +have a base folder. Likewise, you want to create your mod folder next to base (not in base!), so let's make a folder +called mymod, and put the game00.pk4 we just made in there. Now you should have a file called something like: +C:\Program Files\Prey\mymod\game00.pk4 +So, in the mymod folder, create a file called description.txt. In that file, just put something like: +My Mod! +And save it. Now start Prey up. In the lower-right of the main menu you'll see the "Mods" option. Click it, and +you should see your "My Mod!" mod on the list. Click it and then click "Load MOD". Now bring down the console +(ctrl+alt+~), and type myCommand and hit enter. If all is well, your message will be displayed. + +For an example of the final package, check out the mymod folder (in the same folder as this text file). + + +Rich Whitehouse +( http://www.telefragged.com/thefatal/ ) diff --git a/examples/01 - Basic Mod/mymod/description.txt b/examples/01 - Basic Mod/mymod/description.txt new file mode 100644 index 0000000..3beff25 --- /dev/null +++ b/examples/01 - Basic Mod/mymod/description.txt @@ -0,0 +1 @@ +My Mod! diff --git a/examples/01 - Basic Mod/mymod/game00.pk4 b/examples/01 - Basic Mod/mymod/game00.pk4 new file mode 100644 index 0000000000000000000000000000000000000000..9e22f0fc4d5fc8e9f5b9710f62d8629c08cf345c GIT binary patch literal 1687836 zcmV)NK)1h8O9KQH0000802%+ep#O6t zexk$rdY^gsRlU0^4?NiGpzd)&9VTg^rIcSXj+>7k9fomsz6v;0 zkSRO5*?ZsnzBVb~IOpvC*N@YD@7{OcefQpX-+lMKk9+UmMA8W%0{kTsgzO^xage|L zoQA(?GvA*^{(Z`!TXyLx4&74YUj4ZBv9*u>)7l3gwm$UWBab}lvHs&K>ss$4*42+# zOYf_;KKy9&s(D7^4Wi1!J|n!!v+fTIwZG~;T?_AJ@5sWh;r$;^>{<8(>pQbBm*K+; zmpzH^Z412&eu2HKRzKv%@#AHb$_S~@Wsqe(v&+WhzV()=Iz1s~J0Ta03^!wKF2K;R z+7UFfwwc?7kg+#u+RX5M&;E#{X_hkcA^go>ZIuONDZKaIT|jJzd}$-uc`YOE+(;Z5 z0=aOPo#cFjt)G8-Vd|QIoOa#y>F0S?t@FTp7S?P zy4&@d4MrcB*D;Tik@k`rFlqs`bm}{WE;?`R<7*#6wN#zioKoS{{MN2o^C%Fuydp3g zke%>ux{7Y`^);M$CZ36B;+c3Ro{4ATnRq6iiD%-Oc>bQJZFd2du^y9u_s3XHdM53{ zS5IEF=O8}ybiHWy|5>+Atfq}(b}d~aTI#4rw1juC5}qDT{3iWNAb9di*NO*LcALq; zl&6jkBMU=#Nv=eXD0j}8M#2@M!3pi#p-rJ3==T_DCf!gB6DN*_me|9U`PiR{v~@xi zG$+l5<`NM(JYa!lr40!rY}OSZK^Zjb;2nmyzqc5h8=$$;)h(n7)ci7&x<)j#B-|go zJ=ZKXhh{NUsb~ga{jK>ZRCZ`dZm7~8sw?o`;ePEqgeZUbjgk2G3ZYWb5{UMdiUu6y znL_s~{r6{)vuSxzx*yRActT6^mAh`wBD6Apr>@g?Cya-tJ6aVoclvf9w~UXCIE~O8 zV%HBY|6W740VM^=gw@BCbV7E$@%rnt|MFSvztd=UOqrS9-B-dbp5@-~xLKkl1$3#s zN>Yx0Xdq)+)-h`b5_H(}Ls}`W?jK_{M2$Fdj3KQnkesyAUSFkTOgEC1eM{H`DK<;u zmv?P`b1KAZ^L#rqnWvtN=eOu8ewDc?jNFgiS7?@A=IZL|tLP@?BFDhytEy&RTi#xs zt|xiDpD8I$foTlQF6-oMxkQi~1i3^nH|XUOgWO<{OU!bESuU~24Hmg1TW$cF9JwJ! zF0slDRz3?iyQFyj%|Jkoxo_Yr(EpQ+`B5PFhSi(1zlfR3)D=yZx9!Pd;?=hm5^p+e zIllK&_7Afc9d2taCcTr1x0Eg^0P{-9#W$G%Z@z9If#`aDQ3f(^okc4P(EQ5By!%(s zjpX4P(Ojjh>@kpMNatkQH@Y!)?UV}5J>%!Ar^8Cs+%!TXJ0Lnx9o?h{$?6lwuG&yz z^o!p~rZ-%Vo(|JVbW<~5$iqrwDU1Sy!eWUlaV9l<9Nd+jYLT|xu^VvX{R8Y*J(^pp z^2Iv!u%(`Rhmf5*@H5$0Y+(CYfJOE>ppEP7D`gEmj+1?-rvd=IpH5YK71n^BbF5*I zeHU26Ec-68hDG*`um+6!iZya%A7MPLY}zsXBA;f0@_C7(BM={SR1#WriDRKXh@}y6 zIw9ja5Je-+S9c(a1~$z5nXaLUY;$_kR3Vk zetEeNFSpW|yj%}z;WlL))KQu8X}ST$#IeAO;CRL>>KZ&C_ahTQWui}FGO8~(K^=XY zF+EKuAtSCioQ}sh!xkb(>0!pN5tL*c8$~m+1n+O>(_4TV-5Ms36BnNtt+iZ`9WZqq z&67Gk%x_S)R0GToN2}oElL8Y9fnIOM#GQEX=ZYQlvY_6x3YCB5#mh( zv>+Vo`4YvO^qLc)*)(IyQ81)KV4j4{fx#H4$h-%^0{}JxU!=7%470UnbPE_AUfJkc zgP*ZJWMxs?`gk+*BLip8=i|KP?!f_h@8BVrVXquhCx)3Q&I5lZjaJw|bboBQiQ z4h)_d>}R?pYl8X@$Oll?LGDg$HRHIs*!n_C0Lj?WLffF)19=2-Sq5cXn3AUq?7UIe zCNHWsQsOWA7X%MQyeEyw9mt$skX^Pg&eeglI%QWbJtB87H*(qK4(3L#e7S?Uk*h%N zU~c3pmOGdmxg2r_b0b%&+`$5g3P0ICe57_qQbA2-R1iwc`$fyOaK8hJ-5yE(Kod@Fb3og{F-xG`TU% zC601;uMi!|cYkpk0TsKLArGUPU2G$Q_jWN0It+3bv!KH)cQFe(EOHmKpd(xEVit7d z$X(2W4y)XS7F2G$GY!>xIx`K_@)}!6-R`)z(5$?5F&&rJ&T~tA!oKp9cDL9UM6DXfJ*5eNlASKi50&R@e(VH?=neie z9HhFJKMdSqUL8GT)TueNKdO5LaWv8fHkg%onvf92{$M+Rb7Ci;oqph?(Z<)9oh#UX z90Z1gOgIj0rP!WJLc#Sgz_py|11fGoPd=mB7hP^>!J-V3443^4xK@Lma)$d~7ns@J z&JkrnJOR6&i;tO1`q9v44=A@>)3?|6*!1L#iNj+#u`gDD>s|Z4y55*3`4yd~41eb0 zPnP>7P}t4b2@H>jCnU1K2=}9OHLuGT+b>_PZe{#pqSEPvLm}pYKc7N8$!lc7bI~@9lRgkE*OwZMltgMJIPW`Q8zw^Rk7|c;JNhy8yg!67#)aa=up}vV5v?d! zP}g?&_1@4Id|nL%DOupP_}h~C7hTOi{=KXD-^{btm1EPmR`mLcF~7yh=vJP;*CUv1>uk8d=lPr zhwV}#u?I^HnfPOd_|i-MZiD}G9faff(h)d5OTL1165jWp!z^OyB?!;F1MuGH*|unIBE92b1+_=lL(s z_VlUQp5gx^+q-&y$qHWK#AonK?>i_59Dk^knK}i!R3DOrP`MrsASXYPJ{U-cg4Od0 z?L;j=(SvwgZc#INbaXilj|9jGXUL0LMhGe`)?+G&$@kb%#-6s_YcSqXx3SfCsaHv`(Z>7#l3Z4#33vAZu@iwjF z3RIQ+y3K%1e%+?SCckdGJYpoK_a-61vMboEEJlh&&>mOu*P;NGOaOB|BpV>1GBOPD zLJ6Gr-U>Y`PKd*1u6K`&hR%1R7&^`MpsVsK>bN8qcvwK#7G15>yQG+eTE$SSO?kZ_ z3p+W4_})o`8V~-g031*(k_4}nRzmq%&Q{r3CAm+J&hg7%;~W+UGoy};_c5My*mN|D zu_(!T!f0$}! zj9@$Fp!=afm`O-%#8@bV4H%CKGZrBhgG@nh&JD-;*`m?G13Ebypgx`D7Ly2RB zdpdD&JhWcSF-0(ArokL)2&TYqkt}>wb?m_Ms({`I5sE&C|7!`W0nAA;?Q9h7 zm{2x~5~iGuVw23$+XhTG8$}x?q>Z9OCgDTw-4Q-{6^&X@l&kW(lgh#PJf+d#YUMiDF0_t|;j_{txJdj1?78~)GW zz_! znZeVlnK#GbjHss{in;jz!5{{lxP&)SC$TaF=nz2Rt#}Gy*f3qjJ`0LkBIF$_ig|9J z4#D58D~fq9BS9Aq4rb#CAN4FK3L>dzE^RObPI$h5Ha)zQR0CDH0FlbC=TxR5zkpPe z8I|{Uyuz)8$su1>!Z?4>Zs4*d5Q^aKAj&q(WW#a@r}lq|Q`e}JSxk!tm2xsdTN_o% zD;JTnNu@jsP~M5cE`(ce%{xznYuN3hO<>I>AkIOsu;(Z80{*vW(IYtP@T#e;np$`D#HEa2RN|s z;;6$^*aXz?HpFaYn9|vLrvF7%ZbK1cvi?^z@{($eD_4poe^2nAnLCq4yGeK>+O zjPlXE4CpD&+KvlKhyN+JPmmM~u#VQrRMpC7^GIfyk84vBn$hgSs4L*)K6a1tcRI#T*^hCl9Dumv1k^komC7vL~KOBedh%=e)Pj zU(Akq;`!lqX{JzvmJ0OanVQ2tm%#oK(%99#r?#4}A3dz}BQB0tt4%t7-j3X&3^6lcUT-SOu)+K3$uNG~jtmkwK`(mWeE{#_?0AkJS*d=CVfy^($^T=0 zteQT?Yv^+&{d)RXzm-1AUrEs?H$@-3$i-S5zh+zN>Z_d)Wa$;WahGscoyU(}N0fg_ zSH<$w0z)@KH&TUePz5>)0##oQc7wbQ&pqyZ^s~xlW#@Doo=AFHF2MPAHe;8)49nk7 zthrdBLeET?JqK^%f!lLR;U=D~NvD@&#dHSU{vI5*{^gBAsw*2RcA$P2&!!0D- zg@-<#?O!Z_y)&TDuMFgRIi)F3h|2Tz2_#JIv+?fT)$Cpag2C8C<~V_w+ux*?Li7kL zgbr*r5O*UR5DMZMpzQxP!+r|G!f)Z1kyNip8YYgutA^kZ5a#QY?Vi7;{rIyhuuoPwns%2v7M$X%G*U;?bk zEO+H|jEIJjyKn^(N2Q@BQ6Ei-#RV-1EDS6sXF0&=@(MC$2vk}cLc@n!RZlsh{MK}h zB}XrO^I6S1j;P-8x<)Dqba67N@7Zg4#1Yja-o2N5#9^X&!hrJY7q}PvoFsX{b42q3 z^nn)K19uM_$Y^$KtD;-k(u2_?mgzo!vynSS3)?um=d+E|9WfBLAk5(Z%iOgSEKe z*VCHNvNEsyFK({Hm(UZ0gbwVHt<2!jvMoGGm~YW*!Y7_2;c|R@8aVCh&~&2?O*iV$ zbP5CG`#+c1DJ=YFxK>WtR5abi;dC{gOa`W}AEy5$ZTeA!XCSa(2Qlstq`j4qDhU=! z+++ydAm;R*q|odnF{AewL$g(AY=VHsTq-o?QlT-I3XQo`Xw0QTV=fgMa|viHk zKk334O5Q*hrcm-5bYTf4zeN|;Q1T|afUkZ-7a5`C&2%wwlYtXU_|yA&-;c$g(6tM$ zUqx%EU9r7y#jw(NwzL?u;jK3&AX4+jy()?_;#Y*!aLhN*?0uLgOuf5NO_(xK!jw%* zn0nz3UBZ-&5~k=|#cWCw*$k8`)k?Wi2BmW&!coQijYC+HYtgK)i`8+D~sui<7Pv z7jNHuSckW_?;L=)yG?|*m%(IB4|Cz6)k_p3M{%Of+?nDSmZ3M5SHy8HOkr8Y>d87M zlwJncaG!GG>u{L&LHR*S8ZL~R#O?l@(^PA|k(c-FHu%EUnyv8T@>+RYZfjAtrBq2I zB8}BMqWkOXv%}&Gu|&3N779hs<#s1)2O%Wer-X*VWK8Xp7$fx?R-^hw99zmaGDBw( zD4wZmct13O&?Q$Afk`f7_y8jMQdcs@=@0FpXu1ute8Gzu2;vibp4Z9a?!!8V6JyR4 ztyn@u#?F~WSGCHnnx+P()l>(pdA;J`xgMxEL;^yI(DnGOjYzEGBvu(g&f9euH}dEA z2Jk0Exe*AE%mGHI!OoEcrl%|a-Gx4}Vo)DCl=%loNxjqE7?D=iS+fLFCJ|F|WEo_( zLgrDMQqnt$cvHof8|twV_FGaLt+$ZY_&RmklmKas(JU!ITBBJW4j=!ZOBxD`7I-u_ zoNK-RD!SmEqmlm)oN`BST`~MA4c4Oy0v%uBUa{p#i*j4{s6k1wMMy~n@1Tu6lL>32 zI^117Fn%IhOIDU18a1d&toEiwbmB<#TCymv8{MO_N*2lqPkWzWfhQ|gF(2UpN&PII zmMvscc#Rk3Y3`~b?lvqO7T0a~g;jMMMtOL)7ck%bZ-Y?XZOEneYSvV{X2%RLrM*d^ z7ZKJ%B=^M<6HhOA(V^vRMU}4;Xxm-6b%eIv#_~a4%bD-ilK<^rg_9*`uw_OOcgtNS z@U{Xd=l$gN<{f&-8}v1=GL-zp;yWzmsOfS}q7l~$_3$>#{;d)wOg&)=Tr~SPkd~>j zPo(zL#W6o#l&f=+%JGk=9N+nf3MN|xcx^JPb~dLuaG;{;Y-czu{~&iYjpJSHKFn;+ zzF5aQG>WZ8J1zH5OOsd&`<$(&Ru8~+&bCkV@D!rv(*J5kW)g_7qMkxiEm9&9u4e_?|G260TK zgUy(Tw77IBC=>(1uGk{>h4Qf#?S>oR^2)ZDFwk4CbKMQ|`#u6RlXu8Z!M|Mim+p2K zsl@_R%t)%vM6k9)g#s5HgK1p09_?cBc7Z*kTm=LlgtxU#6fxxHx9s#SOp;m)@}!3O z_J;YHashK1%S(IjP(LFZ{TBLc0sjmulE=k-Hj)(>%N@#BN2zrL9Q8Cr@hfHxuEC(ee?-x|_B7s(F34H;aq})_`?7 zoXUk~C*cd2g-1~56Ycwt>Ymzx=|M1kS2*K^4`rYu5}rRt%3=2ADJ+iK&vxh z=(bi=fj7#XR0WR|7m3JT7Mvhr4o`F%^E$qN>V4xhIm{`+LW5&`i4@#vMjq6Q=S=gk;G# zvEL%nURL4mgV|PgHQ;QHi)B3Tr0@^8bE zZOWOksS;Y4ZOp{&+p$QKGQ!3mzat*Y9)#-D)k;+s?b<@aYlw!><^WvAs zQe4nRtp)kBJJTa0B5HC}1Xq&B?SVR6npCTuF2ieR;H-B^ZPWe~JT$J zU%+DZYw33t{nC?x_6g>kqGrt&8N~ZrPK>Sl3uX{c-rI5nlZWe$VBRpL5T}@U@^A_N zgmrvl`f#>r9vk6F#OqgSq=8F9WOp_aaYVFo>`NR8mk!0_L(w}T3$ctWMisI~{E)4~ zPY}xq@qbx3NTKEi%sk%PFbk}*Jvcbr1u|+bkV$ia)(h&aoDwobh>nJO40lmpve21) zKEX}J-4cAy%cBwOEstYw;NtOT+|jFR#rhBJso?qDI)Bf^{XK#TJ<{Pl8b#cpvub7$ zm_$&2sUv&r@9No+=*Ta7KZ4#UYyEUW>;%`M%@tk8ftiiEc6 zOafoug0iTbu{Mb|=6DvcPUtp5qDrw5Jq=S%7^F($d+@$@gc{upM{w^C=)?UC(Ykz| zBvQx|NQ!xqNC~$&9hkBFpnVokAgSWXB@Z&kL|6%PL1)9~^Oz$O?%b?63+@&$M>cN; zo;|_xr@&H@nW2=e>XuM*_?x|>(dYTnyy7u3$-zZlumj^kFYv7dCJLU2=P~|GYaIci ztd4}qeqbt9m^??q1?GC#q`;8S~aM+&1<8rEnPBqks?n<#3|+fkEZo z6iqhX>dv3ZeaGqjB(YMt#9|K=JDj~X_`^YWXRnE4c2A8!%&yFBo2goYzj>3;D9{}z zE)&Txe3(qiI($6_vwuCxNxV2B(S>}oF00k{bFv~m24!RPw~a*k|A;~UB6XVSIj#>o{yRFR-ULgqK4`NZ0E2Grh`o(YM2IUeXN^(1%<0;tXj znIz$%^zY_0?~v5$98fS3X~v{vrRpNlTo6O^naBVR<20L*=DoX#X7A5byL&_}jkP4( zSiOxZH|Q$=@`C2cU`d`tna4}=G5h9T89WK1S93Z|pnsKh{d!XM{3!C-%R14ps;ESz zz|Nw`J&5~g;9uY%utvs`bi{Hi-6iUR%{izT-r%m_0=ume!?JbfIp-ig;Jb3IoRfG# za1Nm^tfJUa3Y=3RIHwdiry4mYZy0b+S*4D17X4D?oK2TRFXuov!7}=gIObKAb&d+w zvFli;>v@%ReiHNXpuBS`ig&s--YFAv|1Udf?kD2h@7pET>y`-f{Kqc7x?UfIc`l#R znCEd(bnzg}({YX#;^vD{q;u|3$!XsK)uD-v`8g^%x5i~3>(J~IyItAg)UnU`KWgl= zWhbY4*C6bZ&Z$m_rTUC+5Yu)T1e@dNkIuC!6Fhc4TBPZ8!bdHU8M(AwtfS3w z*U>8m6@^jW-p<#;%0U);DvVaVm3`q1t&zj$22~69v_#axBisLOY9ZqzZCxK}8CoqA zaGGyDH>g@z*&I;|zizpvRekjIx2YD24$!K8`tt#5fv;x_a>1f5D~ARB<|&w>{y^9q zI4cDE{ev#)oN0VX>sIvS(^s{k=Si_E`jwXxCAJIJG=gSx@n;79=%x3BHGJVp%6n&Z z{AmQIv$Y-v)1bUG@CA;YLGcZIafZI|M85zM6yTfhAi#qcm?jiU$MdUI2Krv~$CwTn z+=%rC*KeXE$q!o-4a)ESKyxpTL$q~K6D^x_XQIBWU6Lnxht)2DS-w~B(nEj1^(Wtn zAKlxe=bH#WZ9A!Z&ew$}p=UA>ud8q_qctT~uhO7>bM5SLrGpmIozcXL`g?w5Ab-FX zT#rNG3~t1Ju=dt_VVHm2q>aIc%2V5D3{Ibp9)ra|a&m31BtMLWoZFDjzfL`scW7)p zu8FhpjVKk?VRg`j@TAo0#mI~_ac*9X^QPx;K3iVknz#ydO``x5wl zINZA^(!pGO_{8tAgV(61dcVsWtye*IWGsHT&m;{HI-}V-8w~{s9mU&A6HYFG$s_f`YE^>sIsTQD z8b{6}B8s15Or{K&qC=_X#fMYz1#0nZo|5yCq%WS(^Pae;*6|sq0JKtvlEy1)N{rX; z@~Daoz4`HKL2^3WRLjp2BjjkZJY7c$6y3m!-Va3sC*{cl=1W%`6C}O7`1r(B9rmY- zKft7!{hP$_Ka4+PbaU|W3Yr7Vc4)1~h74SMYJI&?^wn$C=_bX|gP;l?-+)}?MD-0% z?-oy6>#>IhE^ncX?V5ONL%T`hwPf*HsD0kj)i3qx_92%#(YsvX^hU>Pp2na1V)u`3 zWa|D=zIBv#C!NJe6XHq~Z7Q%g6lBU>lxkIg8C6L=%l3xDd@m`LZ;4a>ZRVuxtA<%n zzKY79QR`nlu)Nv75OV=^ZP?)(q`znKQGO8hX|b>Q|}-X7oW<^W@+>loJIoiVK*V>R;4 zV2H4F7^rbpmb%w!wnuA&*ocP8j2vHCd-o>2vxuk-;f;JdA6oxQsD875nXvxn59UXD z=c_)X>>9Mp2xfXNf~-5#cm*)}Yl}@V2P+jJo`DoK;f~Q8ukH=siTh*SlbHF^(ZB5| zr@xicZw@Rct{co3rbJN97hXL?*&+Ip4iw|jGY8?Hvom$U;yWxN3D=7Ay7#kzojks1 z$WNZveG)oFA$tF3al1dD|DW>yZ#9ViCmDw7|G!6J0f8aP4$=P~L1z%Z<)@P%ggY=G zVIdTJmK_7{7LKB`w$44;!6xdNCj2-P9+u!|nmBO_3JuIRMI|r>VAT(!Ve8TMiC2r;j&Y9=VE2hl# z*Sh0I65CHr<~dzvzYJegxHEb2r*y@ifZ|OkLIW4Ucb~Gm&`$sV0Qj3tJG=_*S*`F~ zl4r8kpu@1-=xa9k_joT&-d@T2gEI}je(O?`$AJ5xrYJ%0Bnv)r&O!PjE& zZ8!Lvy`MMK*t1-$#GXYm+X-#dU?JuB!|++TP?X~xY{p!^wLzp`iQFrUmi4ByHI@X* zwl8*I6cd`YIvu!K*=^ySd-0PvopXORlU~+*5;IW!GjX7NCnxj9P9d}znnnNYq2%*3 zDbpwu*sTIN5&6y|g_6&}bG&xY-Vc7#z(e6g^aeAbS2FF~nka*kpAFreH%CiQj13rl zTl0T7{`~jF%>M#0|A+rx^Z%2-ZvKD#ZOp$z%zxKV^WPPJ{(HZL`M(l7|Nr*Q^A9uM zlop$BTi0hvf!V6099q!Yj-JU9>&HCW8W85;-G2K-OmskzI+fK=!GNh_YOoP6$~@ul z0o;(yI+g6#C&40t@lpQNJ_$$6g23F!)1#!>ln&2#4k+w8@Jl4>4Vz(s%Cl4W)1JZ2v``pt?dqU{*xr`H*s z{4u6Kg5i*F6$SGr!on8&{WEZRW9}m6b8AQMS#(G5S?TD#ycqok>!s~?vFCUA`lh0u zFJVPhm+}|ji3@mMlx)4dxf+_7sx7`kP+#Sh;;gP$>JugQK$ZKAV%+B1U&)_-aAX3g zJf_IAd{Ck)mIy^GMGtvnQ z0R6AKiM{Pu6R~%}W0NG;od09*-vi^Ss{H|cCYiKDJM9c~fB*raj%}b(BZ-GC|F9-65)!9SA=pwC=W~0v`ODoK$%0EQlwW; zO~9hPP^2)w^;v75naQJo_xk<){`sMuoU_k9`|Q2;-s`p3W0Q3%j3-n(_nRCrrFcaH zO~-_4m`{$U7$i=mw61G_xHRIj>A9N4me#7kiv%z=V*`IDh($y1&TjdVDmm1dYQYq_ zlBUOj>4Bxk;fkc`p~*R1%Vs4zV$W9vw#BF6S9_?@v(@<1h;J`ej)>R7jMgz(0^XeC zBYZvM&kI42{41-7&w5h%Cc@-yCLyAx7`^_yK7+PV0O@$5gTRX7%V5JMku6hdhcS%o z6U(6?jxU%YvFF#|B3OJJeQLoKs|8nP%-uSd zAO=H|wnfU{H~35;@uZ)k$xngV7W2W?d&92QbG>PG^!Mg7+P6eGMD=OyTXbBs%wubi z)@>`6UovoQ$2LGFDEI@={PJt37;7~gF}aN{5H5cp^VP2BU_o5O%iCK$GcCc31YWb+GPSGADgx+%rM6(Bcj#tUt?|cK7Fx_U$Ch{p!=<*HQo=KGcRpnY z2Ku9O?cm$`>>U-x^M|Qose@)Vcc9>cwsg%sqFv<(y9c%E5n45aHp0!`8Lfu*-Qa1p zhV}|C0`58Wt}WBJaW^ACclajz3T=4#NQtGLo;6J4{B}xssQ6TH65^-{pWjFP%vG*o zI20M@p#4&D`ePrW;AwutCf$IY+(@Z|Y0C`$E#=H=r}P+tX?0-#6~cC0{Q@&I1z>1a z+iD|y96Bm|C%Y$}tzA`CQ5(27zDmD+gV>9lrQPzF^4q7a`Ky6gjQ$)sYPB@Y6`#7m zx+~cHb8Q5E2;pwTc5k_m3ceMABO#s!9*gT_&y{AI<_3D?wPotHEc3N|eywNXYp=B^ zHQp5a_*0&4V5PU@d}KS=6BahmtEJHme<4LV6P|)U$*jsf0RD?`BQ01vW=hd{M-LH$ zz_{QRdX{JCzguAO4vCJjMhn=MYAu#Xsnts?N89Ww(EN1clV77bUt%M&?_OF_Y5e=E z3!ybDMo zYtM|Yk?f(sVRVpMFf{ry-ig0)>A&;x&RRjlA?XbT?~ZZI_)_gL&jx@?dxxM^_KcOI z!O|PcJGS9D)UZ<&Gv|n6=H}Crq8MiqGcx9y&R41Z8PC1=w(ztzF*XHj>k6KUuoNq$ z0XyOjfWx`PjDahyf?`{0g=w_~($X?50d@g~Zw`n;M66P?Mc|24dx|TW92>I-=8_CS zELOI7I-N~5v;D$bT-KUu@eVt?=E01T7^V1Nlj)_WilEZ|>;Md=BR|3H1WyY+&B-BJ z>;P(Ln`fx~Suv>YDoF)2pWyM&NDg{Enr68&$JJyxxRcm5Q+uj7=0e%_XcT>2y|Jpte)3yGw0$lmd+#JYYZIwat@s5 zT+dA_4}E@=Q4>6ezxfkF&SJsdGPkMGBx*0U%&`OxN+Y}XEtu%7_u`k)$nytTV09m0 zn`CM(Fu?UiS*#pGT}H%w+oaUb@Z0MqydC)on2rf=nrcVzU2bhR_}{O5)oBUbE1mlM zzDXn7$jyFHnoUP{+NGvra`#)g=DaE0UpZa(_wAk3{d#`;_Xj4tJ#N=_$6w4)w$vHQ z>E8^|o{2GRX{0guq-WpRJM9>5l(xA~nNqWC@1!wgaLucwW_n@_mWgA)r`^UZetUV+ zAWmL^*IoaML9izHA{qqA#>c6ZTH|C$c&|#xOhqNDgXH8T`aJ&Zg*+93oGat>|C`Ke z>Ri8TfKl&!>~j0=Nvk;fN-O}UE(-uF$8#l<_Cvd-#TYiK2@jhCOyX=&!dmptWqjrj z3oU_kew7_?ezI%QLY3FIM-^Y*awdTs-Vzy=&~@y8j?Y-(@RN?XGIzj1g%u&R3ic9e zqf45SzI&+nqV8w-^b5aTJDs;-8FjpK&dFHprdcEW4A)TYLEZ8odv^mq(9wBnSgd9T zWW;mZIqe`XG>oC(WDGzTqXaqh$fR%-j6whg2S~c>%z{TZy1k43{+VAyEC(;KV{t zYuTq`^KK%S<`V_c2d0ClSt+swWE1fW*Ru)LnRGeNClnG^!LVFN)Sj^4M7F@uLIq}{Z*b~iBhs+I^n-ME z{uH1hp4k$-5%D|%57J_&FwR|KVw&r?ZH`D-=?~m{B~Wy8;{p|ykeKtxL*kvd*Z69c z12V;DaiZuCY~ZRzNmU)FJB4>f-mx z%`Vn+U^?}|{nsUZFwwW2l~eonGgRH1=$&zY<;1=<)^p#ez$LzJRe<%J%-e}L-RQBb zXn?~V-iEI)mULOAc?5d}$!q>VV;ryE7+_-s z*@X4p6q6MnxN1Ut(N(j-h}{52tg1$P(9^^EckRuVvp61Z7W**nm9v*eY_j~v>CN`O z0s6H*`fO(ZR>g`Aw?|QQN-^sCxLNeSeI!vU^KNeWBS~de=~cWFI3co?GcpTpn-)d= z^7;4)5tg%JMuT=!906alE!^ly55Okr+2fd%!_CB{?i;au7rSn?%_zHm&Z+z7j}Nz$ z+ls$-syFbl38UgK^GA(CP54rlV2Bwb$O&!<7=o?;V`$O<*#I}vhs|pIei`it5x%2> zvR}K)ZOJ|09byUkt(zc0<4IB|fcyTPCFm+HS2n8m{45sB?}y2j^4?D`SZ3>4Rv2F9 z2`>{T{3^fkJ*rg|*a3OOs_h%*@Sj$d6~btG7XA+$vGn0l7?&l!l?c{u^AVch#KX=M1v(-#gRg9gMyOVxXo2>ALc_BL`5&Kz!kZC(~tDfe%zBhjM zvK&b^(%eZUU#nNXn7sNt8IX^AN$0W6WH#FL63n z?tM4Bk$11N^Y_>`k;!4wz?Kmz35xTeTU-fopp!nDHb66W@Tig=5ijpm79X2ErW~x~ zV}8+MT7rzGZBD$G6@M?A-bfw;^$Im&`&{@kw4gpX8ZGk=J%?Yn?qcLJQG-qwI*)M20P25QkD{=Z~;=fJ-7} zjmGB(rbQ<8`%Hf4TM9`Sz1PiwW}1~nN*jGDOyP}ViqHO2Iw;`>)#g}~hc6^B1mk)! zykkVLr)B~7XI!xLenFI{MqkbGPJQJgb*eoq0@p_!0Hf=8FQ{Wjn5u9i%vX{`+_arp_|=HjVpJOtV6pEZPUMuv1`185XEldRG3q>(|NmC zHu8vinmW~Xq2?GSt|Bw+*s7He+xW74D{HB#h1OTSe-ECv)Oc+NuT>rs&g-bddB(oS z7J#4hw97!V!6njt8IFOB!6#_x+?K3)tBO5ObF{-)P_%%^M^-tj2s4b;;|qu{j@*Wb zf5xw@c$Q&4tEXo*0hf8Zgl?;Fty;0c;4;j45iU5_#x3;}-11a!>w>gqCvh;qph(59fcxr47ue-EtDz zd%{n(9E^}nCZI1~dLiqJ?Up@9-~H-Y6YMoB5Dm?y5xezmTlhOR?b~*(f-dagE9~LZ z!bLXCZx2`4!)10@6n~j2hONn~p7T#2pwxw0*~!|->XU(-|4DaMG-djYmuaQc8e_u@ zqZQMh3JgJiebJ*+_1AWZBSdS$ZJahrD~8S2MrM@LMCKmg{ZQu!Tf#nD*k_M$9QmYz za6d$qa6izJ9O-Nca6vQwuH@fFj?K54uUq+d75@hLx09c)4SvEomHOHJrvEs%b&}h%uph_? z)fNKmg!?o3m*eS(w~{iCC(!W|&b-vm9whyF_+4KJR;=5{ZNZQmZPoMhmHf-mbi^e} z8SIDq1N?*&JN2`>&6ak)?&ROq{M&klKy=P#1ip-{N3hOkB>}?;Bw)Da1Tyyg)vEiB zaW0qgsu&9a8i9eCm3CiaHCA%^z|H9cD=)cEFhke)%@Ga zzwP|n$piQfg8^r%+Cu=0cWk5^V6w943;Stp3qa-F0#G?Sq5by0d-M42Mg!yGc6eE)n`gPj`i4nT+4q*!B2Hk>(g~$It|mQ2e8eU@@lC%RGJBpJ>@a;PQHW2bUSL_Clz^E}tjj(2+<{3a zkw^)>1z6M4!r1~HvFF3xCz(SzDHv+ve_96uY$Qr=?Km>w{*>_?Lt75Bal@I$MIaxO z`qpGO4s1Q;XkI$)BaR0B**3vq>;n=u@$(3$%1zEUDySE*D6b{?=poiao`7yLka0C} zoybiSCRY2B1IA|xO=pX5Kr>Kb>>iq+I0ZLigfzr=yWyhJU`*dM|C7uBcb2vDnl(1 zw<5bXn8-J#>bIkQ>~P|Kqui@_iyTcD@fOW=j(3~s9Pc*MIo@ribG+M3=Xkf7&hc(D zo#Wk{&T;W>s|j*iFBd*G9sN_}&mlCA)N# zz4X2t1oP2#*Ic5#-+ES-R6ob~JQW{tohg)F^O0B$T{s2&kk$hh$uGtWkD%sbTP zQzvhN9WV;l&G-fAxgA&Z%_(RzRu0XBm%i-A$A0x~A4W&FDd0 zCiS?t8>{)gPTkKm0(_sZ?q?Wve6M(nj5MR?d7Db;BBQlov_Jf{AG7gm2Wo!HY;H44ggHu{ z%bFl777llMfXl5$`Y))bt)3k3yHo@|A_Xt1(CZ}goI+&G3bsLHtTob~W|1)upP+x) zW7o0Bn8hNaXR63(eTJz*o+&bV+OiV)bsQtGUuW5I#KxC#+0jF?BilIr;wf2!<@C*F zPqA^=&sl8DYnL$>R^iywhZ)+f2AOJH_k=qByhvNjkpi6B6*hkbTd*!Zw7^oUUNA0I z6YUj-!tBYV8-X1DE@-2%{2|usRH`!Ls)r`&dF14~<{3V@ z%+w8N9a+eOUHoZmv=5SLma@70EEI>fbn>xJ*VD{7f|JZ0@SeE&Y~kC&jE}?J%qhZB z#$+pd`I;YoDCc+)O^X&40odJjKrJR)&}FiZRmMF(h{cE$93a+_n^ARA1wFTYY_0H& ziiz|S@A&RZ$-LuXjk~DzG3&cSg=OU7q@Kt+T^p!Mh+k|23V7X}_(2#!hgs{y# zVJhiVt0yy(F-V(4rV(l?SY6dBqtzagQ6jw_*mG5cFSp&N5@%FbYG?T;)?EIy1E`_3 zqRA5QTrEa;fBDM-OFT)5KeDAv%$!~E>>1Upl{K?x)G6zgRuw4SrArb$Wsi|gx1E-F z)(uXDq5?U)^~H|pZha;j@h?t~nzsFmZP8u&VpntziW9c|i|vP~*saq4U7dMou)4^cy3Nf4|#jP3l~Cu{EBI#igu|{K^2NT15)1Yq5nFj-8I}?Cvt{ zOZSK8@tFYa`-}c$weSuMFl6_#>04%#6^Hsp;aS9)-NgFBJE_`TP@uB+3@evpoy;`; zaKC7vRlyA7H}|o^DJSFm#}Y2H zLFweTgJY-PMK2S45EK4~@ZQmXD0v+>z(;t*_$V_OALR~&25rte2HuX&9`qZ(c+8X; zGril15|#(g<*x+}FP-VCjnvtiGONP*Mg|dV;HLuqp6Mx}m^l*V+Ex&&r7#pRY}yu1 zahN?j_1#E`)w|fSEJ{NAk<&$DS)+iLt3M*S0P3F^^mZNs*UEl4)G%@O_# zSQO!EXfL(?Cqj47%uiu4pLiSq+3{b3;t1N%ieof4FVgy$Io%!nm{xHza-+Od*+fF- zE&S3C|BFBDW^_N9D?N&AZr*O0k95AqLunJyi^JUt%u;R;)A9je_C0EXIh{rGayHCJ z0(DJfHG#S|oMk-nB!fB@;`vL!nn7x2N%^Soe%Lz&-s8XdzrcI(ZveWwN8<2?vR)%p zmmBLE2(RZHmlv2hE>pL|nCkj|7QT?-k_4UBBjTd4+jz8xH?)gZ`gWeNg_7FRyEize z!uX#}iP?FG<&K9~?sy1t$5@NE8-Ih}@N%+DZ41}nGEB)TUinYiCeq=jsn=naZE{~1 zPLi8!J6pG{QCT%JI>0#c`>jX4?}2q}-ZWvX#9(g^ujltJamECmB@ZXSLqrRi9KwB^RrKoISftT|lh91SU|V-0 zsVI(mW=cNMNb%WhSo-{iKT z{6%X&>`E+?HFO=KMHNQ?&b|&e0qYhJtd$1yCgn+`#-^_cRXYs6MrmH8|!*r z+tGTinICk-$^?JV9{P**#=o(aS0zDJ3f>i(jdP!B>zPq6l$(U!CiSJsmJ3}wTkIpy zby|pNDw-$*&4RV)b}Nx`l`8cEY8f=1^4ypV-F! zRgfz7P6XBcT?c}vRT)3}m26qU+Q~veq!0o@Eiy2o(3lqjF0&!Hn>{2E-$kl+fkZfa zL+{Z_UjH4nc0U)w+U-$m_oZ8~gL>5}--)~RYgNsKTyvwU`RiMFl}~jjEmsbugg-p= z8kPI`P&Z=(Zv~H~?Q(d7ij9z;?P=?bXTPfw$A1j@kC&ObykM?Y=jY^C6~^`VAy9=Y z{LltQHr%=~e3#kk7bmo;8OHVBO-iQKwrol=t@BiSeC#vXV`%xZNghLATsDozkWn%WAWF zYPnIV%~tl*djbWrB{q5-R$!fH<5-78>9ePioBNYC)gAu?&YAO{38^`grDlB4nk+SU zTt7u>b}(NpQu8NeKTXyT92l9PAILp0Swrx*Bn`o2{lM}E{z?6Sv#F}o%g!@;=TQ{IH)d^Y4@g;<^OjhC6@byO-P>Tf^eupsgsD=EU4 z+f-K98CpC1wMD5xov2`^h^`n(>Rv*nizlMe+4vyUg(X;4CH>@S&*|OOko%^PS#ZhriH8bT=lRyY+5A-5sLm_4q4Fi9Tom__?Ho6I}_k6Lm@Y zk9w5~)}sv43YFf$rwr1HjhB9JwM05Sh^>o>DKmyM2!frg(SfRko(SO*1fvo8e9#*w4s9b1a!);_%1a~po4Z6A>x#5nV!PqV%Qk+tH1Gp^PC zyMeJLVakQSnR>p4e9Md{^8`<3L!GSfWDeoUe0Ai&r?5)T&1P7#E{U{NTMf(E(C~ZT zg$n-WFPWz)B6S6lq^{r57OgS<_)~>@rU5+ae&8`@76 zKwdxK9Sf6O;p{}9)iDxrs22?u%^O(^9w)EO(ZpLP&$vFD>R-tx3$+!M#)CiS{uNTM zhHd#4ZW#;=&0&sl^~V<^N{r8bg5E3vl4K>8X)ga|631A(mC;sB@IfF9+mGn`*@K7E zQh5z%Cnkc)SSZRo1)_`41qGfV0gsnjYkdrZAea-95^A;SK88V!bm^+bJtM7x7#OlJR5 zi?nafensYjM2V66u>}^;YwqHr+}BMCEi+6twCXHd>ULUEcb{uoLCg_|v;H_^v(lPy z3#}uG!R*|)fH_iU*XIa*<;HlzN3a7en5n0JVjgy7Q@66%+@jT!N!zko{GTsk7O{K+ zi&!?4;13DqT{#WPvsW#x4S)4m?t#F);B6XC8pl3QBzy-}#gcLF?%|S?)}Y6@J{pT@ z$FzMtRN=YOnHFtWTXI}|XT@^h)uG(CidQVp9Z15*L&{#zu)h+E zy;bb7TVWUa(>ci=L26iv%rzB@O2)6t}xl?IQ$?}O2dEA z0#jej3CVFv_+SOILo7XnAL7isU;iCdTMs37rQ|KYpX(SEDBl7-a&2HE6$REx`A(E~ zLpkJ^`t4HwZ7NTs(_wFVj;YlT9XV71y3baj_rriB&|1MMqynq)w;!e?!~ppmdcSE* z(r+4*^qa;c{iZQVziCX;&&DLz>}T%EC8?#NbM}iHIj6)le!D@P9CA`~=*t7m3Ib4q z8>6=q7TG?8nR_%)9t^AH!vOtYN>^?d4?5vZ4z){o7O?C=<}% za5v6yz3(|Y|3>8CY4AL1zvd2@C*yUrRO;!UpZ--(I2jKN3&&(N-e0qTs}d%z?|OY$ z*rF71)uBD-KgG#-wX?Isg2REEu~@nePHlD{|B$VcSWd_?Y_dPII`BOj5kBx1hJ zRzOwS=BxNQ<=cU=>iK|tyNxg72ju?N!(FcfsdmdZf2)kESJD_(8Q;cW>iH%hXW)L* z5V}r{IMpb8mXIF&V*`a1^2oRdMZB zEtaI*hH&`y3kfEc(6HYn2plH!;sZTqSIcd!P$ou3e&c3$+|BwWpan&xXy)=w5|wZ4 z`h*!;BM%J9?0#lYd?tf(*w1s2M>HVMm`kLlb0BygI8zYgu?2JmZV=WZ8Ato*BY_vy z3&yMGh;e<2EZ#iRFh!qZN_AN+nPQfbM^fB_esjh)=kSaj{RH}ay=}TqpZN)suI7QR zF1?58st<8FKv>lP{M_lU)AtZYD<)r~S7?2-#=rJu^);mK%iUSPN@SJwx*PV_-ocyQ zVJ;QTbhr=N@v(1+_B{Ai(VTyn&itmkr_-5d5nCflZRpGMwHs}4l)I6&<*SplV3EA=q;$B9ubYH$pI5AQ6 zZ{gl-;WL45OX5u6`?%iz(arKay|Bl2Zkl=uy$woJ{_am&e$A3npHc2nh z=*J#fc6Wk;fe_6JNN{F$A&ZluI%#t~wEl*Da!b|RlJi4bf{m-#gN3_sVqjcAU8a`i zhll|q^cQr9{-MI(WEqw3StwwytdLy2Cha?gHdkw#RpvkSdSK=$|OS~|cvNL7pCG4x) z{;Dm9F9j{(?bmPA5$Pi3Ap{(&LXk zKXCzR9rs$9Jlk|DRBh?-;H|U4Yq#zrb!*Zm*`Mr1Hbg=UdcsWqSVdCaRW?|--1cvC zEY8Q*U#hsGRsV*g9AunBy{)+G8@GxvOz~Ift@Syo=ATyL^fynjW^Bg+64$xAK?q-uUsGVsqDy6cIjn{TT4a^GX zVo#2G0$=5fLwJ&A zX*Kntw2V7Zx>9|qQ7HiAKC>o0ll2sZS06(B{AHprvAQ8<7X*@)56i;mLS*helqoy`K zD!TYmMDHc=az9Gz%$}8&b6-mvx$C9%=3q+wYP5Q3z*NV7m<~Qap@Uzg4;}24j>@yT zgbtotg$^DcqTY*EUexeEhMIR&R|@#isNZOOm;JsRP~Z*~de`tKfjNBdUx=-1f0l_{ z(2gw;-sx1TpRGv9F8dpsb>ja1F$pZ8XI~V5wmutg@2BOB)LK{h91^fUTnRcjmtFto;1urj*!g3cLM?fzw+2?MF{T03|haLM5y$QPJ z?0YknmyL^HkHb_gmc0LsXJ1a<{27VOe{90$2mFoHwGPpZ--6AeTeRDFk#FRk8Y!Rc zfH#pkj3JyLehfhVR^WbG%#0#36pF}G`>>pm$1X-1f+F(O6YOgbqllb1iYVkK*!AUv zBE|(pG}6X9COE;*JG;|!+!8nJZPu}Kv5~QDr$rI1)1e5p!GR@cIVan0(LEJhd^f-v&5->L1ja6g~;b-IrU zo`B1qY)68gLKgW7Spa$P$(y;|iamvNd01v+GTunudGU=_C!4R1H{XuL=3B*>Bl!TV z+~eb&cUZkhJCApr-KtOrvH=p*74Bffq58tOV^bV=_yl*X0`6cG;#cDo#IfPS5l6fi z3U~Op`SLw#6~nWQW4d89v&~-P%G+m8Q2p&A6-Hco7pmF zMK0o0qbA}IS7k2cINHKxFDi^&nb{O;VmF}Je!oQVyz?t_Ed@`b8`?40aO0t~?;F%w z4Neo2^hqHs^;9Byn!SQrGQ58v9JC{MpzAF{dKo;3K3KSFk9l%@FdDcy(!Aa_HalTo znxO?dvw=AOfcsLLHzX_;f^~%2T4T=6-{QfId9YjPJW52(40^IK zxx+HCsWH|*xHwELfv*tHLTchOoxMMOKNf3Pu8pin%pCLM?0$RGd7)jgh5Nm?XHxx} z99l;FiRsHhIuXF`P2E3pO6{sAqbNZ z+R{55DU6vj^bU($rRyEX#qGkaci2;u14hS5Gva*5F}bqq9X5Gt(>qRB*)2%#7?+1R z9QY<(x2f;hAWgWhfi)@*IP0P7YGoU=5Oq+$fz=cxNjX+M<5_AlS+GC??P z`!D_d|Iohw!`uJf|E2xv3GFw<0>`JES9OfTprHm)h}*F=NCy zGU*09l~%ekG!P4&NOP{qJq=M#JYBXIEH2ursP%ok66KTfRLyUz(RLFf1O!WW_6pn` zJ!!XC0*J_HtnW0B|FgRWMmqWP_%8_aUROm8yvdKg!gp`Y_4fOszr_6-*Z_a>w&^y z$anrHoED&=A9J3xZs@KMJcuGlx%a`ZGiBmxgGpFh&q488Uvf0}RMC{aEl zq_FPc^EMKZBIP6ub$7}}DK~C>XFk2t%Y?<)vp5#hyW7FWt#77xDnsMYD%c`oV4`=o z3WsNr&E1tY7NbAErJ0&(6%5Nk;w{yaeZ^BFX@bxhbR6^O6; z?>LCTF!%tb9Kkll{v;5ebs`DGUoSO5Y{Ise86E|_bspul8rEYDos_%U>d(ZgHnT#j@L|e{QZo6K-Xp zEqvI*Abj+91j6gmsPBvIPpMQPu8u~In18dL)>)n7ic$#^Mdz)*vT#LxvF+dv&P#WS zym!%^+K!Kq_byzmUjDGOcd8cg>$}9s&ZZxZa%K>IoTtyE`*Jo@%hzYpRr=8|chyEu zkg1P8aA+$>V&h&W4x;~G^pktX{o=xW{SBDU(Nb)b)iNx45TTZwx`^c+&RE2v4ks#N zDQBh*=9wC-_XGiWs4c|ZuhlT#T_C!HYK}J?7_yA&2xIXn zIbni=xrDWrS>ssim%9|!T6ikL+A$3D0*n_S^l}uL#RKq*FrH|gipFe{(AYoDI0YK} zw^c4nqUBt)JY(pJ#KCU?m!>mK=>|%L7r_@^1QHE~ z#Lxj6a}KtD+&Ov>L?=sTgq{f^7b|$=VQictr{1vDni) zEw0>M7FWk^i|Y@2EUw<)SzI4~-r`dHz9TfA!)G#ufWy;o%;qB*XD`+d^x3b^ws?;@ zBZC$`3-v`DHw?Z?{PgLLi0yPN2pV;n?#oz#>*cx+i$PLwox@9HnT=P-GP};)Ifuct z!F(qoCOG+YVdQPX$lHaHcL*ad4$O7pW6k`0hVb(^ zC)i{SZOa)#3Jm;>%P+*gp7r=t{uO%<>bo~{5&O9qao9*M%^xZD_>|RE;V_k3au0-` z;k~F#)nJ>Lo!_4?$OI3pL3;t8!?m1;lg|+;WsY-I$^y$J14j!uj{T1N@?8ZtdV()& z>v&rd+f84XXBkf6$QZ^8!%iHk_HC!8YsMU={}OTK>5vfpRNuI81jlLG7T_|3_>O!l zHT2&ny+&Jh=tL~IK>TOE|K5&+(cj8A=uIT zQ$v3|>D}6TY&5E^=ho-cU*GFOn4kxkEm+^nrynoG_UmutawvN5iJ$riO}4pp8=CNr z9Nc00nYWH(zJw|G@T|5>FNJje(^NC=1E03CCW+tp{wz%p=di`9H4^~4`4H;Ym?rJ2 zm0DX~O-01-sI2wlv^rQJLiz32&b1&O^T({LX94|^7j+@Kp>c$E+NH3$($mC~_=!G; z{_HSI1r4*58oi%a1<)vMvAT{wf=Qxb0=ZU?rq!YI@yhh_UVRVW4JpRON6qb2=Oj5% z5q>$rpO24KIMqn8`JDHVPxer;$l&?3=y}=|Y zw$!FQr{cfI1G`^3)bh9Rr>X61ZKn3=RP`E~HPcF&P3(Aoz9n~{;KH`^v=|J?&brlM z*OuE-_tQsv9OrsxwjRwrKxB%OT79}FBWuOPXmOMBSrtveXBCL1m3v_F2!kGbPdecZ z90wRxD_xe&Y%18D4kb~42LH0cs2@aAe3^8_ec5nAa=xhR{N|! zB4@h}y&ozf#2!rJEI<9MqDkAx*R0D)yO*z7>yvgrU$Z_Z?E$`K%}?5ce9bzdv`u`? z+Ml$?_?q=ZX`A^P8l`YIpK0>3Z4aa5p8f*<#Q`vZa5@N{8h!wm){U{lNgrMUEbE-+ zH|*b4w&jxnqjdv_9AL_AMa@Is5!+^h`4spfFO~7BgDwdn<>pm>)yeq`ziGQV;XFBm z0krA|pL2n1-DKBS*tIvMAhJADUtw!fkBM=(P_9@jCy2Dxt=GEr6%G;{*_KA-EG=t- z4uM^h{74f@PkRJSRjV4+Lc!~VX4gw}>y<*}8-=U~gpfDGbQdxQQ~sddt(1N~y;~{$ zGWG7`rsl3&DgE;GZl(0g(z|(~sPmF;o2eD-K4EGFyT_%%Mq#}jy7KN3F8qE$zx=l1 z>cO5Uo}O@^9zOudiKqi*VeKQmrp%)pj1->_EE;F@SMr&1F&`i`sxy_k#2M|XQ4yd5 zd=Ogf!KnqXdGHi&Mw1U{mn9w0;>24{XEWv5u~D3Q;X!`)QU?7533ypdZnhKg*>z}JoUTD|6v2h6Xlz8b6WJc3Gnt&b}RTOy&;Dk!GfqZ`0}0E|(bu{?)QgQ*o+!RiaUPJeKlqi>@+u0)+7b+)Yzgm#-}*n4<(xADOgi^LGn)Zv$6I$#$e^ja62h#hk2 z*M5jGg|<2(U$!m$hpc8RAMzbD5BW|I3$>R&?n`oVt5b((=xEZpd%Y6}WbkR+rMkxl zWIalPbRKWHcQlIFbfeJ<>IUE#siqzg%BVD@;a)ki0^Sg74P6dtH_2tdWT=2cL@H)H zq8KXRlK7}sCq~_>U7i;j@cE7JKgWVMI0tQsUAIQ%It8<6}gh0xYU!w{KTE~#EmB`g3;pR6PoUXp}|*) zPtpd}xg5!HBk{}0_Mye3Dg&+rmzd`@1x-UyBl6mU)6ZBj3x^_EEEEbDGc#pAwk`oW44bL$uD- zRIKrLsz_Q9-kZ4YZ{{)6T|-QFwI`s`{bl%t>CO;T)T{VxwGM>cRt}wyO=P!~E9Wy4 z+3kEb8MxnEFy#dN@=r3iUCg#bWzwG{o}1r@^W6Dyp1V--+&)gjq}@^6a4mL)A8FmXC(341aAU;%pn?qK>_{(N)%^B3Rh$f387^0!;@?R?JY zMPR5QXk6Vq~8`;U}eP`#DpkYIq zHL&mhOgvj_lH21A=ojzxLW2jD1I1VfpoHn4u_O!?|;@W4kVP!Hh&V|)}RN?IRR zT;>2NCXRZU4Gi@%JNW5k4zSb9T;Qgcxxq{?O9wB#ECa0cvdpmySxM<*Cr^JxU{xGH z9D6T?!>jf(M^wv$G_Jf(B`@q6gyRrxxCWS2Z2J! z@i=}r9sNmR`cs?x`m>BH1Syj!^&qp?;b)lKhXsQV%Tw?JEVn)AIf)~;*$*v1(MBs5 z0Z-6t8}O&sv(Cz|;Tg`+#-+|-sRQOUyv}n(>Wo{Z1Lac3Cv_0}fIeR=bvUe90r#kl4oxlh`Ad zD;4+2#$q34QMx)|6&8rI;kYX^O~T5l!Ch=4w$-#UJ>K$*QN3m~8d$e9$0ahB_M9%q z=p@&a#YHTO6x7A#t8^)#7NfiMJ<+{~wvhxNG7^{qmpoo=3P#lofDF0x>pSVCUG$PE zR)INkTjMgAT`mENOcARX#5-9O+oPL;l}IglBuS+!{e3=NXDXdzAzedy&oUe<;O2@f zp3Y838iyW$eNvWk@(~8U&Dq4q-?%pZGA6iq9q)3u_+|Y&p=04`OfNscOdZA1Ucw(^ zE78W}a^t=~QbW!3u@VdNX_nYDV7(@5MJ2w4T(L&q-Ha@NB`WAIG(X0zzg__3aF~x= zRM`zwMm@kB;r!*Uhn*4v`Ez0>LoApkn8RNgs9qY^Z=PU$st}Z6{pAA7(Z4`>-^rLL zP`(t%|J;M~l{@^ka$c13m3V>)cH#tHNeeD$veYUNRS+jo{gM-1p~pCvw;7MsxGYC+ zf=j?aa`8PV{&#$HoqV&9zFBGf{DAtVvkO67#4P33f1CQ<8NstAez*BmFjt6#QIqtU z&(miJX1-*YEqah)23#q=Lw@777iUX|@0N3k$lysuz!3-6x8DZ1l*?E|)?FnizQ02G zA{mFSb6EIv2s#%z_rNNw93@Ut+smqsYNhI^HzUu`b&#Jlswd^XR}kHd_Sjs@(WBPHgj;yRFM2r+d}K`( zE0#GcrviWGWZ>UyVBH~HW^EGipRAe+{O$ia1^8#YK)^Rb?@bT<?JE<2 zzheUMpZos_@b@Huzdr$dEDPgNo71Ad5m9E`Vuj$RzsD`{2taz;@x?i=3SDGbLcZ%* z8AbQ}jzJetc^Ru`IxS-!Z9Q-GMP+^hEEo!ZU~`;x`pxkPTm7t`EfCZrrwuiXmki4_ z@B?IA*!9*wVzJ?FD3+~2M%Fk7k!pnhG#@=YoG42%TBK}kqReXio0R3G#LFzk>%U-x z25xCY%9uI({GUXCqx(t1o64*^ncwy#`~r#TC9dXei^4LjjTaBhPm~ysA7th;orEt}MQAuv+h=_DKcPzqI?x)dW`9a3s#f*LxS`HuYQKYH z_8_NRbjy6e2fUD>W}*J9xV8b3RU&Up&UE^8f}$}EKMOqu_*v~u<&px;BX?_ zs&^>38D5wEm65hgM2qSi2xe+CHGd;+3i!k$xISJWM<8@7N#szZn|=mWQ1Cw&=O*o_ z)gzncUjCL}Z7$sc3P zJ&WV4XT2`Rme8%&u~tz0xB`lqQ!2p+EJ`IPYM7w1@rfHWDdn=Mfm2%NjV7O1nM&ypM4$+G0eCn+qs zxvi$;#wRH(xv>Dnk{ioUzD(1`R-`C7Kc2dFM#aQ5x|=St_<{(p$jXV6A$~zsDW7)b?B&wTZeYZO6rv1>&5B$41yeUXZsL55T1>7zm<%1nY?p-dFF2R zWyL+Y#8T3iBNjX!G3!Y;#XOPtdP1tJ=&l(bWsg<*bt&#hk~&;-j8Ep^S;=7mg+Gd0E?#rI<-XCEaXN>MNleROwbAJE zh}u3TD+A}WxCLf2T^^ImRRn|Mav9XccMtnhu|n8&bZJq`)5iu^&qa6AM#oVec)I84 z?{1d%sEl^8IM=NvM8GKE&`x}WR|>UtXjgoXMU0oDf~!}tXkSnvuZDwKy;w?@RnSYgBpDiBan0a(aN^-9!yC7rP+ zW5AsW$FH49CsktInm9a8PaGa+s>5Tg%&z%uihZs$KAz_QJ5#JYM9qVnVrTE_cVn;` ztA}kz{b54Jd=8!^yf{q&j6QRu8JY{~x;jGS*qqs;X%7%Pad415zSQY)UUbk@3;~>j zaovzznfgH2M0lZ8q6$)u(VTVcg1bcGeC@Rd@>D4Z2PG|x<^ zSQOGSp7|UJ5rzT^>=A#kox~E;<$uv69%sMea8_)xp5n}*V;n=Ss?i?gnP6T^$txcl zo*hppwW_5+xL1w+U$we2Gv!@PJPwF;*L;h=D)3BPCh{ExQ}5Pf=S6Hc4<5GZ#db$3 z=0EskY8-*B7AwY9U36zYS=6j*6E$5zb?1&N>^yNbon6cw(&L)GtlR-}#>5QSqgazH zrJz#Oty3;@O!#9`6}V;hH1pAt(vsD)erK&*7OV3$z1Zf#N$ydrUT-U;I~tG8M}G-t z+lpHbYfLXA*IT_0xrK1LoqfNd7jt)dk0MkLTdWKs*7#@r63A<3m}zRbk5_mfUOO71 zH@pwA|K1jU*y?eGxy74igttnX1S+g!ujXn-s|m3>gHimzp*_3@2M+C{7Pd34hiAfG zP2)Z(BwQ=Khb}_+My{DGJJkYwrv7Ej$Qii3NEmV7#!n$?5tahpu<>wV8F++U3 zj!0U18ZRh*r07Iwt1Vh)ga&M(F{Gc3UU8B-?K>Gg4YccI%P2UN*R3tX0@FyV6VZyx zwBId!DH8OQM;6-_zRj6z`Fu;P4eG$;Ae(J*%5j($6wjbPCtCgpk*8xDc7*9+#)iob z?M*07uF5oi{UfJETO(7|WD&D6j-QM@*?&awWc^@jRW+XjAQ6Kz)H!C>Ij-0?u`~0T zMrH^($og}H9Aw!9AqQEILC8VKKPq(2eM=p2WlY-E*&L=l-H&wBUw9V>XzWW%lH}*6 z$m1R3oR;^Es@wxa!V!6~M*XEfFVdf->Tj+3tGxq_$ZQrULA>Y>OwQ>~K%`6%KV%40 z;d)mhd~X=mq>p(xS|VqCiU>Om=FKmepCWyJAsBvYgX1f9t8`b@M&*$5b@B*($r8kt zEjC^`f)(?Gw&jbBSD~O!A3;hhWqVWc>(jdxIwO%$;j4h&t?*hi2_+UKz;z#yt02iG za<$sI=2>VxJYBuBtdQy(D@=j(+n-rrIr<+ddPLqd%UumA0$(i?d0 zI935$7|=s}#aMkVJzOTHLd{zDF_AG=Lx~Xptq!V+`Wp(P8~^?>dSgKf3siLfB$&zQ z&($E)Wf;+)tD(AOFwvi@0gIWsnFnl;*sUREw?5eIY5(9~&hGdl45LS@JsAsk4I0kD zSI-jT=vUb|+9Af#|nVkBl?#K&QD_5P^b^7E1$mAv?u0z8GMuj z`vr8?>?W+kf%4p=ufm?CcvS!X8N|dM&<$QQyJ!SiV?m6A!K1vQa8#KX3Jv2`_KbQp z;Tbh`7|~tPH>C5_t-Xg{B|N!XDHGk?HF1jaM8)+8h%Os$62&aSD88AnH6X3O!PZr; zDtw%+Z$Gr_APpAQcqSkx%bFF0%{- z?EynHYyJY_kci8JaYtoDh&L7{$W%pzik^X*$3DA2SoOn^btYSoQK|R>Q^$FjN3Pfg zvn!bi?$Dj!G%^%NV!ZVBeDiEgD@DgzBUhU~SAJ$ML6x#ptPNk>L5M^1F#jkNtvEjQ zY;TAq5s$S|7N=yH(PFmnDrb=(kyEZZDln=x7OcmigLll?`57SUhCJ=^R(L36_>J

xm~<$~|M8Fcmd=5e?*%YG<2PldUOj{L>WB_&4bF>*)Kp3Je{|Jcixx$?0gYqR zvt}LD#0cf`pb?;NXm_zcH2Cx^wiu~svQ!3Oa-{7xe&L>LsnYfupV>_nRU>%uGxVa~ z4dE)%4KdMQq3y-Qy$zRA*bWI9POrhp0b(Z8)#9g;C4MT|;-``$ekyt5r;;yzDuoEM z#RfuP5Q`CL3yeiww(-ezPU4)7LQ)kt{CaU54&EU(z!ZW*0&aIKnX68A)DZT@;ZPDZ z8t2f{+Y}7gegWD1swxBodyV_&&9wvup!>djnlGgW&lWf!;U+sg-9Oi*zNRy1BAg3A z%)QO;zI>Dsh4WTkpG7t?09U2oZ$sP{HUl?Hni2ntjls>5YY+>JO~TERGQ?K%jeonGu-YgK!{yH|-r**md>ppa;fJOefVt^6lkvhr5Wr;svm$)G3YlEI!X z6l+U8%*|99#m}0jh-YY>9t1k(sK2kj6^o61K`Zrnhgxj817l~0PFPz~#$35OLnl(4 zcMcNYA9^)K8`Q4y5Od3~RaD}|`~L=|;7OhtF{X!mV-kdH;^UYEA-gy_nh>%RXpGl) zU|LbOE>>{W<4!aHQHZV}M_^SO&jlW*&KpLBBi?fdtJ2ZN53b0ah->t2I zZGEV4Z7f(1H;9VJw=$`nPv7B$Pufatl?_4YZ{x@X?v9A9qmrJw@YHA1zGYhq-YlRw zHdM&>jGE2Fuhj%j#AU;6FGDs==f>hKA?n7EHSF&264r9|uEN;@ap+wG#HW&2i?G#@ zYOjT9f9mBTmGpYR+r{PEj%}!gcz#z{_F1j2BlPbd^v{)Ib$yEdt)PFC&mScFIV<=b zu;q^|&qQ7$_9BZtAKHQuSXw@<-Iko{UAwtVyTKM0er->a@LRhy(7G(gb<__({1ONB z5ruo_BNF9TpP3I*pv?)gpbkHjMR`r9k${e4@Xu-`b|qM!uB|Q;GZ(XUPf72b5$BHwu_Nms1O3g)c%ysbgOwE^BQ5ncY$P9$+Oics$IXpFW()j+N6sl(TYbna5 zbq!bttum)6#%`_p;%XCv?ba9j8GS7-2kyFe&sTO}4TcDAGr@~cnMQen>>4uBHowvVPEk@r%(1x^# z)7+!97Dq}DLD|xlM~evvQuZq?F~{wGt;ps#mOiOAYw&dH28WDV%da|G_h?HT-rI23 z>Qs%}zu>ZjzG1h9O6*p;y@)zcP-1WGO!R5K>eF5LP6^zpjlcJzNB2(Z(Usbg6Izi& zy9*`~^d-%4t5#w2R~p+koAmH(ZH@WR-J>m9NpEAHEt2+7{1i`!OUg@ZO^I?25 z(b2UUchuSYb-+@fqk7zSk+$TdcGpJfZ-wT#1#xpebBqW;i9HD(_STowAj}s(srpV> zM_zh)bK<3Y)k_EH>y?sl0)zM~0_|~3xAO%deS)kAG{yqz z2xjmWZJxfkUR#ve6e9#cdztaaEil|7u9_tsD?$U~sN1^9IssGC^qj6ODva${8Md(S zZ{==(zl@&TVDk$qj-%kp#{>mu#yght!gMH@8ot7YwrwB1?YAD4wg+m>wqJaHy0$-r z>^-cS@yDBqa5L_S%_P(Fgx!U+6SFT9i0Ig`n_%TuSFG()Cie9lKv@NAmT*lAM>t^9 z@Gs4%{_=h$Ncru*CSLG#IaO$~R-mY#>OoER)(uWj`B(OGP5|R){O4n@&>yjHJ6*d4 z+It#Q{2pR*Z2zQawR`r&^I*Fp53#if@sf2}b1VpN>t8U(5@Bb`D!TjI>B-S_ZD%A$ z(_MXPEYC#u=1~_yH_^M7w{MZ)ow-S2u@M!!qaq#^`>b>azFTGd z`V&?QmORJ92Kl7@Jj&Mb*9xFE`zs2PT{4OFUK<7J>51|f7CB=3hbFEEMD%QARt{Hy zyS8t+3Xi>K^M;A-Z9Y8lp570XA(xBfGPvnU;zYXF+T#JYvFmYYGt$w+NCj8x?CSW1 zzM*mX(1iYUqbMFPdDq!UWs4>;ALBC-2N^A3<_^uA9MBl~Yyx*!IZP>Is>D#@&wJGK ztZANyp5%T8KPMf70z(VqbHw4@0Re#|GUQgv@bKT{pB=I1x28{xHys*p4sDi3m%zU} zSo`F85GGnZ_P_ruv@^D_2hE4eJ*%`HRMSKcLM!TydwKCHy@>gj!yHQ#?dC+>v&sF> zk^ZCCICfr|2$>ez7hCuUkEq<^TFTu<{IsKA`Iyx*?fb1C?fpfkdcXanz5j9b{#t&& zaA~c^myt)Xu$q8AR{fIs&t<aBYK?pH#DTmI!x`yDJ^Tj;bBo~{1fGvO zkAJMg&CcU1lE+(xML|;KJKCBZv#`qd8K>C@+_#}AkeY?F2?5`=XBIa6KI7N)EV3r5 zYJMgSHBYLVze^=D5V;LKF}nUE7IQv+*S6X6+F59Fjb*mH_5fAv7_rga(=H}@`-~c@ z5E^lW#;h&Jh;AkpP?aJ#u1L4^?>XZN^r%nz_ndLS2~r~cd(OBA&$^?k=3lwyNmcW7 zy3@|mZV|K;+g~eCGcI}uIjqrD)biq;^_JPv-smjq>TtZPS`2h9pQ?n0r|Il;_#Da= z#A=oeV5e;pNAglZb%!bpP^pdS)D0^HLwcrQ$c{nu`RG?4jKz}gyY@`HPy2(qZ+~(S zYa%#XJt!n5;pp2L);N4MlHF<$^n^K+0aUJyY(d@NLKU8x7CiL@^tp;n3+J(kKjGXO ze+5Ws8MWEIg}>7>59vp6@Ksakhi~VD^eLu}3RB{HE_~0G_@2xIW;ru>R_ICj<{WsW z)mGK`jVqFxcS6}irC4v@p49Y&@t#Ys+o>-kE6B)=uly<&+hiqn?C6Qa@Z`+(%cv+rlMM0`~MwlOs z6a`t__&2INncbU+|I~;-4F3^wPQricCBlFIpmMc8!1~ehCt9F6t(FPj)8_bD2NfD< zBzDOYurOO^HmH{I_8+VDy?IsDWPECWayonpGv|>Rjmrt0cJHD696Aw;gFjuH)5G7S z54vLeN4Nq9=mPe1G4`zb2ggXYPnZqf@6N}&aTFV8m@bMrEFnhO7U!LVz$s@_iMIYl zg;Rd{z9T^+p*?wnS(A1OyBsDqR z%bv}a>IwZh`tCnePt^0WNzZ4$GvPV%zkuc3F`L)!MaU}W9dvem4s7>jrB*E8*@~!j zzjYePm7KlA2FndU`&epym@NVpZ5LE!F22%lX7|!Q%U%sQaj|LZQGu=;O|cJ2ck(GXJwsbYuHQx{s)366bgBdrP7kPvCbpM<;%Nb7sxN`#{a) z{;i+3e}YfSzY2VE*n!1shH+}0I}x2k`mpndfozOO!CXrcQhEP>rbQ|f_6G>1Fb~#R zXD4v+eb01V>iYV?-Z@s}6XpzlH?ss||7qgM0b28;X<5ZNE)Eti>&q_Y0|$@Nd!uPR zUn9A(+T)09K?e`+s?>LBvYi1N1L20zKoBT@{lN@~}kA|7x(6Gg;MIH`cPKs3Kb)5OI?&uKV*G%2p?zFA_cp{e=$m z^>}3;63(rc+@Q8fy}#I@?0d=_%D$)Eq3nD74rSj{?NIhTbq;0UQ}0mrJ)C)3rqime zN3S&>Ff#EUr@sZkf6|m~&RIHP{V;#Ev^MexI{7R#!`fVS6CqYAG;9m~)!On#G7ni% zCCpJR3?3qP0}fkURr#*SlMJMoHUo;w8@}hXglEd3ho^?<1GO|_>v%ttYPKGPzYF!R z2$=_x_+9Wy%O>M{_}$k=9*y@7HWn4yTdVi7(?fWb+IFWZ~uE)Ba{GrVc( z^!H|Wh=g^OOV)YkKu-ryvrtg2i1?_W-D?fnXkvzP2PW+w$#11wKP-`y2^TG_kZwCx zSnP0o^Vyx9*xj{Fc4Nb5t(*$Em&WMqdxsuYCGsP6Z$Ms`!p#lbLrRK zLZaaoRw~4p5dP=MIYbE8dE$hy*HrYGgfQ#d(+K|Nd=XoOJ%}jbXK8mpg&!8(2;uW^ zAI9!yNtI=^_CC-bUudu~kXMr9G9F5wl=)CnCB8U+AynFT4mcm*Q4!ck7{u`W2hM98 zetr+RF<)4`fVQw>vi?ko`;G{Ag7dYpKG@VhmcQ#D^ls`OzeIO5gHOCpm@ckR1TKoN zk~n@z9DfL%fX1LnHrD=GYGb_!(I{Sn5DMh_$;*r zkIhnB@c1l6Oin=B>ON^IX}Y0vuuR-Rie6}MRw?a-wmBASvP9Y%#kcV}<3~u9d93TL z;6k>CD7E2`{48yetpb#xwRC}Hsaf?~s%ncK*!(pvdG9QqFgM<*uxU78uf$ny3B4A0 zQ0nMr9luTcGMwsF(;q{fd!^2APSMVLB?~MQR3fsll^1GabLQ1*@7%fuqBnv$&9U)% zE63o=^~=kmWj6it;;5ZC>C4NbW%hWgr*gJ%E7#Kw)3#u5li$5^cvMH?l`b44Gw<(R z0|-0&fC~?vnH9)!34eMT@u$Yo6qJ54lS<7f^nNybuJ*tt#Q)ERmFJhGrBg`T`msO>+g9o7H0uds(Vg01JSp!Av^L5Z30D_2s)lH+qQFu|w&Z8EfhO`?!bV zDti9$uPN9&yUxTJ$kmO3BR~ejmJzg~1AdR;#Aw?!wT*A?<;t7A67+K+P*}b*v^5LzlEDU9z<~p?2s=~<9^2eUz|4ya@)QJ z$Q6HVp|IVT7e~IE&zq!<_7Pvy(;kZ3t=1`<%=)$-aHeLzzJ=6-*>JX=p5)_+}v^w_-D08th`5Z z=v&N8Z?ZCx;qxpaKX1`0vj$IFY8-gX!Gw5NXb{fldZd2|O-4e&WHg^Hxsj1vie|J{ zTWxEXN>QMmyayhGOpbK-Rt6>5*;eX4a9@^oRC{y&fK{IN@k z)2saeR!^(&g#C)u(<-vV{%oxxC+yGBD)PeqJgp)>?9bPW3iY}|y~wB6`ShYvev;s9LY9){E-&x;nk6Uazayi&pA&EA^sAy{=I&3g~qKt)e;X zZ`LYSh5f6vieT6u)GAho{j2q&R=uuO?`Q{~x`BT?*zIZq|8}sPxq*K>*rIv^|8}s+ z>IVMpU_1T|{M*5xeN*3nzmc`D7g>1hS!4q+{jqFkftUTO6|ORW({6_sV|Xox!Mgo1 zzGiER+c)zyA2Z+n3}16Bz}pA-dVsID@ioV{>|kHU4Li7;&NsT;yv#Q*%gq74IUqL&`3Ao$^p3;e zar6!NqlKYK4ENwyu?Q?*F;gk|nl(vj0mDl>*nxCDo2d0eW0^@9sj3D?`^Nu#$x6Gi zN-$I1ODPhrXI@ocU3|HIbD5kS+$}KX(BEV#2HGIoTM9B7j&%-Q-WuNw$$^*X{cJy)s)5^yzzZZvSquo0SF&@8pP9>;$|* z&(_P$T$oHhJ193+1k18roU|42>H^7nnC%VpfN$&swz{pNpvn0{AFI<8rJ zWXf^P#Pgc*sn6s1v%RGuekA#EIgRv3*2@7MA4T|^NHF9By6CHOEZXDz-QiF2px&QT zkk`e7O4n>%7!*z79M&)1f~B!Mc34eQ^2ukK@##Z2`J5g;%UjM=0HUh10Ana3V}Bf) z+U6h32-3!Ij@(sd5m=bEm z1wTdT4vN1&Pn$U!)zmX%4L-TG@B8>?#Z{llt4XoprdGgFfx z9ddxemDzA6G-fAsWUr&gRW*TPsr8mE(d{Pc_HXl0o}@ve1(EU!p?hXRV6)@?hZu}0 ztleH|U<2Lh{k$@tOUPNq zTpnk`wZR2KklOS4uz!M+%gosRaQETaP}VLR+T2SGjg3ykPj#;)QPOvRK7o>UjB~7v zH;8Z1t39^2*6=|cUE1lN!`ZosbFj zrdjN-$sV2BnZ;B^UzFwCf{1c7)C0vFc7>Rpjz&*9XH6t#;^3Us&8+Yt`RtD*ctgxj z^26oz_Jeb@+u(H+c>=$~9f-SX9Xp$CEAL8ra0?z-v>E!^cyOMzDlluhzLsOS4vmh9 z+m=&XGmdxKrLj5MZR7E_E%Hp?r5f6)Z^)Ow+(`6!QMhDiXN|r*s*59V-2M1RcxCLO z=*vp6*OH+;g{4ii+6JzcG8%mv2Te)&q34wSnBZ2FrJd=fi~9}}6ty`I(r;*7l8P;& zN2>u_PBfM!nxiajE?ZqJ%7Vu0i5@xV8nd=M3?2+;DA=Xu0d(YHoG)_xf+kDkHtZ4O z(QBO+ZOJh$8j(m~e&Z2pg&pQIu=`c=RZnD{MC0+#bbp=J)X zdfX||t8BU@Iv0@@?9r?2!xH+)lH?Gc*8GFdxem@*Mbm$1XY>hdoM=e`jy106oEMS% z2H{KIpvBlMPK)eUU+oDxBPzgSx{4>LQkM)%$TLJ9*d(PKN8eCMPK*O##=$GKW5#~E zr17qkh*($J7QPUXxMvgMMIcU6bS~mMj9K+kNA!y0G>Fpl=oKdzH0@r!u|NEwA87COaHDrmtb=S48}RDQ||p2FGb|bIK7Da(xY^y9uT@ITiYvI-YyPNYK}m zOEW9=H7n(+PG3_kSB?4_6)7QuU8jFhD0hYIH2n*o+`(%;C;AH&AFnv>M_8SGrW zQaM-WD@zmTy&L&SnR;S|34!{uj@_>F;%?Ugep0QA%XOBD=#kBC$21^zTS$Lxwne3W z4{e7jnmcsD=Is6Kd$Abt3Y>!CZ0(klvgr2$r2YSUkl^!aD*oiNUOYGs`G`qHx6%0PSk>v!Cn^!1K+F_zZbVb$z# z;LOg&zi$3bhna$Dw`G+Rv4(4ho`rAB(cbrCF}WIxUp1QMWAXe^65pb#22^Y*{iJJe z(yu#Vh|&zgW}0oSoSvd(Fhx@l5++eJx3-Tcnwu$_n<$!FQ8ag)qPdOB4ww{8>mC=& ztqk4Y%2aY~J3!lgY&H`XD%`2mcZegVD9m|UA6^@MIZk6Fc=@u{vH7So`Z5U2If}rn zKPCi5>Ri)`jZmTW9T)4U$QE1}85`NPKb;<&kO#(TRP;1FNc6PwZ^Br@iPFE<5e*q( z8uDjx)gf$??bn!{Q9&LmDie&-YqQu+tyo2Fh)7gO?D}B|)@Mlsaws=LOG!uy2?B2v zbrSOHL-W9i)a1%P&Srz>M$x@qGfryFDF{& z0PS%{%kq^y8`B$Erb2a^Kr5hqv=H9Oj6Tj3XmJ*wD8Q7hHcLBQXpjncFTvC}D83U& zi6{pL`1RQ$51JW?awMuuo>`0QDurHs!W;09W2+vD%MdBA_*bOWf55J zyoJ9*iY-Y z4aX%kJja7ysU9Hkdwq%ogbySIg!gm#@s^*D>S|&~qw#4~VMtZP4k5qhTz8rgi#cw` zid$%gYtupAXnH${X~ha__M}^#U7w=OoHV6QZ7;p?R@zEhcr%?{_)>&TpDF*p5YCQ=tOi!G&i|8H_cJ&wEHR9I<-eZM{thlZ=Yfk8~|1 z%)mtHpRuMlgXNm^2uM=sQN2kf-h5kdbjM_RR3E2Du5X#x*Zt!Oc)F!R@O0FqM=yTU ztofH8DSA|FeyB%%=;M>=QE{9eB|h}4A5EY~h2NV@kJiP%)c&Ih^eA1?qdcZZCnwOO zG}VP)PEU^#pU?i$gmKQPm_(2K@vo=+XaYSdj?*I#)1WN=&F0@6{>|gx{12i{?Qx3q zO8lxWmTWLQ4SjMY=u<|5K4ql+>*&+;5?8wMlNXsjWr98(nLwY?P5NXc(I-_jKDFrB z)S}~4i%v{K>oQGR_bILKND{5n){oDQH)F;|s%X_KaojbQpP+sVRsBBnRA`|3(v-J^ z9-cJmAubHZolFli=>ejPfbzYwPKxxyRA_UE87RYq9v+m3ZhB-_^>Y^MxtIb4Fag zJp~EfULGf*ZDX^Qasu{R|0jBS^;bS3J^fS02hh_i{x9?t`Y>aEl0IzghZFQ+q49L+ zcLQi6(hoe+WT`U#@czY6A*>HNIK_*;el=YLUi=mx8tMJ54)|vs6A~4)PPBx$j_w2d zscNn9tr-hd(1#05B_33G$)*wG&9g1x)R5QG;+kk55l+=+ckK)o(|zUYDpoIxai7h! zF1MSO;ttbN++|vdyG=`RNLFL>^*8kOxYF12gY{f*J-@b|Tj56k^>*!5T>D>X(eAe+ z9RSSQS+$%l{8G}1ptjF=X9pbTA`hYOHQK(O?srbJSbDnuLBGa(uf>f*pJg-kejf8o zzqZ%-T^aKZsl;YDg`GMaXEV0nF3bb%z#&^ljP2HUG9xhQJo6O!D>7*U9V0f=^2)9( zuWZ9qEcx1qT)-SD8#Rojo%1abhsS99?tIIA`Ly3?wcO}U%cHB{x0hbzofGXh+bt?j-PIgJDV<8?%0^dH;yjs9xn4o#Isr=kRCrvX;BhgUL)ZDx8wrS<*w$!1T3S;0qW>gxNb~X~r4|%-*g*RGtLRK@q{C2AymR$tbA+6d|7;)S% z_$pDj(;X4p=}?dn>_HM&;Ci`)c@~GPbC?V-v#~eUGP|zmE4xiG?X*c4RJ)K@M?AQ_ zof#)4w&}VrOOvdh&F}}QAb>Ur0%(^YfDQ=)=#n6SZV3XIEDp%!3Fpm3^DEjVDdtk!^l9opC`tV%+EtSJbN){$>S;Wl z&8PJLG8)R@Y7yy!<3zZXi0=%~svMW`_^VDcIsH1~9K%~S@NX~wuC?(lb%eJ(!1o*Z z_g?&V-%r2j$|=v|sXt$t+Mg=bA%}cl>qeWo2XbSa_&ptLQd@!Gk8XTavu#HF%JU{V zf{DAui0w&^)dWupG}F3A-r=@Kpu<05-yE z;je_%!d0a8qL?;k{%!uD2lx!H@Q^h z46Z!VdQ96c-S>KB?->=4ge|+rsPaM0fu#HJ$e;_{g|M}Y~W_0YkH1=<4 z4?PfHq!|0gQ;dD&?-)Dka-6aPZm8`OW2c#;k7?Ci7FlN|0C4EO9dAZ zo}pby_~+g5Rkp~L>4cRQzR%-9Zt=mD9P@i6QkoRcyjQQgS1;P2*WIrdZPe=?(2J<# zLA~gHy>63U^nhOXm|patUbk5<+N9S#qZd7<*A3`JoAtVFdeJj_-44BIK(E`Q7j4t) z_UlDE^tu=IqCI+DR4>}E*B#W0UexOj>qSw$?ucG=P_HxeqQiRKxL$NbuREp}890S1 z8rSPiIJ;KD>HCL0uyJ3H--w0~o{?_Yv_17PK=|t?!-3=4Md82+b|^a;8g%Qn{Lp|K zLqVe_s8RUGW%6>sZ{#75O+-hmn~38x2zmL!E`s;=W)?tN#`F*zNmhtIi8ic<*qub2 zNWB7P+yb}A9qk`Viqh<8co1Yh%S=pmJSAg~H-9R5{-jRU_buyM#& z0oy4^0*!a|6nYREZ>-`Z>cErW#6TPs3+f=8>n5otP#w&1(X-V?__g_j1)QDpV2`5( zXiG?LOhU*iJ$1VA`_*P@G&eG0o<;kdc3>vbHJ>HNl5B&AvDd5R|s6ua-LRNhF1KQ0awuEwrjgZCArsleHpFqez2ydRVj@kkEB+BxMK*yweibL z>dQR8X>8g`b^7+qQldztv=O}h=ent#y~d$CVlh_Ez@b4DmR{&h_8Gx@kb{~Iya_$IkfB8qJ>*&@-l-h-t9NLmm251elZ9|Hrj^c#@IF9L5T7< zag5~Loln^(BJq?rF6Nxpg2Jg&Z5L{eVYq>W9b2{XVH;nTZ?yp(Qwy!HdjAJI@sV+% z_2rz(yU@5VI-k8x+GUJ+v|#E9`_|R?13{X4g#X8>ROAPZ#?PCWQ-(X;wMXYm!d92F z^THH1c3n9(C*i-Ua!31Y{(!5TC&ptea4oRZ)HGS?CKI9Zvn$2s_LUb?03o+`ue0-K zR`!}R{dXZMID3ISMO<*+)`<#)?KF)Y?Cw!+c3u&U32^Bh+ffGF=Fmtm_Pn1~A404l zZGpV=4Y(=XWD$*+)LV_SXj+kGoIKi0O%{KZissUPO)N&z(p%2X8T9pnZ#r6Pv{E}w z+pO?69pO)hziAsQ_Lka$mw9IpuY6NVc*fN5omB+(aH&IP>(gy+&E3U-y2_y_>5hRZ$m{t8|u z#;pNf8?;QYJPQ9DUmjh@-lI)k?WwN}1W@tc0`W!k_#xB^X30u{Rn_?uM9M}4hV*N< zz|sYC2m=`SFkv*tJ5k=|$m#A+H4glKJ{Dcpisjzz4OFK%9zl8Vs0^aqlONyw#gp{| zOt0RdSsoDQmmWS}l|U~^aa@K?E8~QJDeHP^EMFdUpgRamV0;PJ@jxwy2iUu`U{5U8 zPOn&k^XP8u3Gp>eD8?qoe=_X!Lck%e17iMeF*-+~-3p$q_LlL;m`px*X|J z@i5hRX~8{Bd(zKpM1|UC1X^)qsSNqt>^NI9ZjYOo2Q}I%~dtIufwet zw8bU(W;Ok5rhlt6(HGZ}RinrN)hjA6D*J zU7_xe(lQTiOAl-cvW4QU2Y9bc_Bq%jtGjC;up!7R#d)Wsb~2VFy|9{I;A1fFd*U?# zH{xa8<=UU&u7Yf=MjUrD#*II#DHi;44N;F`df?cg;xJGbbDCl`%=1jB&u%Z% z)UJ>1KZ+=$Oy8$(t;ssIIX?jLya2-WYIAjVOJPd~{w)3KKPJlxp zpUD9hJC(f`avHn%4mMuMoL;6*d39GhkrBa7f!3&4(-*En{QC1 z$gG!iFqn%&dwzFf;SPN_?K4w-m8}mP-0{|sHCpb;(jwNhAxSJ}j#No3=Wh^dw8!n0a(L2+CwXQwWKxX|y>tatLLMN&?n==(cS? zqPs!~r^~5$6nOGS)7A##;FIL?xXPx!`&4I`$u;5f5*DL-RX@5XNj|&&`;+CftpA#d z|ANF0!(F*pJ4$j|K8?($mk@2`goFAY8z z$Gc!VhpHkCjRD_cwuQg%4}`adot-zOOgzA8ru#tq)m-N$u9Jief!jbiijDtjW?$&y zNCa|3rLkx!2@=uJyD^e!O3L6eU*-?^uQm~J=th=lmM#q-BChbGH@CwF47CIKRMB{N zh2mKD8SDORu~gL(9yDhCmZKksNM7oCtHmDv4M00~CX|&9cB&YBb(a0roPxM zdr4oM&KK$WVnIxv=+&8OyCG#!p0Yx9v7M<~*_o!tW1X3{shR4DM?C=+%VG=E?6?JL zj!MeerQ)ASs@^UY|IEWZ?Nag2%uJ!#j4+DhSwV9knE{)^Gevh=*^CWFytM#UlNDZ; z#+#Q>`Ikrv&3~2_++E-U*jaG}y<9jV8IgGLrhlEYqhrBdyS!oI0>*yI`YW^(I`e(G z1NwT*yzEAC4AA=zGEa$EWTBn0Fh{uxvn4{94LrhZ(-CGfjxd{C#2L<}m~6$s_~u0X z3;m6b@#@()VZMK9fy*i>#9LPAX$Y7Qn!|&j2EOj$-*xjzECs!u;JXU5kk6E2oT zAiT7ETyt|ZF|R8Bo{2WfTT)oFLa4*kSm+gEVzt;smI4EPK#(yu12u5 zJmRZpxS4*@%D{bR9~)JF;?>z=^+J0kFd#-1St_{TE){HW&p#PtFyljl45m*AGU$#6 z8O)G(ixV3#UCo7LER&6k@+>1b8lRCa6Yv~7;4B){`%libj5(p*VpFUK-e*EJQ%sG_ zFjW;zB;(PW!PzE{4utOEVq@dQP9)Z`5HyX=sh(@m&SY1OMOpX3Q$w4> zt{P`1yK4Bd?u&bB6lJm9iybZ+eE7zm8gpu?9VZNcez@Oh32nFHMEO0Nv-c102~D-L z(b;4ZX`qw10UAV#M)v=SA5e8{TRB^#3YO=_68K?cMkh8O;|P;x>+283am6J3U|I#p zr`bkqz1V0c@P=3wDD1)JRbjTJ3bXN4xKD;~oa>)R#w{klvH537{J^!}nH+z@DEmut z|F_-vPxar!{m)A3e|Eh8`g*bK<58^7b4b?yA&E%Rq z_HG_~H;>zbJ-U>sAf69g{;--+HS@; z14^{;=%z@N1$#HJ1jfhcU4qW81ENrwXb>9OmSLWE*Zt2_eztCcde=u3shIxd)4#$A z>RmsT1dT5Yv3j?d)w|75@A~5E-TYIicMDDR?xioWdbbR1(?6e5?-sIpH-D;n_Y+Z8 z@0OYB-F#EMTbxku7AMrZ`KEgJ`9GNI-7@N6ow0Q*t9Q$kde_e^E){^CwixSHiFU%- zNqn%SM)ALQq;Nc~e+LI1HwwwancQk(n%FYL=z6{7YUYrNJ*!~HW_*1+>*LA^HmlN% zE4Q*fu8iZEBEwk>($D9lbJwm|e#rpDfV%i@Xe2bFui0~^GK0x}_NcxjQWjtEzn;b*5gSiOZyka$)*i{xw$ zG;wgJwvQK8D@07@c%x-@S*0L#u#s#^A;Q;+_`qg8lScaNNhGmVe}faNQ=NDPnE0mo zOU1;O>xdBQ*2Rws@8%efEtzleb1E|)Ng9nLa6gSJu)!SM-%b!9OI*Z|0NTM+|CqHZ zu$G=0Q9SuZ4jf{G&YD1v`Nja1SMj+K-#$XOfoy1=IJyFg`cLpw6;=dR(@QexyP_9B z=$Sy#+WUs++O{-uTX{MH%>mn64garGE#}N)(Hh?xO3eIBqhesXnfJbE&ip3p)R|{F zk=7(Db~B~9GHkL`BSo4k_HIW7v$^pXOfkBV*V2U6XusD)nmrpr;{n?D3#Z`uj`95n z{IRq)Hx}ut26womKK$D`7st(NlQ{tm4Thw7PVJQTl>7Pur5!6$^Ug@pHx( zD}GNrMO4%s^4bPlsP+JoCm6- zEegW75b6Ae=BY$}RQVg_oD!?^;d$aU^(l4#lX;w@WQURAqt?z2mt<%;Foh4gDUhze^p_0+g_u9*w4i597W zhp#N6&PMQ#Rzng67h>Kn&#%yy)K?;^SA$!78Vb{h?dFKB%ZY9Fg?pvFINqHtA62NxMW-_u`Jvgu5m?fw1rEulI>NNxFuww zcDp23Tdif>68b^$3}@HV5TZhdsg&a@Mt{mC-VH1D5D;DTr)=Zhkg10ZzGaL4h77$V zQ!iPmmt;)Iq}{Mm!-JK|;bCg{MM<8`F&NG8i!~W&X>!i#?Hu&FL7s6i?1mThLAi+H z$w9p&s+YWo>iSlWEl=HsTYkyPL=*^@%8uRe482z~Fm}COlkrqiYV4AXj}{_w`aYU@ zu^U!0IHH^syWtu944|Le^b+dM!AV)LF{Wpb1DnSsS+E<-B-jlM*24_eYjIV>JKp{g@ttoT`=3t{B%>oYX^{dcNiv z{lZE;#9q`ZZ2IDpdb&PS&nVEAK-Z<$*|n0a)E&k`YJiIT#>a6%%caI0DyesbN?acJ zGmkjDw@50!)z03ZkPtYQsx{DuRyA1yff^#d*Y29n9!-R=0;z1jpC7r}>WzSY(9&!p z#8wfBfTkF}T2OF4&38-IRYYpm(%;?++NBT$0>r&(Rc0#TFn=ZsnO9%KJS-tZ5z~u1 zG9$JQy`e5rvqAT{EAkZ>L5oyTMEm*}RSy)>o zLt_wZH+qGBZlSj`LJJ)M8It5mZ#X;#FswWP)^?&ZOEqYj5feJ zceDXcETaw8)R)nQJcP3@**4nX(S0Y`*JNCxcIv+46MRjUl+&MOz1t-qp$%ZoQ-(%t1*fID?WNVhaE!tn20+2 zaEf*8Yub`VdWXqDhn|iJE?HYiRn!ERtfPv?cZ!(Jt~o}SUeCBS;GqTExe=^48S6fUn^=njoX5}DZtyT}t zo*Is{9_TvY?Df*iUS=Ykkruo+!^0gW}EeouphQ`#bv^caKH2({U zr!CEp4b)J-aLF-RP_!B%<#V>ts?r-;X{liompZUku9{RSjBoDA z;cG@P_vCSfPW_&IE@XJzQ^?nhc<%9Wg&tx``CO35Z5m6FGRR7y4jsSx=GjcH{H z=^XTjHmZgG(8hDoA6l=+=npMeH~pa%TSk9q$>z`>TC;vw zBh&DTj$wFKEc~J)dV^grI*xr>WWx?9GO*~2PGAER*|GF#>P5h{Fx7YSE6_I4jVilN$AT(TUKILr!E$bsy|2k@vy!PT^!c;(yY<$ zCDwSmbaLCF-G{b<^R-a_^xcPeQ>TOW9&>iCCnyjSbaob{sEa$nMX{8m+gt)N`nI{r zi(M}Ct=N@`=`40JIxcp(c_c0mX1drVtHPCzaTU9Sq_+Z=CH@Gewk+IG&l_20$j2Kw zpXbcZb6LkbI*aBK^K$t~qAU*m@)J6^Uwx1{WSXJL%+a+crelt-JsD??Z02@8ujLoS z8#M$+qp3RWpX3mJfm4i}WbobHcpi?Y0h*Cz}b`h=h43}Pd zoTw%$wgPL+FfWOyj+@t1>4bUhR^$`c>3Zog^Ey*6J!xKh5ba!DXUUgR_iH7q;QHLK zN~eo!93YwMIs?oyE|1=kGGX0h|KHb5M5sd2>fzn|k=6}$5lhF%TN*yx({AbV_?Dis zYK~1^HMEf*pcxp|3IAsh%j{v6dLCizE~acW1zk+RXac&J021`On9vdEyO^L6+`AYw z1oSQ@eFX6?CUJFYO{KoTQH?O&HNi;6ECtkzb1~g7sJXc&G2PcBP4_iP(|t|SbYGJ+ z-Pa^d_cck=Jz-7z|MxpRX-)6Kn%*;cP4B{*=2D3Y280H&prlTn2`|Y)DZcHHo@B^J zv#|~v@(Bpkc8Q4_OHABYV&XVV~x{i?-zPe*1IsSk3g!5PF|MefK|EsE}o^s$ zxSfi=swVe!oX>}mo%@YQug5{3(ARlvbJ%XglEQZuxyzivcPAZu*OSV3C*;m1HIL)m zd86kTLIeDH#e94@FI^6qaQxrt$xr7nAMQC4MdZ;9^AQpdU-eTlS~0%0llEq(=ZJ%k zC(GIJ(p^C%;|_k8KgHoTsqC0T-MG{tPq{ku@v{L|y{FpvNfRyQPER&{S+^8C=qbd- z8wZc-atIZ$skiN;)t<6wz|MvX)JV9Q8vu1ciob$4uL~}P$`FHXNu7N=X2OYkY$n^s z=Dse5GNVsVG?W38#!@itBQh&&FIbBB2^M2r#~k)6rohu@@!5!B)_A95HxFBat=+}wk{#CZIv@JMH>3S4EfkjXcGyok*v^R#`uekQ@M)surBp=UT)^{cMYC@^F`{(U*k z$$k(i+t`dfCuxMnp`!V)*e*-w^{_A%2Qg6RUiKuf&QPl-gI15T3p!rm6vNUYw(qQj(W7`00YypTSOGRk6J+UlKGna*>$Vnf6YsjvKFiWk2x9OV? znCoD-59`2|ybciEd1dIeca4XAIM1eK%28*~?K{8KnO3W1a_POFm|9NqJ0=*vwnv^z5a z@VO?yZ@WwYo~g#@Q~-YkD8dBzHJ1V4uE`tkUfy_rHN2NG%sZJL=|YzL^fyDG1Aqx^ zD+stAse=qCbO#f04s44|C4?1Mt(a?JqjpT;0F@matkhv_u#qD*MPaA9QDQC8UOEVa z_>CNd$(UbHZ>Aljt*-YY0`k#E#n~;R>vd=66L3dBw+#E^31pi*x2N;maxPSOa3Hjk zm96A`Jh7Fuyn!T&mC3e`_DUI%h%FQLN`3+-tof4Q1RDR2>Gnzv;{vrM1KY07afNpK zD%1>~!Bg|%mt+R3J?_eyj@>|Wi3yx%PT-e5hY9RFpvHmz-FQ~VYd3?ja=PG_BQhEzD^glikD-HFXKE zT%j8T#l!M|^BZ9n4Y@xo)6bhUEs9CY@KhR=7zwNj_2IGycHvZ*xP*?T#0#Vg=Nl}^P;hJ+b(yEP;3CP4H!_35d04cKuo1Ez_Y zrycbN;3EXnbN9`BAV2?4Y+2#12)L*qV3*Q?|DKODzi#4*=)Km1b1YPzdjKRQT+dcC znI=hrWh%&uX^hk4^Y_$y%8cKBW+T-!JGd& zb^$#)chjep{+plAPI^x3j}~8!x%hI7S%tj#vgszM#U=$733{^BGfzcj-~^i+gk?_| z_Z{IuX3;WX=N=HC2Jd_eo7)De{WS1nVxRCmshLp`X#Fyk8dCaiM`R_v93-q0Q4T+B z6;uW0i$kZ~8I}C%WzZ@AM%|Rx;Am!_wg?Pab!@-(CAQY+7=fza`0~{$Yy~F)v5V(3BK`AzVE>}3 zf4;?2GXXs>HlCUO8S582loG<>VJFb zwE1Z7-h%#5-*a&s>fuZW=;jxv@1{A53r;bL**`heD5U>C_(=U9D4DkZ`fp>gQ}v$n z_t4nP{@+ydf&E`T{o~EMPuYKXQW$UVnwSrkI+fR&Y>``f7u&;1cC2{vqQm!1a^?|F z%^906K2`FfQzb)jcHIt`siY~+&JalZM@lAhy7(0YG6UtrZsq0>BRAg^t+-z9E_4AAjKd9x?a-QUK zf&D8~{TnEnrhi&e|9(>Of&FWr{_*BterW&Bd*%bzuj*gsbp3lesedv52lnsq^p7`R zasR3NXN*z%Nf7}QJ^R4^n)GayZ_0jKMEfngS?F0|YkXs|3qZJEHr9)3V}166M{~w0 zMsxPhK4@cUgA;ey=FLkBk{uva8rH=&El=`>z14C?V4&SJZ5_P~rmc2OoF4ChY~-&M z8~=DdvGy&U(9E=443rJesijD|mILz#e^&d?foPwN^x>W$X@LOo{PK9>yU>$he=Nft z+{Vopw%^C~wGQ=u$KZq)C#O&2d}gI~|41o#GK;a~7mVviN;wf6^^u-8S=8;w40}h8 z%eZKAo#g*UinyV8AAnFAD@4+pY?WaWJdCwpSpZ-)*^rk6 z)xO2mz@;KM#kbec-FkM8rMv6sPVYV}&!7Ok;|3HQNfdN&fsrU!#RcPuf)!kFEKzV3 z7aUI%e31)IBnq;*;AEoUJT8E4O%=@I0$ZZsofhhMwUnuUQ@&5xR3ez8;f@ zLA}9Mcc`lEEqK_sN7elu-A2~usH*e6hpP3eY7IThJy6h(dx)IQzDGFPQcUY(D?j(_ z&{i8>Ywg5qT;@pCZg;0iYiG8^i*2d7uNORk2W<=J{hmw&5_kU+wJz&Ss)gv4T&?v$ z{3F^ed1=z2Z9z<(d}lT__)9#1?zgQ{TaRAm4s<==cA*ybq514zs=Bd)i-L1t$2e&7 zZf*S=kykYKJv2t|YU`!JdH8m`#ny+C2INr#Dh^H>kcS7fR_n>33+LnRi%_dAIC(&* z*7|bNfEs7Yfd2m7_<&H{tcnjN7ds!nC7{YZ!KCtzt;MQa4S=&sJu_tU^89gB;@+zF zW3dLUJ4;PyC%2oerv8TSs@!Mx$s5>=2VcyN0lCi?)Hoy-ncLY}KUZahFW@ER?7Wf+ z^4IU->&xb{ofgkUn4kCX6Iwm#Bq_Vv{@n!3TCL0;dMBlAR-}CP`#9dv^w4HZyru06 z<`mzqwevc;8|9i^yTXPy9JJqEk#Z;Aav=!wn6>4PNLxzcu$RBw`XlWoyXHhtVn?LA z3xns3leE9uH$!)2iP)|Urlbx?oq9jrq6Q-LMYZmOiF(1A5nE;Iz}QUAzh?8%3k7~{ zaec&HSsB|O+JdiHv=ayeiR#Y2UI*J>^8j=MN=5bov)vxrj5)VxIT6S0t=ZZR2I0aE z6n<(y{d6HbepcHV2k7_U=duVT>c*j0`Q;Kj`A0E3>nKk)5+7|B6F3*bCdbBG& zp+Rf-N{@1IK?psU?x+7dbbp5KPuKl!-JhlVsY0gi&(r-mx<7l^=d}#`E~S43^e^9{ zU18UYY@=}Ev(j&lUSvm0qj1@~DuaN@FeuGCSnkPUV6Zl=G#eq^36clJ^S?Ad{(PLD z=g4!9`MfaxoYi)vu4p_=JCxTroWn9HMP&X>QgDn5(#=$4ol-HKF1SJ+P6PQ#ot+2a zUBl1Fr;ii7eQ~It8shuo5cf?Ban@@W*`inF!~xEIO9FZ{4)p!94qFL$FUBF> zE&z_kAsb4)Q%xu+3cLJK|8^lMHpo1gN_}5!)Gk2X52$NbW@}YBAtNEnGzRmvgP7R}1J5X>7h%ji>&F76C z7=0P`ibq3KL~kTx(ZrkXTeUlc&ATP^eOn5B%RzK9BZcYYL_|DyQX`8AoaRzp%uDrZ zUaD9Bi;%IbQ(t18W=>wGnY_+6^E%5E)R3Kw8szyilh>)wT&MDUKo;F*UWG1oCZ>yW zS=Q-cDHw>a(<4%F&|Ig7Xzg z{nw!6Q_#T0Ni=W@*#DI%IeDp15BPN-0{9L5+#_2dCzlDs&FL_8S{yKlwuXdDMSd%NfzB>E*h ztV8x2jncyL_ORduoPEsMd&A#jvEj?lw+=5p->Ox4v^n+H_ab(IZ*`?sl~oz3a#U(B z#P(~o6Cc&mqgs0O5rbabcHM`kDZC^V zB~t7Y>%lel@?HXwPym#wOmxQ*)yj#Qk&P|_8_RqbY+Y8=eix6=fu!b-3cA!%0pP(+_&$nvY z8dS=C%R7wTo+iE3jqnpF4nM()iYM~Z_{_y;*M0~c&QgCw!(YyIjzzb~wUi(+lf);4 zB*5WyXuzZUj)_+j2O&HzK?zU58EM%G(;4X)y!q(avg0kUY1pmBZ1B_?z6~O4H1kRL z)T+=9p6E)OitCUSU1?WFdFhfcn`Ku7Slp@{d3JD=B}5!-MgnCGe+MMoqqhIx&cVN& z-SJ0u&fPWmYQll|RdyhDh;weadL8b$yT#L4AAnmeeFkpHd+6>6TnyBQ+)`VGP06OK zy|l+9m{vv{z>DA?$-8C8G&?O+>0lU zKw1$m<8kV)e3m~jwe3UuxNUu}v`%Q<{o!Aj;9O1Dgs|Pw*W-|BR*;ao#-TU$ZPC3P z4f9p@yf$ko*hHz-;WevfDk-PnQD~NeS;mojF1FnLWFr;7{Z7FTQP?(5>RILVEc`=g zowPMjS!sSpQu@(q6#n{%tv>sT^ z_hsrOQIevz(Kv8AqV>bKL=OI`I4uXdm1>U-6<&Dubc^qAGN+iS1 zS=yp3Xu0i{_&qEYK4xJ{h5dR6T3)T$6C}8-)|Obb8k<%EV_i2P_H^EWXPHY-tAxpG za|E#g2mFV2VFdcliYCrkP5r_);+U^diLX*lV=1v|GhnBntz|xBpvl7T!Ox%qK?doy z0huh`bF3)JWg#NkOOri7QkWKRYn>hvVs5i9@$k=?%tG-Dj)S8^*PFbf2A_@%4sd z+7qBvddWCKg$(E=27Zp6A~0)%1C5n9{)s395!-KI`4+Chd$Pr*!4$hC=z!GW(Ki@o z^Z{(hhGWK0He77c4u*!DMEG7imOJ2mv+Z2%E}_bdyl;it*1Lu^kAEt-jY;s56V5*9 z;F#2{)z*m8vq!XmF=y9bs5rE_ne92dMwx`ED&f?`lGE6<7p(WtT8J6721-M zhW~`5;d#IHfL3!{yXmC1Ksq+&V*{>i@qMef>2jXNX@rbl2F|dIbi?V0!)f(n4#RSi(!UZ5{|$XT&Y865^z}Gi!dt2Odf@+X zKbugkALr|Xe0_|6kMr*d{yoXRz-@XzP@b{o%=wmhpRiTY$HlgubQO4|KbXq-KX8Nm zAFJ@Hdeoe_hOxdR(OTb*()F$U33y;j0HpSwew-}z^s{2WKZ}2X-A7pA55ttnNstJ> zT~iKg8NSGG66pgmKqeP3CB}uX5l-%A-sb=(WpZ=+CcCDR{V@B}&+Jb>vp?N9e36=* zzUk1ds%8$?@$hdJ*JSQ*2+2fH@i%5g<^cPd1MCL}SY_sQ3NLqzl-hVf_1wyF>s)wY zxntr8rym~;FLxkvl^a3ysFieTC0uF`Uqwy2d{p5{UK#LS=tExJF!wIp&E4omH^P0{ ze9hzN=5cgqad{zMGbFkh65W7=0FvO~N;RJJoWWW3rj4;jkf&v>A(7cT1mwkzwllP| ztNhv`du;!uLoI3H6}GYBg5|chg52k8kbkSxu3c@{vR$cLQ+I_g(gL>d*KJzB9{##r z3pm1GcW6FWXvi8aazV6NZfo7wpj~APUv1O&MHagjKKB%ctL0h4Ia<(aBc`AJ*u$&r z^v4li<)A+#3AiNPPs>Z%2%(0_@&8rOd}k-lB%18T*MCT>vYXd!Kc9+p^Lp;*Ly_(v zU$gOY_iDc81Cef7%NcyltGl01I%qHAs7qgu5Gs0qA)AiF!$a@q$SEpIPB~vomK;A{ zOO~8!zLqRGb$l&Za_ae7vgEAfYsr$+$k&o32d)rWHv+(hPQ-@0|6m2n#Bu#w&WAu| zkE{pNSsB|O=32wu*ml^dkujN%!p6o$}Tx(lVHQ`&fxi>I`)k&2s; zt0dNBjUbms(+ur7W5!mCWP+VO3#{t^{fp;)m4N$mwqX*~9Wm6aj5j+jj>qGNcr!|) zH8So<6bk=xn5Bmq{5KsP=WN&Lsu|5)L?Fbjog6YLDa!mQ`I%`V5>=~CosyNIyVrcq+LH#2NnpOJ|MTIkQ{Y98Ia%lbr~sbUMN*aX=+GdK&xgs4ftwj`tpB zDF6HjrirQ~p#Y(pJWRb(XI&CUUma}8v6Yf*l0z(E7UYmi-%by9Q@dj%!Cf!^>6yl<4GO>l}0~o)aiqDR&lT(t*?R2$C4Qy1Mc5S6jZs#}^I}i49 zkqW*HT@(xRvN%nht@K8ERqR=D?EjrXJ7?1ywBpSz$mYB$BWKlla#o!$XVsX!W@n$(#>1-9 zDjhVkEb+uBA5|y!v@_{)rCiq2Wu095=`vp~1EX;Vj+JzoBbU{5nJJgebm_Ck&&BA@ zZ;c;{(OsD}e(cK`$VEdw8rPQtZljo_MK(P}Ut}jhqi=+JI=R0bz@*a?kGw6%AykrY z)~ZX?awTdFNGCA6HDP+^%AD$O%qI0#L$I$rEF zAE=kO5CL|!sV?zxEOqIiQv8cLC`SWsShokXcF?$RR8C+H?It>qPy2J%p`oOecF+vO zPgo1dUFkifNg4d>`TDldpKXW!ID14AaL~h?=-A+2Q&3x@e;t3OCNeu-qZM`A>~{zM znlbyt$67sZcIt3@_c?pJ5f%JE0qrPf*J-r1M_&%Oh`EC%$~dco`Bv{x@Z5;sHDY1T zvd$Lv+eds3X0=X?rQ|}ToTqF(K+*GV?-&MeS+HCs>>9dx7ICuS3&P7kLc*^9Xiv(K zb-^hTc3ovFnCtAi!-9__rR!R5Pu-1dUGFa3)wT2H46W3GyyAZ2b2m%o&;e>Vj8BhU z)09w6SQxzZ=eSURK4r8z>{QU`SIlD13olRnob zBHvl`T}{j%!i8%?GpyQo?cVP>=(V$n@olm$mC%^M)bRIgxo;JG*WPwz?(-@i{pAkg ze-V%%GGX}NE% z{6t%nCC04#LoDZ8g7f3~oH!o8)(=}tS|v?X$6s4z1X5sx>!#s7Wvaq+56Gy}WfaLu z@BuO5I#t#oCX13F)+N+4y7MFPRLVGW0%sLZO2piiaEh$vO0XcJ+I3SG6w47;{R!KV z?d-#$_1Fxx{*@`@hR~Brf2E<6mOm|DS9AfBZJ#arHJcnM*rQ*w^Qi(G%Bt^rm>DkS zu+J_A#E?R$JCdn3gC#-3MFszZ3$%46Cy-r<(+#%ftwko2SN=N|L%27FgtHeRCpBRa zDr(@C@~tt+gx;|Yn}qdE1ZIcQOBXZHZLm&=_=`gW`E+GR()*)N{4ExnWDl910*69E z%Xi~jOKV|Z9OiHX>rW$ibTE_(H)!9pYoEz|zU!@UGi{%0r0Ocf^Q#>Z+Z}pwhmFH* zA--ct29iK6vg##yVP&zk_3r4Nx8En;>Nyq|b|(yGO0xMRw)Xu9F&zX ztzp_&;s!?+6;8rrb4${3z8n>X!en+;Oh#T=hzhm)(02a2hyLzS)>g7%wi4r;u5VLE z&4cvUm9V$kMYm-tNK`p>F>YlgCRPWFRSa@5vpOiec)L`HN=Z(Vp%ry>rDB2s!$U{S z6K^@09EC2<`8-x2pnUPqt}V9ub@Vw=pqxxIrb zdM+_{{Iu0*9Q`C>gX)U4^uaoUl@ZMFvi$FZJw&rrINXh%ReY66S3%(_SLy3KDiKSN zF1N|$YP#IPlS&K9M>5&aDC}5wGJeB#z6wufZt}x0$-)bJ`I1@c-F(UX_D;T3hKVR4 z31o8;zq*b?2Pg|gxbQzROAFX{1ekZ*yzp4?K)N?~8tuTptmpGi! zv@#-C1wbjC&qGQP_U$g{__Vyc`QX{D-3G;9!DoZB5;pDLtExwCckOaFAa|?fZin3U$lV^fLwaz;P~5H; zow!@KzWpvEi8|U`dl_MfFML8;KneF4EIKDiEp@1haj}90T1c#AvSNC9JS=*oa~*<7 z#I|;#!QesVMXwCPgp)fcLXBw!CU%4eMVv8Bzv9U7RQ*0UZNDdX`tXN!dZOJ}qPyc# zcz8m0l_KTHgzg@glg^roQIP8*XBYOy=m90b zDHAwagUYU4iGUB7A|SRlQ7*{x*vZ7UY{gitad}=EKcpIOl~%?@K+Pxg5+G2ZZMs|u z5=eh@xO<{Gk=OqquXD=qgXjBbqcLyc)RcYuNhR`m7(DibhF`F7mjcn<+_Al?WBX3gvD+rU|3i}$X0dq!5a=%H!=b_W+rQ(tgWpMd z^9NtceZAmeU5P+37m$s7tXK*XF_z^+WL3GeH*t@n>}~o zP}6(2YT4uLZF);sKX~4G*I=ZE z?k7Rtz3O5`64+2_%CvP@aPZ9Iy3rh$Ce=P!VS8Kb_v{0(y%Y9wrhpKvBM~H8&+Fh6@nr;8E&1EfkNEYKd7A9md-s}!@Y4{-?(L?C) z*bQDj$qIhc+pVUlTMeXp`hm>CK)Mst#ErFGH0Dr4nJ`~{Nki(MFkgK(r6)*5!tcXmBt z;hD|Re1Wb3XYT_R)BM31;W*wCRI4N)Lac-hNK=f3LZ6*AQ=9G=*)SVsH6FI7si$I+ z;3fJYmsr^RWe!9&eW2pdE_in8J1e#4n2ActRD^7aekF99Ir-9cl_LwEHC}on7F)g| zw9PX$zienceJKr)5RFG03>h<=og8Vvum;c7pxPsT&R+?axz{SKmKDoG+tNvl)PkPu zrc4qrx6EB&!39Fr7>^`wGK~8ZH<`v=i5rJ;YvRUb+(b7@5Y6I*-^f>*0nffqP@h+9 zvQGYDdGZ%8Oa9^|$zS|r@)y&Szc?E=7Ud9@jc>Z}%}jjr+1HZ4Y0AHdZ}5Ts*85YH z|9|Rq@gff;`Tw8s!T$f#Ki>THKc3p7I-U>o+8LjAB|7JpByw|_Rn9Iq^2p7ptI2nT zIl20M4=gQk`6~i`+82>6@DptE)f%xsf0FhGhc`N1TjU6(p_VnYjo#iLFK3+)TbZ_c z@*;iUa%g>wbi+r^|1+y=xI0Q$*5U4#(Ey`*1TU=AQ9vlD0HSNk@_|c`P*{o{*jB9qPnv1#S4psB0 z4>k9wnhWu)9||~tVU+HwXj%WPiglE3y#1t4d|JB(wCK&B5{JsJoxwR3Ml5hKN52EU z-x=ly;XbDMeK4#U`+>I`e}mr@M_BD4uEAx%uk0o~1-}k`9r!hn8pp4wHVMCSI|{$D zv6sTH-wcR?88InY36*=GYoP7Q(B7D-KMMqjF)>zr-Hbk&$JxNL@hEtjw!?U4G!{dO zR`m_r-R#XvTv9aL?8$B&$bH?rov!knedpWQepp+VK+?=6Zu9WYazh)}mFL#7f!jTu{ga9GDMO)|a60NN#CL$^#3Nrs%YwvUBkpb;(zux=*|JQpZbI#e1wfA0o?X}ik z56RoWt~bI1)^(<%CHbPXU^=^28aibwVAxQX&sRE#rB&!EbRX8|la%fs#C77`U3arO zNj7MY#;InLWR}T+m&oMXxfAa_oeIs zvI9pmMQNEfNy6J4)vMA2I9-dXjC^tk3^d`x2g_t`U+-^&v&4+$zIl!2ZcS4;xveKJ z)l}Z{gVRi9W!zL&onb1AZ~jN7^3+Xdo=SvK*HX+A>calLI_8|8B`nX8qn4Jc6IG-- z!K+hYyj0kuzYK?h8Mp7HR}YyAN2;m#-c89<8K-=e!3;XC7gC2x#VA)#QzDO`N?b{hD-$0NGt;`Y>_b#VV*R~lA z^J?Jfhwy~_O;Kbyt5?`QFQ zwetM;FBN8Y;xuq@*>9LYs%1u8E_k=pNcVU2-QUme8|U#JbN4ZN?cB6zG%pN-5TcoL zlmA0lJqL$a2oMD8#P6NP#Bp;cnd?q_9P!4~LHAKh(6-ds15y;d4L(js;Nx@zHanNl z5%@iEv$#YYeoImxew)9C!XWJelA)-nmWgrxFRHv7cLZ{K=f5#3%!r9b2M71%;XJrME$uuxQg@mqRFAssPl8qxUk+u=Lnim)h30E2vTsgU^4#R ztgq$dKzaC^V|32-qQ(T361A}r!9qifD|s*3dJ^x4QKUiMivb*10P3{3-#ch86e5jkTaJPwTAf8GM|y3jX5&hP}Z8dgly z+lcVr_XH{l)nvVMWi}KAD7AZdaw|Zq$fB z+QYsK*au`}ofq?OD8qC3>Mx_wRzG~JH?^Z?wK)xT#M&RAl^z)^FF})JakFh&HU_oN zWP6{xM}C|vm$@bW<8c=Hk0)8^+vzg>5wc-^gshq$$3~|;LTZ?<^}3kf$O<-GF02c5 z*YVzYzlA=7tQV~oO7fBgabJex8{T=LL0;lb{A3U?78(=AanEpkLqN!dW_gJ}@e_=I zpe(e=OIi~@S>(s@4My+b&@Z1JrTH^~fWOOoAsa8;jfO{e51fMu*M;-ZuIQfr9!l=2 z%()3h3gz%tk7uW4_mMVvmr8`9hvMiV9KVK+u)o3Ch8-@dVGnRxbDy9!Ui8)lz8C%x zjF+Cd+!e@sZGbbJM@L(HLF+mmV{II#mB>s|9e0J&ygwS%(rx_r`m6d!`uk#Ee1F_t z0l2!pp3kYmuyqebIiwhewL@B^AMkEUCuclp!N4EkdkL4W+i=+AK!`TqL0 zhys+2X5Bz`Do7`|jRynDSN{6ZBus`}b_j|sK^2HVOjG+YWD`^m+uUPkumd;3*XttB|2d$#x zO{|Toi36HgcOTq=KDN{vnwP_~yiL5Z5EN+g!wst)!jfUD=o_s`N+aq8T9&+#t8wMc z^w&s6)=2Cvks|Uwr1m1*gc#e(k2`!&**jn6FGtGqQgj6DUtum_jZ4e!CekRO`j>#Z7LTjRh&5YfTiD9%)i=pGHV^JQ+Nt;PR2 zEDl{XGR-Ed9FW=Y`cj^8Ja^BuN=cr59P{(eJ}D2AC&+~-VJ-@fqZte!jvoHz$L{=K zXtSl32D}`917KRR&c9skh>wBv8wKS{)4RCj#GcuatLis;u0 zYI~dVl-5{o-?Td<9!V^RHXPmajn`yA~}v(>Fx^%B^l?E3!H7U_c6&A@a6d zXK-FG`DK2y$;S%-qu21y-^;_L+ zI`Fa3lnqlHV4wpsRHVa`q2-5zt>8RwbdOB2{!unZ{XQ~ywh9;~+bPi5g|{)8=v2U5 zUctiI1taD{HcVWf)M?$kO5KbGM(8I<>T2m4!5-b?#+zN{T1c#KnZpSD5_%Us`5_$8 zcz)m!K)H1IQKI9g3Z5}M{wL5|w%lPDO+x6rhBy`f5O+`1R}2g}+1dtCn^4XAbHnIc zU5im1(T|T#Gt?E8QRIFH9%Aej2g=pX<8Z#AXzrWn#w-jO97N>tF;o!&wxWX8uOBm@ zu!l)UGu>Dj`Z$c zU6;i^?*6+p<>OG^??a_}XPG0r#R|LGPG-(%RGUSH&e>EAZIv=Z##YQuEGWFb9wcl{)k4wTS(wHR8B*8h>BFof^OzV{fqzeSBGm zs?0|;BC)e;5u*GG)2J~uk;nwE>%EF&|n>*$M%Kr+kNE3G3nL9gCsjVBM(Hh0Vi{VYc&q>tooClNe>^GpKda3m=T}ZjC70?|) zg#km*nV6hrU%!DG2!(JegU^#H<9QNPo^MQ7U6mU^QVE3&;#sF$BZFYHLv?{wes`&! zRO$&zkHq-A=;~Sp$XMH{2LJsv6?q-!o^om!<- zVue_x3BJ0Oa5->XtMTd!hT*B=&4<*oZi!9DvTiE-P+&#Jf_BVR6_B-{jyTHD62(>B(C^+9H{UKe^6Z!D`lTi}D|75GQ_kGOUZ zt8K~(wwj4N&cNscdxiOw54%Y_ASbmbQH#%Ei%(;Vm8n`pgR9X{RT+v#M$e;sI513T zPjQ(ZqK8^ZBf;;PH^aQ9NWLG{>fpt*U{m9#i~#e1QGNSA<1zC6IU5(AuiMR%=?N9^oH45d~+rL zi@5xdFTXP~KCFW$43p$}v|SCD{>(di9A0i%kS)q*@yK`4Q3{nsNA;Az0wC(;5!BZn zs!fQ;zV_+5glB~LI8=(hqxq*(3>hUbpw~G!`|N@Bc$DdU$^<5YPI*eJ(O^38#}9XjuYfMg%tfEwb)`|dEiD?IzP75vU2TtRaU zue<83G#x_mBvoA~s=k0#M^V*s&vvbXtUW zMJ+Y+CX{i|gve^7+z1%P9zmmKcYGlJ*!=MD)3nYw%U_H~QwgzX$}?8HDpZ8|#li8o z9)rSAV>do^iE#4D27Fp+5zF!1_>{{*>v2@9M`Jk~^{ENFRX+`GE zO1UCy57K0i=9Di>kYHA##xzogla@}8oEx8_tKNnwGP5mgI_lp`-A=OVYVeKYpRMZW z`|uo#9_mRP4=;4Ff=FZPSPg9_F8;ciDVJ;vhZ5mXt}I99QLGmNNlOBAW2tc~k5%Dz zH1yItx+ChuSx%8zW}HY3gJTSH3K`wq(4#))_K6p9@E>c#3j@8W{TDiP)2l}}JyZL` zIy8TI49}Rri8Rx4lpSi=?*|D_))7Y$E%1%uy6Mf21>P~GZ|FWQ4cNrh%wJ5udvopU zGrwp4?EUU9GTDZ1<%0AL>;as<>d^diPB}N#w_L&6j?f@mh6lpYAUy=u;lDcX1lBWU z!w^#9mr(e%y1Htc#{!<-(KzQJ!IaczUk3AX3R(WT>9E&JTm{(2@}$4LA-W^*Qb zMzPBXu-_4f^D2&@z&Bbx%r{{^|>^sUpVQp>sQ5o|J)t#@APKZ~L@s_hLR8|>`%UW7Zb^!m7MVeU{7?#X?K zaF&jJrscJ0tFwfb_c$zXDLjTsPdQ~%R{+nw`)OhNU}3tWL3#+hN(;kwd9Rp4J=UnJ zLvzW->zCln!VbZ+XLtP^RBq$S00lseHyuf0w|)Mu9#PxDp9bF5HW!qsNv=ykvPbX; z2pKma_D|U&7cF}?Gp zFG1oNxz(7hY7>4wf3)1Csj6$nIJQw ze62h1_LOZQ+cKe>H7#3)cX|Nyfe7lCjt>8y19((Ac@t$*jE8r`javHGUmc45#<_yG z)n7{Gtb}MMSwh=WDzrdlLa*wUx#(XdU(4_LuI1Nv*E)EQc6<*HsSzT%(}Oc(ot6_a z6wyT{wmDby*eH5zqWQopNYrB59C&-@MiquvS(0`MCVE+jKE?Bu3HpCxX|6@}ezs5s z=gZe-i*O*<=0wzRcvksZBRyox*P7`^j(n|!e&mU()O?{o%Mmd;@QyAo!u4omh8!Uj zpf33O^D!3)1mGigk@A=@pnzk5i!dPX;MU0X)`Q*sm}G%HPI!CfjmR~>+Ark?WtVD> zI*8M_25x90#BXD59Dz7e*##yzgi1S0Xxg)tHBr>D;w(30{i74jmJrx94S}#RqN8s4 zT(~$A^Qmwd)H&lbVdpd9fSF+TQyYcWP>Ch5*(zIQU5&e($Q&}WuK@>M4dnUrG9u3p z0qMU7NdG0A#&NO=WW2~7=+>8ePHK!o|DTCbNT7?_`Q87UW126pDt}lXKmyX6nGK|0 zjmd_M$M`6oAvYH|EEIf$WKbKV96s`D6vBDNADJA>g|dP|!pSRn%r}az%RQ+1zoa?X zmqEtxB1RmQB>jY(;UGw2C6{(usYf&YOWe$6&XxoQZ6tDJ0U8+CN62Od&n6R6BOYyw z@V7AhyA6Mf%t<%#GGw1So0qX-nVIk#)d3u25}r9=xbOME5PLXih))#Sjs$3%kiSiA z6yAAlI=CtF1{b0)!hompj)O<(?k0=q0ks+#wsO#05n zqR6Pmd%tOpbnm|!bqTOzJ`nEjS#o!T0_|bfMfZ+Fojr^y$K-^IXx zAx%L0_N6E~r~=v*m!g#)E;JhGygt@Q{t323nV`Iq$u*%nnmqYS&;&*)%ZbSZZjcFH zBK_Qk+R#Eh&#$WGM(>^H9bLhBi2uAaO&1Ix6rL>JOh!&j!&Dw?CbS?fx%y_U{>@Y$ z;cl2S`!xE)#wDbFc;*`^^oNN#+kKaM>UiPB-$G>_{&ig zntL@@bn%@9LQ%8M(^xxA>@l_z!@~{PXR8$z!D+84pZ|HJ&Qrwu{NVDGBG*{xhm9kl zi%qwXrU%!JTMNx=h?|qtQ zRT3c4zXtl3laDx}L-^0h4Hx8MgiIqIr_=ueyzQyuXJULu4lf}A-e2IJQEuF;$MBAu zeIqM5cJ!Un3nfHcBuC3MVGv31)9wkVD4NP5jHpsM4R?&um*dI1Q=6$PNq)F3XL`^ zz=zGVHIw`EJUem+^Bi27#w%eAe8iTqZbp$<)EEMr&l3m!(3!vnYCQ16wf49!x~L1{HaG{cHNGPf ziHz}`!~q_!NR4%6*19r^Ib8X*XNfa>donSw%)5Z@(HUj0TQhU|MoJP)pMbOY3ZD=BOi70Dpiz!FBuPsdnoP`t#ur=Wz^P~?TUd# zaYaULiHl9D%)Ts6QV01iLcV)kJ;rfbTiiG>Vsov5@#vRBz*upH=}iJ-%+)b4P7gx= zLKs+Rh835Q^w@!3OKnU>yX>G|HwW3&A~-k?O-M73Iw#FM9sc_h{5SdBH1itxuL$1j z^qFaynVIRC{C`qr;{P(#^Hx3_7&J$!9CqM1-a5odCQ;DeX3|Jdc9i01Cx6yxC*P&8 zt2hz$`2*{T>p!s9)c$Zh^^}r@0&%-6Y4pMH4^YrJ+9}rNQtB*rHmH}^k$tdV_mb5U zDvddKX{Pq$F$%ghB?UFA^f;onMy)3ZFA@h^3_ywzxUV2>eF-AW?Hnk(_vQq8yKM-) zmEu8)#0Ywl{3C{L95S;Qn(KQ=W+FsNr6f{nT1L4sc@4=Qo{UKpgueM`{gp_nhtU85< zMC&sb;~~+p;$m?~NO6Y*Z)NBqanq?3hlG@HNEl(tjR}=#iS%EMtl9V zhlGIfjD-97FTbkm2fsXh*JnI8zE1a7CKK#?MkjPW?sT1JAmxUMl+=zC%O!F4%cna; zhQNePBpo8ZIC*B6?mPLlVG3UvgX#3}^#2IA{OR;PVjmoe(H%}>PN(m2d>wtyl$gHf ztayFTz?h-h8WL7wIw0$5bwG~O@r4cONoLli(Ccw>x>_R@q;C+#5A16oCDA^vB;qew zG{c@|o(lin4FA0Y|9$tuH1ntM--G9Wy)%`|uP>ibezgku)!dyzesz8rlV3mFeMb3p zck0h;XFE4)+5AK?W14*>q~5a8PeF~}C>5&Ou4qC95OQMzxNq|4p67IS8Z zyv9KuC^^Ujp#|G1*j8{kr3+lja~y}N91copjV`WL4%<>dDO6u=S%&(b&Fv1%2#}`y z%{B==JEx;M!6P{beB)S2soV3+GF9np36ot)wdW~#rPTZijc^R8T;T&-?^*HzUks9ITZiO|6$D^}QgDs-ZkHol&&frE5$j%A!g=)CtjdlsKl zt6UF_3~gcHMrW97t1vJFz8~tyd1=l8(cxG9`=wwj?GSCHWzw?B5c&&0^ z64wR^brOzZ>!j0ZlAfhP1HHs6@KJ21QD7M-u@eP;-Uw)q zDb9i5e1~Q6*lO4y1)jlRj=4fPRF6#>J%@s#PV{zB=P&mll5>^g;;{%ufoDq)!FWd0 z39EIssdat0gs=i;I_Fq1qL)324T$q zqGzEx`X#t>3)H8(Z;PLqAHfsZlt-N(F1}Q3oL-!t2QKFGvz}0XJ)tc{NbaXS@hsmT z&(d$QBH()i`VKxTHp8=gGq6p`+hG=rI(HAcXRnOg4t0DxTyL3jA{Kubzre0Y?+w}v zuo=?3lefbLyLNBJ@gp zoBn2nc$b0ih+dFuX+ipj%J*PR znmt(~l~nD?DyXDtPgVp^Y^U<4;}|wCPN^LjG@`Fy=tbCASd@e)e2L_=hFv#Ze>m<3Y zoqiaG+S8r6yxc}{OY4kUMb_qxTPz(RMOIiJu}s#Aab^K^{OHz`T|rJaB3^lWSVX6SefbJm1mV>@A*}4O^2DDm z^8xPGjbUJAm+b)h&bcfHC|?DpXND!S3x{cnHtjlz=|NqCYI@M4luBVUIh$=H-^SU4 z7)v^`mEv7)r%0Cz_=h7H{1UAb_Jf2?@tm#r`l#=}4(#PXABeEg@ZWvHjWVxUClW8A z4FpfSgk}&t=~9uSNTggs@AcIpGe((6w}csjSBu0MRU*|A>gp-ck}TZsun)Nt+lXwF zJ4<==(Qdg@%VmK#4xMH6BzLN5Eac9n*mt|!iO`_DhW&Emi%1vtLf;)Q?dUbAQ}Lhc z1ZV?rvk$%lc(N%{bhz}YiW&EM12VgGmqJs)CkkD+M)MiL`xS$j5?Eh8BQ7-qmCGXL z!(|oI;j)R@aM{H~)0FTY2nd%XV8C`Y5D@=*+g?fIILZ++)TT?oZ`P(HJhxsw7fl?! z!-cJ*?-Ha7`E+O25%x~#g^?3oClk;hh=XiPf9;5ox~X9b&L`}lXGkos15GVEj`_xT zRxovYA9xP8w}FE9jAK8^L#x}`c(YrQyC1`wM!qEjgQc)-0_z7kf0wQ&zy8(*d=F)b z(04o0D~ivvsr_+|iguW8bX!tU)6q;J=m9Oz3l)(9Ln?5TKz&sXeJK!MO2roxVe<~+ z7neYkGDjKr2gdmitvsS9`yWb)z>&qkZ^95+jKXNgET2OcU^gPUv<_U5c;&_$fg-Ss zH=+w;t$d?_bq$hN{4c=@Ik4_&l$1>mht>A+87*xYu65Me4Wdmef9Lxc7G2V<(Z@Bw%g4j> zOw#k3r03_7p4TQkV|+&#Be8~ba1{>9g-3y2!nE-zb6K@L3j1y=P*9pFmra{Sm*pUOBZ~=g z*$=AohbuOFkS1yN5t<}8(;nhrV2+ewm=UE_EK;ksNUd>;WRzRQBDHFZ)Y=rkh^=CA zTH_FFsfJh9 zM^u-ioan9gErt@ZwSnV9JSxx!o{`ZivN7UUO2r5a7|+94XB0^oK?L9#7(oi)=@;v) zBKKmQO(b5dvri3cILzrs?jGj`?_y_njqgfMK&Gw0f4#He^&Iwl{2g3_1z9Wpc5d*! zAAA6xgAd?yL(jCORNl0L7vPV+kJYO9FYk#r?8^+U;ML-WdDt4}%no(o&t}T`j%;^uJuS#E=B~pKDFYxSV1ZyKy%tO&ITf&qB3LQY z3oCnc+7~@*MJN8Ycl5B!E7(e0;tDiRil^_CF{SM(?R=%!ia}A|RaT)zGpV2eCQ0Aj zU-7+a=y&y-yN--*V$Y4_pAdU)N_=j`>>$xY8cO3_wnpe({CMC5icen+{72~b829~E zBqE{fbb)JhUNbw3iDc%?H5UL2Y?cBa>V1C&rbDnT+W-20bh|#``QEun&q?2Z)R6Fu zP@*r18nlLj6RYx-+m?F_>1fM0kPMnbO%7e`o3#cIpw1XqIIvkC-Qzss-!~_=hS>g5 zU9A27Z50XqV|rKvxq0|A0;fj#$JThZXx!Jl-;i)6x4(!&5n`RPf5ceswJ$ngdgN_k z0VURoYEvwPOQ}v0l1{{GOj95-v&)=FaY+ypy{oZ@Rd~(3Y{-Hwg^Z4JW!%MMg$3G4C$}>>j_t6JwfR5H0u$#IDCNhrx2jH{zl3;mpk#Q%R~{KPKkgPBXJ*52b!OrbB;W`B7YBvJ-}LYX%sCVvpa{Mj z>3b1ZHXAFT5N(KN9saRYP>04?hx|$hIxL3{XUESSwYQGgG_QjuKFE1OK6qPT&=5E8 zI1$F!{?cT5xAOFbv3U&)I#7!a^OD1^K^tPX5?ne_#~#6hNjSh^plwYaR@)ALxD0q0(k{QiySq-@}2y zJj%jb8(N3GgAi1p(wA@RyjQVdaF|wm>jkt=!a8X|KRYk;J#s zlA2-7*3qvmF=2;|XGhd0jGBRprkCEDebu}(>Y8{+BJ&Vu3Dr>oWA4I6f9>-ruVI8* zd{>TUhWzrD7^?8WKC}hUA}jGwc53K58~Gn(hz-w&q3##s&@DwQl{s=D)IylQE73Lt zn7?aSrwvAOkSvSv4~07+ZQN_e$!kt{)x^VX5pY}L;I`nitU7A}+!He5775p@^)K zr|@6J{@rcexX~5!U&dfpqnhD@_Cz2C0R9S1nij{q0j+ zUjQo|dLA3aVB>55JVwWkcSL>X!o#+g$LQRZo}41J$~{la-?rr0+)f4xoJb+!zXNRJ;b7+@hR%W{j>q?1S=lt^B2;d>NAP+G^!nwy`=+DR2Zp z?O)orvUjy&xyWcU6bW2=PMm6IE6t%k# znMUCS^~l}L)(Yk9KTINbzaVnzfzPAf3%cJnwe{&-P~bHzj)ji=o(jDbHIeZ+z1z>< zY5fDgV@%{1KGN976=7mkTYuFJe?R2>?AoRC6ZEhB_g*wIzJ7DmGj4qIj{*DXI(`ooeE(z<^5d1%dPw*$OXar7sRj3QIS(S%YB9~U71^&-TGb=|H zj}WqIs1p;@yHU;lo30VDEbrd4RSj_4X4IqAhy?DjjSyGiuM`pSLG{ z-yQdit6=Den%aM>9YRU6NN#C2E!7h69o0Ag(&l=*KTDS|z?h(RkpA7?!a4CieEQ9~t`Q@JdSQF$u zp(g?HuL?OZCT2Ifbu58YLL!P2BvmMHh7mm;cGuc@keZFzA@FTX$=x+YjI~Ac`hzMzM zxa~!2 zzRo))A#_nwboanWfNcc8b}hnI;`KbSf~p?3>~s)?H%VIUt&7_LR1JPTv#EL}m2f_dOBIm3Tfj#)KJHwtGb{503o z$_HusBDRY00X__xn|Bl194zE*T5)`zPfYaC;x*nKMm200psLD|hkm>krO;2L{Rm%O@F+%?Ioks(v2CBv9~J;LZh7*{W)b3*dC`3#KuJmpMVsO)l4wuqN~ z`*_OPQZljZ3Q;znmwoeEAg@!sbMfu*{4L=qxVx;`LmQT9Lm75Pm8ke^0L4EEF}!jQQ2oi+1;Q1Oy8B|$JSh^@_0F8)^5C=V~-mSs6SG*u?ApC>Eg z4Mz~=@TVyu{WLuuE%G=F8$L}5t&$$M8d+jBWNiF_)Q!KG9v?spHGCc`@1gP2l%wp7 zRR}Nbd{ak({`UEXt+hb4fzhGs_2nKT{8Yj%npNJ}ys2xksb;Bl5iIdl>M6QQsr_khWB#yqx4_h)p3VyKc6>HZMP0Lb7Z>I;a?GdGulsGuB4gLBl2>5hz^ z(g*0g4joN72M<9>d7hmJ3NP4nH0A96hzdH0n(%_oqbVn8Iu*b8HJP}=U<&r6^CNy>>VOehUBiI@K1f zY~-b1{Wv8gf77X@hqc|1gQu0}IbNabZ z&Z;d}HXY@-zga?KsliyDiptvDebZ()h3;-@`EE_3eXy=27i{gu)y{3Gc>CS3pDP>0;O2sKj3X`sEgDT zlYaxUeLrTl2{iL&0~H~+hAC5qkE$OAV^ZnQ~O9(j>00h>`6vtpGyx1p_< zj!UeHg5w~O6rpIwZ*;{^_G5<-il>GB*!c)y=QBbQb|HjaYlQvSRD$<8QS#_sEV)*c z{QF*PGf$LUi=$roiYWQTR`|9$3{QVT`$?9L75=sr$3$w>!-4g?(S(5Q@(pj+DN~@E zfxDXZ%0<{;Cw|KPfElx};ooi5UhSJ^%L*B@TBfsdBa`-Gn-AdEF8s=hCL2YRpY(3n zJIcz(#}J$cwFYoPsRjS~z$SCsm$+j7N7>3Z1k^=Ta*J5GE4SjZcD*Z>ZV5b=%bh&! z04ld?u=L77v6hCdd@b?0zU3U&d7Lk1t+I3T#e6C8lmG8S)3S7m|oN@VK6lbMIuSNLO^4emLkB-mLl&3tfaKY&E2@4druE|4#!IQx^Tz0%!>3f zAT3@m0DMc^Cz2j|9+dYyMZ0XGmySNz-Dg8B&!Z2ZfvH)2$t};_p2i1ME~fYat~9GD ztr0OF?ii7#fqYe0zp9o<2GU}|T2emy&v3hrS?@S#yWL|Cy7rz#kvC^NE6E-{h`Ly1+hk}EMswo>GId~MT=<4P%`Hl zSK~T!wNAsbo)l4?Lxr~7zG*t(ua&pNRtD*qV2DHr?XBZ`_9e2d~WUl zWa4t+ztH|D9)v#Iv^98!_zct5;_2Ztss()q2hs!Ye95}7SVy1N#x{Jm`{M2(ou9t5 zLM66onBF_13W$at`kKtTp5qtz7dUr%&!v#DWKGl__KspNkWI$x{WnKZ<%z^I(by}K z+aBRe5GmWDymB+CaYw||wpfS)RvKW|J5_7P=$@E81{84c_I4`IyomvE8~UHSieHw9qExA^nWSsB1NPTdz%gCqUT3`b4ztp|GgHSRffJIc zYp<|)ZMI`BCa6=#w|DveJ~U92`zol4*!yLe{3xRCdrXlumu_~rnNRpYhU+Il8M=uw z6wn?DM>fbjzL*_;_rUKWc^mD1D{Xxn`R5KvoGkWTHW}m2B?DTQ*pEfHAC*%(GIgz2 zd(qI7MNI8u(N1umspAi*U>Vj-p~XUIf;&S(t*(Q@jmLAc@fLZj*6`4;Iq)3445a4DBaI?kma+1l4a@7v85-nAj zk-Cw!lvayOUgMYoFGNaeOa%r{{GL{QCj|!0WHw!_gSWMwL)xLTJBWv(qn=|G{%;*_ zZ*m)n`8VJ!We5Df^BYhXXoJK5@|9~j6VUdY#4eUoi+ zb{&G8)Hj+?(S8f8r{OKiP(H-G98^cs6DNG%1>bpRvF~W#@*-{VW8Rxkp>C;0cNBGg z)XKwP@2$Y*>A53HFBZd#S^<4!H3$7%03E3GX!ZgToMXN}VPH3qbKuV!J^l#1s>1Va zZ16>D@EA0>OSv3hlsVA#+gQ`qiw)P7^S!5R*p{gqDli2G=hF}svM*ja6SDLmIwF?oFU;*jc9#HMeIWYwdqGf2qEZk z1938#tY;&YoWJ1sS~$U=Ez5UxS$_JB!7o%6#-Sw+%c5UAUuB;-!FgQP91wHzv@^Vl!qlR8Q$@2?Me&d< zV!dpH(>nmVP4ZTNb|YaD^~=eL7F8ER&}MTX8`@}O4>`nDQBJcSUU({qv#4?`fZewp z{^JqI`osuVkH8r@mVxmCynlb^B?UN){}E|h|)TErylD>gUeRmWVVv58or3~0lIxAR7Dt~yMr{o z0Ry|Dg4Gy@3LzFv2r&$aD@cba-W2G3V|f;>i{GD2Zm6A45xa!{dbk~-TiH1V@P0h4xKG=w<$DUr!N^+4=jDTg{`E=8?7-r7K+)nC)P$5wUMQn zzvHdsY`T@4O}CP>=~i+!-Ac};TglmW4yl~66p#J7Dz($)_&B>xbegdOA8(wi7Ji5d zH>!mnrNT{W;Z;=FqZWRG3eQ&yKLw9#y#()qSpH_YH4om47HsA~Sh^ZV=YNkc`_F+z zb}Q2dWCbd*T{|w zGyERk5w3G|_OQQruOxfurBCxlvpD|~ACN2OBqS@<_+!q>dT(ISY3r#ZjoLR2Cj3Xh zUL5O06|mlSakGwRVcw}UwT%SH>`wBkL7+6f89aHWmQeOCf*BI9eIDol<%g!Livyw#Rs8Hj(QI?htGN9xZZ` z86zIJTj>bqK5}+CFl-KHBw`;l&{v1}nyq?Zp$Y;;Kmdju>2fqjI-VENK!HwGFq8C) zfx)Wy@y(_2)g_OwijOa+f@}$tOSaMY=5l!nPX+w1R6Cc-__+iSP(*t!NvXvOA}Vv@ zqF)&ErifSqbR({dD~U&$%|X&FvCTLlF=uf;VUp(5gWZ( zQy?0R&0S**@3@x_rh0RBdoz@u116D6h4Uldh+q)Ep%T2BJxj$t@D&D$@B}%jXcK$$ zu&EuB$Azv-a69S<`;G+v3!m#ZXUs+g8MY;si<5-PT6WMo4Z1H@Ap6~Lg3Nbj9D=`#n~+j9#x!?-y`( z#C2Jct|*ZB#M`*K7LrL}QX4CI8;@ffi4C+3A`2Ps#6uce)L)u9PoWyfRv8Y84+>)t%Wg2&mZzl1Wg9(oW=7EgTpX30f`@0b|xsQM=2$hjk zR)AXHECNy_%qS(96JsVn(w)?KPGaZ1i+JY~Qg)ur=Lp_#4}I)>|3n1ET@&KtcHs;a zQRmd*q~s1?)hBdVBRYIqfBMn#E}D6Y1f1g<9Hr=xY^O?N9X=mU>Tp(Khkx_)4zJQW zR58G&jj&mL%vZZ~(D=%!8>J$-ive9i7x#%S-ry9#8tVd2K1ZwzBqKRAi5am;#MR5; z9reGO)X{y39ZjH)yg&Y$olH#GNqb@^_g)y+!PT0hDN6ruN5S_65^y>iCIX2tqZru*ml|dFrkwquT(Q zhbv8QPUI%ud8|YVtZ6D^ zZ$^kxruMV&fT7k+?P+@Q$q+q^Q+x29Q=`_~^B1-Cc@1-BZ?Ni~%z?ufmFZUCGt!s> zY={j$r?Df9qFD&bfaXW^+#6;*<%;$^DE{wfG*M5s_ZJ$p#8I3L)(M#{~Hyg`-@(kA-z^|wIc(cn1 zC^w~B-cv6|7Po#Ftgs+iKZ3yu(Jkon(C^SfhwZ?G(8_K8G4NB_`&;-E$vx&g=$+|# z`dnDc(gh<$o^FeCbI|Lsdxx_v%GA%G+z~dT)J?as4ss#6de<0TvmUstoyzUX7)s!v zT(=VnLI=?HS=dFH!#a;`j?bBE={YmJp4rjWrzD zpZ!<`*2uFHlNx6v0Ni#A0Vb0v6=)j)y>zDr^q<}vgCPIl?_*G&9EsOi63^7vFbBn` zdCB@1*0!7Tf*BO61Komh_jJE4O|$4tk8fDEA=#oM(SLY^e~icd6BVEAKjzL@OL&vs z@6lDux`i;T?!QaSk{L2)=7u}=v|Pt-GYbB7+BIGiW@6iHaBlE_O!qGxVSko$!y-rM z_9!gZ6>RsS39Qh_yt@q{X?HM(Ex?dq!3C0YwEw*!7dC1!{#{iT&jKSRC z9a77ctm>$>LK*#6bTgyaZr*cP(NP-=*V3#fub^+j0-*iTyJK{L&3dQTVfJqoPMnDo zN_X#`c9_Eu*?U!nN}9yR?S4;>`KO&N*Rw7(4+}}_SgNdr!$sCYz>GfUPTx2*Ve>R; ztFr7C+U1z1+Ub8Da{wmvk6G3CP5U|aT~ll7dJ*TXgLn66s_sl39atofx&$1%2XHKn z*yK_hJA2`c)69W_2Xwx(x!Dz9!PLM)yUz4@xIOH>q&W@spQq>&&H}rbZ48!59@mX% zSJe}}U$XYs8GgRi?~$l&dapKKhLkJ3 z2}7m)JZFqY5I5lb(+rsV>pChEWEyWR7iw113ukkbh&hbJ73f{4{MUEjU>VQz*(WNj zAAfC?H?=>2W6kHwZfXy3{F%O>l82%4`eL(!3{7oxHy}zQyTJ?H&&SAE zZV6;xKbv8I<7Zo7lRn6!wJG)!(I{=2ULERz(b@vG554iU0vv1`-$Otf74Pk6%PIv5 z9!5OIENL*dK)2pmCWg8EBETS^#vhm-4kIG9ZN!!A@hsN}$L~Ev&f()4z4{sL`P|BX zZW*IG;P?ON&4m1E{ZmdPJnw5md?hFF2V%8i|C6~treb+*G7|ppI6JZYqUkEhYfDM; zEP~{fD|4Yy%(V>rAh)kvnHhU*)+yhPJ!$Z<_Y>hA*`M$%;9;5aYCq@IjeJYUt6TMI z5Vv;z%ObB{s@e^C|43V9(OA?Fm=oiva2Xgw^rCaL@_!Z;Fl6&x#9N#) zmh={?$N3O_g*TPH;_rW;x*v`iqOVAj$74|-2nh6-hWT(S55L7{4mwU;F|*5H%4V+B6V{T# zRFdo@nO;_y%ff?O@Q^=1`(hzuxL$$BAa{ufY$EzvlDBGzz_z zPVqQ)jo$P^*sUDh$PoacnHr{7Z^ylg$8SSxKAH)6N4ha7^|tPbI^aaY=woW6Hxss! z$E(lmLEk8)nKy$HodNx2mD%t@3#&o?%ot-L&`r{anY%)`m0T(%>9~WH;t-E0{}_N9 zt;T?*H}E=z%q77AxIhwjpIctm6OZk)vKvavt7|-KH5xani6S6WDi$%gIrHi{?MUY@ z=G7npA(7BTe7Z2DIeMWWu7qAq8aAyq zUq<~)nD;nH?iHr?UNpi4shU{l(kA7Py%R~yHMOIVS;=l7v=REHbsPb_1?*Zhw;x94 z_5-#v*xT>+_#eaxUtX+A;ur9%^IGZ=F|<(h=zFjq<6nxK^|Yo<62;o zZ4}t#5;ZLv#f7|CC2WzKRU2@g5Y0^gT2Gr6&B%E86Jx5hXo{)(opD3cqH!M;;4*k{ zq3~DaL=&Zi9g@2g~Q7Hh~{$>qRpV zzI9Skyc%c^-OD961w61d+m&YTB%ND%cgZ9XyA?YVp-nU%z^L&6X3hgNsI3Ml0FcgG z$AW8wm0)&sk7wzFxH8JKuL!p-LT|USZ|h`=Sb^Xf+{z8NQZNqH|1uq`zz&AbkOcj9bD^w`)(D zE63VAJ1o~`00l(~^fyxazmItp^3U^ud}3;^g{Jb)TS33Cr{8EU?45LsY}H$R=dlv$ z>Qg2idZMiEv7}8D6t?hs%yu-uzV+z9QrQW;k z!6A7sqqwIf#_li1x4nZ~C-8ntgisKz&Jefx{r^aNClM^cct?8 zn|NezXtlal*x3HziA-H(vz`)40QZ1bsdix*{@$-Lb-5pqyyv`ZAx+(3+$+Z4B2(+! zW@>*+mq2Noo(KZRz7_BHGCcVE{`DlJQu+MV?~RA)odZJWcrSIlPs{AYNe=!AvVTZ@|V|$={IJz>X^?UynuyE}gcT4297EWMFqR${$1P!|Kbw z>{bnfQ9Q<@-&X4u^U(YR8ti(_bpBQIrR$uPrUSty@TJ|f{EJg`xooAHqRz3$Gj;Ro z(fk?FFJ9_gn}hgxb!%+`HGJ7pIdRi3q1B&_`*_(?Th4_%8?EOCCs2J#FKTvVB z=lg`CMR7Q~=KIMwy88QZI9kea)a?9B!%_249R1PvlW_FLCV`{ocpR-&rbEH@eWvy) zNS4|iRnU6wvB2@@%NHQM%5fG;zDrtPag@P(o$oyIEELCQq%2LxMMRwZ4OV1CBh&sQ zP-2G?*$FH7C(Ukqhtdl#nsv2pn-D=TV}woESRejnrq1~Z%V!?RbU$VkS>gO9;=kAD ze2!20d}e%}G%tPL@5b>dgtl&hwxvv>GOcS7@pazuY$;*>PVXpz%1}Fvb)y$+buEzH zyIzY%2?YVo+=6`%`DpYY_`x;}+;}sw!zFx3A4YeEz{EjlLR{U<0SGcM|@7 zpo`)1+b9xakc%yZjut{k3=v{MU9g~-Iq;tyDCRfeuR|&OZXCsY{QDdmEy_3HHG9aB zN7%^2b6f1QWIq97?Rl*!{WSGXniB<-Co9jnHg~kpZ}8hg)~VY zyHZ|(w+;=_cTOLFsuuGXE2A6}bUYLk+oif>@u*bF_g=SQ;%(U2R=sj|?g+{-+)JUT z@PG4CCrw6OhT-|R{6!~f6Y>{{@idX%fSYaNbwnm}XW?+TrI+MIqWa_@j0?)1REG+` z$pv+1gBtDfH1?xH$WO2!T~k9Aq!w;L%6pd7`vu=6$u^ue;UtBF)`n^AsSZ6$leI&s zoJx*XSE%Nq7OtQj)dc^#%b~yqD8Q<9LT?KU)($j^@MR_UWamBACwD~^YeVbN<`p*E z4yEwg5jt`xGRiZI^8{N;!T*Htde22FFcUagE%&T?P?bggdJ_-RmZ&KC)02cdoAr7e z(}xH$2M>gRc<^@P=~Ukk2cDRJyl)sRV#UTv4E{(7cxRqO7vfK@!TAXm8%^!G*xxQ? z^cB$hOhZSXY59fl#<|55nm~-KtNV}?2%8i$oN8=TYkj_%O<+d@n@wz2;O|G2z8t8x z%dK>Ok7Cg6{RYmf+0BYivZWNHtfrjJGy+HH&K^PD%WJU$Z>KUJ+3#Wln`zDlisBsJyhlTld{Vp z*B?hsO8rq(vDP1vU0HJdpj-@ZUXhE9a(!4XhPoT&Vko~wE6UsG*Hohw#46P=#|%I5;*Z6|%fm4FNHxAOzQn?5Ib%_sl{BVSlcr?me;olre<>AKHM}7ZEoW)(UDH!#^Kf??ORB>uSVL(rK{rxR!`tG>)^*j{JsEwA=bzn zU{*FocH!WKqficfLgTChRWJeV4kV4V8rUv~YWx!RAi3LOceS23uphJ9j|Y9~Dp6ll z|H4Eab}?8T_sH0y(3p1Vm}PejpwV#ZBStfAL;@{I#Alqw1R-X8&ksMjiZJ zJY0CadT*@tY{b4Fx^hV0Z$F=^?`{08_d+?NU`ZZjG0DbpB*MT)^&GbREz$DvT5NRr zGqKsLlUkj8_G+=^+xVy3aQ0^7C+`oMvy^w&!lAJexe|692x73-5nPW?!C|;Xk@b-0 zNzHn|luCJ?p?Mf$3lQgds8F9rbbTJF)8|>^;`Cwk7`q6xaDz?IV6w|l=`Z3qajdJc zvs!in158}RqHI`Tp-tyAQwUF!a@mVyJOZNbLI;%a9b#vfcBv5?WV($+mEA5BCn;?w zzxITQYIL((gUYx+>cZ;^^rXi4R&FTXWKuKY%as$M-WiwxPsbKJ0G&Io`jWyG zV2-80U=BrGDnPy;rJiyTL6%~(tiZIh<&@y4T|`}}^$cn}>yNJ|{W)D6Drw?Eq9L#` zC#LtK`v_>iO0=IHYv1ve?VIA-r<;kEcN{z+H^z0X;x~5{a-0RS;_uO*C`BmPb#_^g zmo||LWvg6B!LI8hxe#MrW5Tte@u!&AVeB~eGRh%-FDtY-k1Q$(grm+QwZTFQJGJQY zkmb(wJ?VRd|9l}Fie_W_2HBc^0(d%LZm@me+v*I?yDk2==5-wNUk0ZMG&3pWjygXM zmV>ah=p6O_z8n_(J}G3mFTF=T$5|-(KWJDwJ4>^hpm0(4+T1A5iMZ$w+!?LK9&E7$ zTdb_a%s(`#AI!lP_}4J6w}dRlHkV}S;~l72IUd}Gjy*NCem}cga=y9P*Rh8cntfwr zk+56u`+jb?|36Wg_ofe~c*V(xkYBE68~X1jK5@m#P~HmlwMouYYf5axzykF#{+}$y(d`csGq~iEe9gb4f7iOqhUEh7LfH<9340-B#j+-CuDKq zJL3w(0X90(kp%Df3dviXT?fn1c2e9(-eaI{@XkJ*q{0Sy_DAzl`9qSt-kWKCD8~#7 zhU*pdJ1n>~eQQ#znd*j!1)k;wP`-?W= z)lTK7(U@#h>wHk&p|-&Hnw~KWk8z0N;kz<``OhKb?Q4l zKkE;qN*)a$IuhB;Z37R=1;dkLp2E>-C-e+LCi zj69|5`LKMJUN*}Zv~;toUMX1B4WhZ;ndw(}0^+&P(vlJEYzM5l@*M! z)T$4@h|>U*Hjj)hSTl=~8Q`rRh&*j!o9!j}5_j#^43Pc>+mp;79@pk)44N zla=;IVHJLCU_UlG2jHzEQyA`q#F2bSbfp-KsBJr3`CP*L(#+!6BV2x z(By&^yL!G|$>xZW=v3Q~GiWDi>UR1i2WB#fNQ-Q1*Urv`#)8*cI(9 zkucIfZm>J8!8@~pcjgCM3W5!;AVm=lQhea06xcOL!CQkAtv9&(75*ko^;->6zPI4| zHS~tu*;bP`*y`Q%ZN7T_`33| ziVT360*uS6@n7zY4vi8hZ_vLH0{30|9fp5fe}$JeA9A&DTBTcg0+y(`l*c-(EDuu5 z7KQzmZc+0B8?C|hZ;*TL>XUp2##wM{`aHnz^u|>5pYj!m0A9)u9-F8O%`)>W1lO&c zh)Qpsz(A$>dB0X^zE$5_V#HQBNrqLyKS=(~zgG92;WX5L`}W*$Z=r#(r_dPG2MeX4 z^x#NJdN5SWT$HE~cCCyLDIhe^YDj_8I`yjx>gkD%$?Q=6&%e+Ghcn)m>kKKnGW z0*GH^a~+0bJ!xoQ)U3q{8{^#RMH^Wj*efl%XJ8f|Wty)zwT+8`+qkq`Fy!+y#X)j$ zUju&^58Iqkhc1mgei`c?*~253dA~!E%Y6B4R1x~yyA5oC6uezx3(#F6c)I~e7?3iM z%*%oIF$4?YwIMiNV#6DP24prn`h3Fzmr>NSgdmVLWOrZ)d~Uq!YPr2jwK?_(cE zHGhokGSrCNUKD_ejU#h89lH;+eqoBQWP8LK-p;)Db;;~Gc!OsRX)u-#6mL2(ofFS1fC(cU%w?-UYugY-ya*2S4rgYzg-y2skfx44 z9y5+MX=hmbwNd=?qVIgg!dkMkBD2EPMipYS3bBPlEU&ieMb2KEUgYe>EM)_edUd4tQL!6BASOS*JOFXDL-0P!gsH z$}XczlJ@Yz%Z#l{>9;yvL$20VQyUHazf$7z7pnE|zRa;WR)4uJMg0a*{|8!q@~0Ry zkW&V1>C9f5TTI>+YG|ru+Z6T~X}KazQbZ_4nQ_qrxZw%a^NBn;(MEN`r;7+q$$z;inYx#kKFMyW>cnF~Gz zaJMLr@{&{bWuPQYDu7YD13Bh{{irDrGK|kyAJ;KEkFL3PD*KN{qm`ajH-nU*DDy<` zwQPqncFRP(QMPCa>f9cRAC68?s3b(SPPyt4CH^7hXFZwT8^jd|&R^Rgp3{}px%-?w zNEg$BR`v`jnVbW@vl{BjYYvM*9rfzCSdZ7pXdY3^UU47|_b$p`<-n8J>!sV+F|J9d zR?BCYYs$)IO6t8b@hM67%BUsGitctdr#DAMyw=<_9ltrYDQ`}nq-!?R((i5fTi+aQ zj^d@SqZ~8XTy+rhzeiT1I+;&qR-8`f3_;TdGUZOJG;Q@DzXmY-#+%^wpSI?-`VCO>k zS6IV~>D}Hoh@4X;-q+(q<8myH%`Uf>er^06eyX`vmKILZ zRq`1`QwsFwQjYFNGaLj94nmO%nz^k&@F;2^0u`Tc`y=Nm-vrOXmFfxMo5pSXr-qR> zIKPRmgiye(6{t!mp!{uJG};mM?n1|Y+S@6E!#sZLsi^a6Y|Hx}6pWAJ2Y#(Aq&(v+ zn#cciJz62*)3W=Xpn7=dFYTn?quG7p$lopK5sj}F{Y|1|K) zPQ}IF>CorUQ;L>h3%W#k<()7BJk)oN_%`RySCKr ziD;Ae;#K_oodahkMLVpm2D0*`Wd!<*MbQ)U`qJ`h&nm4#=T+QL761^S?_SnLu)-GN z?04(i`W$+NQA}|#$U|}dTzTYF67coZb>jSd{3Xp#S^WHHomkLbD6R`WYjGX%S-L#A zqlRRUpxAuf$NOtS04yERMPq9{wnx+fKYS&>jgN;#T^ws{ae4_14!{~s9#Q`D%VaRr z;Q~L*Sz*tmuU=+W`Qf&~{EzP3!woHYN7TT2UU;XGG-bW*^pZsDNPxpOzjzZ(oj3fYdmZDm*wIMI)971 z0Y}V~pI;bcsV_#;%>l^Bs)5Aih^WnJs3%q~8aGkLe>;?M_}jBWTb`P4l3Bcb7~=uS z4>?>gKDYP_e+u9aowDlh_D8heY=^wcfzu5FQ)%!yhX=$R__JG2Z^{(sZax5tkq)gp zH=DXpkPRG9UzD*HFLSsNJN#3moIih}uAht~FnCXm$ImBWiD-kO_#KYGH}2>#4u0nG zpCjRCK0jL~!JrRB_)zneDQDwQ>$IW%C{K^#@TZnTVIDwv0KB9@ch~>DL&q&L;@w0)! z0BAOHZl9br?& z@B;p11)*y--v}MYyw|s0qw|*3D9w%Pj+n+fyM0yMju5S->C{~@@{Sa{f(3X^Yh&n- z_=7yhWd&tUGw=rF9!8fGaO~ENnqmh#l@Z^au&Dj^V`0=0=ZokVqNyJFtP)fU%S^UIXC$>KZzz&oPk^?y?mP)Q+~m@Z&4asMx*x2 z%s!n)iN95&>NKA@=0b=p1|FZ{dHQ^KAb$(zXTB|%QjmQbpxQdGCsxW;IRy~7GFEX} zgxz4^|SSF+x%39vPTKNqgTfo%*DRN>8`_wyCt$jgkBnvn|1RgoMV=XZQM|?YDqdzNF zr>HWno407vmGniU)z}ceIn5ccBYxivs0)yDKjkKe9exN0%Y_oX$?_P^>wwJ@bG!T! zD4$l|Q;J`AYIT%X;cL8oSqx8hr^J)o=(MY)mv1+>Tn?*cU5{|WK3J;G7m`gFNuXCB z*lc!EN&j0o-|9HN8ykIDY?^gkLIjb^d3=%C8SxuEO9@aVdez7{mtBdT9EW-eX6;=p4gu((bK zqz_gq3b+>FyLS zhRcD{ph|;HhFAF&nCthA;HB(`bi>sw1;1lpl~OI+0>lh&@xw0(m5QBQcCt?EvO`&Y zWs+)!y6l`jP_PJ0kh0d7J8Ps8)g-s^b;a>?a7lIO@ zjvR)L;TJT#iunIa)d)xm$IAWGH4-!kRuJaX$nqbhSVrX#hGJ&rPwzn?4K6Bgk6 zQT%HVjek}rPl%h;1Xod8L2jlI z+a;>NjgC5sf1M{84!5@5v;F+$HGe zgROcUJL3Grn?+dn)Jr1v03K|W2_ZZ1WcxIGq)uz;Zfr@6(?H;qXyDW)fOGi)K07lq zlg1~)9H@tF5iB(H>sEgKAn~4VPb&hjLsBKTwB80ktQgu(C727WIWZ2|V+37MhH zq|SWfSaGS_^U9;D&~fY%5`O^`u~r3ivLa1D<5q&_oQk4sd<4#V+_VLT&%8}LCBYGU>Jr4zldww?t1cWVO3MFN0`xssSB&eAij(`0}d~K5G z;HQ;ue-tNg|8N!4ipbDXg?-~*zPJbQyjKk!VLn)ehbKa=)>Xp)-0Z$m&g1Xy|3i8V ztrikmEgr6+m7qT{wEDj2z@JV?gVaacpfkj`#&p6I%EhLBnjXLF&Xf9dnnXrRav=PbAL8QYqA&S3DMt_J5fc1KLACZ#Aeha%Td-e~@{{+tP*t zyi$DoU7FS@^P!gBRo=E4#RMg7#bjNaL0qk3h8Juc?oE=0b|}Ai6u@ati!D$iEs#?K zXT;_r$5?&;^>Gl_5yV!5HqAD^d$Z7L1VuC2c~ztv&5Eai0xb;6{s@Qs*E&IOV>I!K zVX7tAjhn%d?{VR5 zu)< zq|xt+SH)@n)al2w*7_AeBgLAD)ye3#&qQy7NUBqV(-$FbMYN4op_;FM({$*Ip2Zrl+VUa zROhdk7o#|At$zkA0h}I418Hu#167v^Y!vD)lMN#2Hp<$T7CQ$+H$Op|%C*JB-SeEqqt}7w$Eegt0Md zJ;tBrm}`{ZNa}txFjCzQj1u004=lvMfu56(QQrDb7NyLhJz=@3&U|=7M|5cw-qaQ1 zS$2e1dxclC#8R#gr|U4sdr;;OFY~BcMso|`W$b@}GSAQ`6z?KF0%Yf6NqS*$ARsKq zcf*86p3?4tXnHQ`0ZLKNzTzR7gwz@h~kTG>RRQEzw>xx^nSj>Oy2HX#rXtZ!RarNit`*4u8@Y7SKu0Ne=n{c zievcllDNha&0S*g4XjG{RFIjDOhg-}tMMQ2ot4Z+KJRP^B?O3gzmGX~*)C<$;{g2T z?&C&>ekrcQ=I&FYx{sRze@_n_N)H^?d$aMMu{fCWN+|!UzeS^SCV|=*_7uVj~3DqZe>gy~GKVM~ta#7*lXC_1@o@bf&?CK@JV?b@M}K}3qqBD6bz zi2;;>M*WVw(DUd9g17ObXOGa~6?d=$6+5Pmom^y5CLDx*xdyez)P6Ct<-mlXhA20h zdki+wTt_yX!*(=Agx^-!RC%WM?;uy7fo95iWb|=gBUf*2GIihu1DS}equVA^JL-Ho zqQ1Jo3A4BIWo()3R;?|W>=SG?w~zHumbWnnIHwpF zUwRP^ZfqDQMvYrfGug2Y*#F#p9miV6gsy|}J+5r|8OVk`dgnp^M;%dB3216Z|NdHK z3U3&)YbjlX5aoU481|N^=C%6{wWe8jX zO1u?r)H&KK@H}}x(OwRfGh|w$DrZ{xM%S)9@HsBv9LA+KkTEWs<;eZOxue#!{Fm5M zi{#v8x*t1r?tmkv#H_g^m^$v$(NSuqWN6M^z6q?xEEg@){1R@$h=Ki2U^68Yj%ex0 zkY%a*b*!AGIv*GgM6l??|kncg=pILkrB%AvUUS#lr(qHAv0 z`RA6YtijB-zV>;jV6rT|%q`)NJ(?bor@W>*Dts7oROnm$TKcA72}W5l145e*L~{G4 z%`*Fb$V!aP?!{*ZXPJYuq~I*WKy7+I^aukTc+**8@RmADq+p3*QF^dM8oGa28`_1N zbNjf-?<}ds3ivKd9bC*d%eMP)OP5Ig0ajvQw@OaM)Pdd`(`Ffc!+J}MWFp=I401l} ze(ba8l&u(%V)1z}g{rrSsWs85WS|nkn&sTY;LT|P`?!?@7KWs~~=ImWOQnNxInCmRDdTX5}mSBk$ zrvho5F>xy1gCXWdaUP&biM?g=Is+l8T`oKxDnE{1LxEns@Qg;cq@(CI)MD}PXC+p4 zs|A+~CB@u*(;95JXg0oRw)jPJ_AEXZZRUG$$>z53C2Oz;8!W*F>%grDAl`C?r;;BOxMb-~|!_*=w3IpAjw{LO;DdGI$I z{^s*fR`}_Fzc%=r1%K`EH`~_*0_Eh8adCQIdf${2tOiPsJ{pon!+`tAS>1JAACl%^{7>LPW&J#IF5{kMP=>s&*rxh9vy6(ZEZ@9A}+ zmL`uGLjmqmezzbYx*ZTN?Yc^uM!I|xej=OHtmtka0dTOr)Pg4hG3KLLg@IefyV0^0 zs8=W6if}=ot^3iV(P*%XLLI1X@&;a$`*Vgmh1HzV&0RWc$(n)3AegJ>JfsUF@)jlg z1@3f z)K02Acteia;mYY0b>Fx(Nj&CtzwpPzbfu7 zi*R(u!2v)3C@_rOKjp)~k4ky!>UYI;J(bLrJz=PZCT1J>1X^!NsoJ>a-uShfx{|6s z^HW^8w49Zq?*92HS1#?|czi>QquQ&m_S;fUm#DpZeoBxx-V*~kjs8ynz8nEJXn@a~ zpAzXh?nwk3HG3Yv3V08%dvR*Oue&D+<*2gvrvl|f<-h+ZCEUh)aHTe+z?k+hw-mpd zHuiUEV?Q}&q|W2vOaE}{24B8AHW8k=VzH4ARhoVz?z}{J*ay^-+>8)L9)4$-WATXH zOZo5x1-|S@Ni&A=MS&HnKov;qStE6Bj~lvC{^O_v)h(VnB3dk>9Bob{-K+;|-0H_b zXA}_>^MBkymp!B1o+3V~3+RQ{A>tK|!`u&PY^M(=!8IFRtsXrRgi__R$D>h??FOpD znp3Xn?-$bepkm%nIUOZ0txh!HR$jb4zMmf^^z+1R@ajt5&yW8`+H@PnBDA1P)p=4S z!yErHG$^%YXi(}<9ch79uH{Dm9)25dqdR48Nw!Pr`yoMN<5LB{#vnx>h4CiuJ1TSq zTsgT*`8h9(fQIK1RxtBO9qZJQyy-o^b-e-2SA1u)aUwp5PQ9WLmPl)2mPprmc3(%W z&fu+1rJIE%3fK&at-R7=?g?|9QuGa(+F@>}maFnz?yIv$;m82V#V2L)5Td!9QY4%2R=9Z&+;_E zfl{rU)jWn`nNSDmpXGiq?N+0ygB&qS49-o9rvz^`1}_iZDh=F8eh*b7ln2W;NT?i; z;#}a2I`rjrJ2Ow~w>xl0oZs%{Efd9kg=G`{;U2t}O(;9XoOs`6=c51=F?BqKg4(SH zhS+<~!OR#E-6v))0{v<(%VIG=~p^`m@S2(~EnU?;B(1o?dvR&+Mdq=y$ zd6;f+MyMGAe7=r-{@WOZ>vu*L-Hag_B0+j&Cl=Ld@IQd9v7(oxP~J-^+8fC%)LzFP zpMg6(4q!ZGW}$eS-S3PVxjQ%xbCImWmi=oG(BNze3r&ehYT4%s=C)u<@HCjDJ%XIIeH0M4eyDOcg(uoaw&G7K;x#nq5~~S+3Xy9O6=N0^U@; zNKn1R?x)`Se9qiG0~H;|{D=7)kb8X+ygK)E?kbOc_PO&few5|TSVo0%MJgy8=EUVU z@ZYSB9rGL95_?qTjfK_hhf?56z3)PHogIjGyQ39xJ|Pf8ePH84kd$PeNTRZa-Ig6V zrAK#dv+oiNks;JIg}%%K5jkyOtX%YKq5Ubm2VcP>Mz#RapaX5)?55?Ylyd&rbU!u- zi+yffD|+AUYAnV6=cpLQzZe%E>7pEnilzS%orrZ&pvAfvEe}&0$;2`Pcd9Qk?y+IC zi$8mf@(eB7#qeOcgNoh2zU0v^S_+^uIn3VV!7ijVYOIT!@V&I=w9ziMpsgoGyr3*s za~+mIPZa%dLW`nz7Y0tHvD?hPY}O-7>mQ30=6v=g6tnGv|I+^)>64|E2j4&V_COsq zvKP&F2VTLb9Fz8zIqZ?lfp0tCYWXH_1R8+ztOwe7j5h*BY!PYYz`2p8^(WcuQ{IFT ziI?jKGO5Pu(6xFL)<_3yTQoJ`cbL03C< z{1tV&8u5(XhTifPOuE3Xvjloig`~we1EC+H_m(we=|s~5ya0qEc<*iv&2=n9yhDYBV z4h%aKp>*;GGPb}ssNZvN>;7*1SMG~!6_NiU??<*CMAJ(V{Vy{ziG*jEjUPxPJeL?E zKQL(Vo_Hv?ED_2Lv(x4a`qdFN5uxStJ&E-KwMc&rW3EUmnVF7a;3!-M_=PEJ?fv9H zT4JOQ3*-_r@6aWeSa^p;`*=`1oQwiKA;Q2C^E2{HyJaHNE~dXIjby5z^YBjuJCFU8 z-H(Q7&M!?}KhZ!OK_K?ui2AIeTXcXjA*nOcc$9rO1=a>bHo+R%(MuSYs_{6F&u54< zp2V0<`*^rH1gLDfjWxe&M}0?viNVPwYa0SimRNZWASApgRq zWA4~&IUO~C&;>>gg0F!dDKOt*(evIAZw_uH+`zu(^u%7R7UX+U!MJu8DkOS}ZG(Z#$Cj6oY>syL>j0e4B}UpJKu(w=LONN8uN>%F;0xlflWw&aspwbN#?)?L6d;`;Vj5~S{Rw_@gdIWC+W#y3W~`8<`$N+Gc(&}AvK4O)#p#Qp zBpwNHxQO$YpD#S-p9Upoj1vHzD>!0;wK1}s2keD2ht8V01TLL5jRY>8HBAKWWdM@8 zTO33ldRng3cFTmvKJ$}ZdNx`bMqD|25yum@X*Peww<)E-LbA-+yKnK)PfE2+oXv|Z z)4+G)*}T($VT*xbO9VqOdmId;6-)%qg=v}xyVBAyr(RYwz$gbl(td|wW%DQlcRs*U zC&6Pu{3#{m(f@ni{aepF>;FCPRB`2h>v?BOao*YD&pVVshMsro8P~Ue)S+P?*VMZ6UMmM8rB%2STp8{}#FILr;j-OJGeOA2&H1IP6k0>fdt{|97N z_!4UI7rCa@Wc#MDTO|~qzhn(SG+Cqs38Mohz>)qw6~kHh4b4IW8q&a0*=ap=1GAi; zw-jL;7pH84N=Xk=BLf#Z;)s0N4A{&4I`KAcXPER0hf z2uH;+^sPWkb{Y)eRw-$^;tq!2h*L2(+ZLFm5*R=M)wZRm&Ch}dQ;dLr!V%z?`{;Bs zFXAmtO0{r9Ah|JV-M<6t?xOnVr>w8SlDOt?PI(Qrq+&F&HK&jk5uW4J5`hwL9YPEM z9>m0Uk~Zpqtbt4208YcnSdzmxD_?oqtn9x7u8OAsE}uGSSq$7H6vb4sSx5?DPhpx^ zG!gDPNwJ)=VV^dUT8$$J1`@X%`~uF(j5>6aMk|-YavvT}d`}95J}pAA-ja*#a;yH@vp?tp|v?yq}pRi7%FhWq2h$x7nzG# zpJOxnnPyoZFO}#~DtRnXDtYWAn~BJ3>*zy%u%_0(*tu=-y&ZkPbymmSqWmy7 z-2YpYNVfULW6p-6rJ7JOi*@(^gg_!gU7RB=Sh!YSV%-A^VwSk)pe61$kV&S;1q+|) zfI`=^PjWFA_%V=3icuoj`P%0pi%FJN2#LhRQ2xPh(uye`#iWui;;nEu0?jRUu$ds) zMJ^=vlFM|cmnNZ(&CKiQBjq&bGbymYs)#0@t}7G0pnYcf@N1t>=?;~i!tmn7_K;;o zNa{k42$~4p8x7)}D9_iPH!wUTnFfM*O^PlyfiUWTb4WUe0k8o%a&`kGIi@ZR<_jMCY>@>AZmeJyB;u?Tt;h43+t80@ z^;4klkmWj@Yw3ZI@qx&9j|Mqt9-8#yq|3%hM=kq;IIuklU~}!Z%6l&$-dhg#>ol$Yl zXD1Ly1Sb9UnB4cBM5%LEOD(9DSEHoxWUZTSXP5_e|Fkeo9Vl40n?#IsAqfdV^C zR{DRN4tyujtp2CrM|!u+Wj_#-qIf5EcP^{W4t$XWAirXZ1F$&$iY*@iyaKA>bV1D+ zh)FkWy|Gu~T8|=ZEXt)BNm`*HT@GC#bxzJ^-B9bX#4h2$I9_0rKP{T6fkq}-CnUY29g*}12$dV7k_;Lu(?5Xf<$edmqzl>L= z#HYvWmKmSGz1#*f(5$&lUB5S~9L@0{UwB|(U~?2r;ZNxplmjhlg7>SZzm;<= z>93L2IUVEZxcYBJZ5N#Pg+^ko;B+^0=4ydlXhDrRoaXyt$=l{9=z*%}<{E$10bY8E zVbP03b6NbHucJ}XIZt|_{z1;3pw4OohH)}={YeGzQqE5$aVJ?A6&+(Bl%d5MwD4AS7RSYy7flr$VjL|cKtjf`F{RnH@u%Zyi0N0r(V2>c zd5p6#j{b*K7RH9i%UKxn-6`n&kV*PifF{_NJnHVk6sr=FXfiqzdAdB}=}yDHXnk7a z_?P5`u?XgcVtc}g=MaQt-5Hseua79HAsVMKFA4B4EpfVM8|CZT^eTlCutbnX8kXqVR3M98ul|z z!|eSlRT^dwlI8$um?aer=5iCtBKUa#CG~6{3_#EPQ`43idj+-jQ=C3Bd}VX z6`{G;77?dHBVzwH$_fjKRLrJoW08s-ewP-}b{Z;XAu47URLo9PjLboS5ko>2Dl&Ze z?;b{|@RTN0|CeA^5;_t z^5=8^XXVeTGs&M-Ur+w5`g-zb)xTQ)tWxFACQbe<)#T5zMER2pw`FdaP$qLbW?aw( zrmj`ic=>a=CVw_f;ZGocRw0_1f=>>WKmSXWKdX}EPp>9_F4W{t|30qaKm_$BAm}QN zpbH5>pWq1Uho4Vz$&F`ErvZZSCNAi4kR-o7#h={$BlO7<>Ao{dq@`RUErR7KgGaYe zxhG4cS*i@lg;HT-5iggD?7=b7G)qotaUr%`II^6#A*@Y>-Gwm7!SSw081F*~<9$?& zH(6dS)#TMOfW)oItGIfa*ot=HBxF)2ZavM>x;d z*#8ri=fmz@|3h-?Kgfv}{F9uxLkO)%J+pvU$oeOR)?|48F`<>S30CCy=Em`RJ^SPM zy#m3W##n`{1b#1v_&wLZn>XP6o>h}t|BV=Zx-}I1p6h>v->ajAjN|uC?WIMuo`&Dc zB7QGN@OwGL@1dHs5tkfBQ5P?@p2JDlKdR$7qkQ!LY8tkZ(=b>61OG3iVgIZ);a^R| z&M32f4H{-kMZ=qlxF~yrYE6Hn^K*RoxinlsVWEF}x=U>h!vYw8F zxnRYM@N5eejTSQNIx_UUV%p5?ubSWK@niWWjOB8u^xx7U&JBc5@i>iqB2UwKy!867 z=^Xifvf@QfRHfo5##sNm`bWnu71q^|IrLh{VfO#8-l6=I)3EIRhgBMe2ATh>?ofuR{Aa{3 z%V}wtru2`eVexAJt|a`Aw|qMD9SXT1rM^Q+#*_q*gfC4z5fiU8e$=iKFHz-MyoAj1n_Ad%Ok=Gg*5% z24~#J>yB#HD>Zjx?r(gN`@yu;XsOq7orurYNr{yUo_3z_Uc_dIsMWu^nBvOS2Itv> z^DM!6R^=i7hIL`_pzXf`oAl0Su_6DTcoV$hExcH8H5H?tf~zUvLyd!#*h9D4g3~27 zQ)1PIV7Z|=o$UegdHWg4D%2`hOGu{Y)N=j=)Eaj)odYA{q8n^PyMU&w-h9iVwID3Mt@ilH< zuZ_YaVuVDu^5aR9c~%d_e-WRTP6a)RkHaiuU9XjmT1w05X4B2l-M#dgavOJHOl4Y9 zjB{5R-3_yZDJ?iCA6$5zj_uma5zwF<++xULC80<7p87gk~9)ckywj*lCmv=_hMPV19!FJI`mU zB`nB?7qOt1u%C_HL-Q}>z(&s92OB4=mVJcb!EoDI_|iF+;_>EpxI?ofKpM*HdDHO` zx=@Xwg6&dn#GfSvZuofv{+#84LP`-8Xu<-MO(UT|5f<1vYNQSj2lv=7Cq<*RVh28= zz|3{w4#cX%vFb{^p~TuJK7%KUL5mGsjo-SS79|&-1ud_9MU;FEN~-IJlX{_@PbwfE zR2{l-$`e=tzB(za!Q#tV*IKdV%awO8n2g^ngeA!}`wwHePW)+Ve}{fk+(xR8(e;D$ zZBb^B-skqQNODqvp964A!Pe))A@;#$p!<5S*|Xxm#+fUdrE10U&dKONTJ8rJYCibv zT26VM#n!N>u4S$oe{m&`Y>0kqy7b*lvZe^bI8qD4=ri=9mJEYL7D~*>gG9bPYUGyS z2%<<7j`24<0{+|Q^Q{RpglS>WuJ{xO zA_YFs`;LUF9ByoOJG^TJ+=qq!1whCn!5)H{VG&$B!tk~EQMl7Cl9?wb>4i+1O#kydzH1K_AvE*%F z_3%uq<1E%KN{4T(SQ;7_H4k8gsyur|v94tTv-Gf;JR)k377?{r=YNl{`hZPcbt^Q9 zG3O9!D?)ARLSHyG-(h846vGzTzp4qw^bhF&{*3cgc60=e&W@uyG#t}g^O6%VzN({w zvR9@bP>T1C#sk0-yjJ3;1*t8&b>%9jSrTWKWi0DuXDB^BrL zyLc=LOCW5%`Uy^^ET-lEfpHzW-VpfF{Ru4RdOA3~=cwn2iiFb*aB>g|hPt*0ami<`A2VMs&w(~{%)wyEPs)NP$V6i1w zY*l{3-?5cgp;!7T5nk9f)TIB#+{akMMB z=D?lKPWXxjGC@1_?eMVVBRtIV{SmI&^}mu~0q~YQV2J z2QbUg>T@&_f?3US_vL;70C!R4nUtTt*`ZDhssi9tgw2xf9nQ8W_sx$Y1q|~E3rfEh ztd{ZVS}0VeQ+_}EVjU8#`9OEp0wc5vUIhdUUt*$f9q=UU{4KHPPM82DXD8Q7(rj*!apy(+goW zRcj5V!h_xpNR&>AdNU{)R1k_$yznxSF%OEpfW@BU#g4$SzyBrV;Mq>os}W_l_#Nan zM@l&Mj3>OZ2A{3XR(eAx_G);8DM*oPC|BHnny3>#Rl;HjNQE%CYqkozBKRS4KkXF8 zdP*I&Qsro@P)1=BD9>>-DXb4NT=NlOLj`qW9~C>oa|B^m6w6B!wY;{bSHsPEASp#4 zVeDq(c`k5wb%>i;A-o7%d58}J6WK-gz|^2!H1J$p9t(2mu9utWiG;^keCaCsuFfG& z&ngFI22mbZC#DNfR%wqtlqq+u(Pp8JkH~T^)#ajB&Qk0 zQ!k8nt|MQ$Y8{`Tg%!%+%T$3}k$DBLr&Y)EjR5Glx$v`_Y8EMf+De%^x6^o+^F_P+ zNr$dk4{LvD?L^(Tdc0Qr}Kqq^D!kjzPmQ zjtXU%gJTy53rsT3gkI@ftBt-&xf>qvM{_pM#=e}Uv|M@MsdIEBTOSgZ&H3~Tea8yu z7b+QxK)x;`NgcNH!ETht;g=15smXWMzskY4q&A(_-6QJb0`f zj&oph6z0$QiD@}nPqI_p?`2@VItp5iM_ZcE)Hj;5Zj_q!_!=g&6en}8$Z>pVvR)TBtPgx>cYd-6H7E{e512Z^ffy9~bjY z9b_qU_EXPxm<5@rP~Lh0@Jg8-9>Ik}6t;IMUpzMgD~A#)FM_8hpW7QW3%o~(;mH^H z_G5~&@`Z%mSBaai8Z&facjZ*%4(jWJS9xCp?1(qZ`3W4+AUmVr2t;$xz_tgQ9M)d_ z9f(_n5|B1!IYA>b$|4<0$S8{haD!0?=T=5uy7{;8-y87Xhu=&$hx60TU&4PFpC~X` z5ICM$&HB9)(3Tf%ddn-^K#@QEJQ}UXgTaW=^}LR%TG|R*o>yt&apjfgHB2s2e)oLb z4*$jTahuw$Jor5BQPgxOrE6)A+G2auLYumPHnm-B&F?+Wseq$g8P6+Z#i)S2*-jVA zJJr`pE*5W=IOhCixt;)F5#?NZujlXA!+Uidn)Q|J2ogaF7eakk!ynWqWmKzGH=OvZdUkcjbko=S{dkz9OXom{T@SVgO@6oWC0bVN>AH zk-!Oku-N50n=Q686DwW_=VH*nZZPyW!Ko8ioUNCO{k}`s4PVdN`38qv@9*e?I(&~h zw=G@_64N+V8)xNwOK@gg{~G%0v$7cpM$YB<4NoJik!a%_*FpxA8n`WH;(YE<6K6y4 z1_ze9KKEE|xMN>SK6VslufO(1$T&`xma1_J8}YrgbjseCZSzM7aSPKo2WL2f#dXYA z6gZV8E3Dcj&;7&6-eOEVuQM$#Le9Im4ycI|2%iegiiWEGa6DM-3C=7HR%Z_kPyaaR zs~hN+`@Ep-SV~S2?bt^a7{Pjfu()cGbJ`8rF!M0| zJ+kzBShUCtx?L%rzpr>cE3V?fbsl5&ep1fPLd)C>_s;Z3^aInK2V3%ab*Sn)hxJh1 z#S+7&=kJ|~^&`>)7-Zo07{L86!D@UPk$yjrNtGV!2+Q?li>?A3Nb^mE&Y_*hppnz} zKST6i1^qjnn_Kd*D)w%rdPDmjM}6;(?>k+V+B){h_3lMiQpXM)_+63!|5UKpzpvPj zT~p|;-&4QW#f|%F826QGzo#GdxY(%uSdsVqm|Q=fRcFZ?*i2XCrZS9&SzQ!_7xjL? zXt~%OxoQ5v&&5+XG7u?y;AEuef#dSr2ae-!_B{d(uYV0{5#21gN7*I&PkMi1;g5Mg)OVH0Jt8F?My$fVw4KhjINey#|}Uf(F< zvDY`r#dGEQ9J#nruFsQ;o8SAWJJFlM0+=gIvQ*dS@n+fylf-S*{OJS&Z*Ht_hgCZ9DQPjGD^y!6-qy9)_CIE9(E*^{&=MaQPBE@+GDM`V(pgM_9!58W*C zxyn3r(;=W?flUk8-|8`bDn*B{KQ4m9*Pj%z;p=t#ilIl3Tzp)HA6&yYcNSavZ@Nl2<-#!gn%DZ%_ln3j%0}%(Tk&FqKk- zmKG`(8xTOKTx>)DTc9UFzn2pIW*@PVykK#DZAj`c5&u38=yway?@*m7@Yf@Os6JR< z=evyEX$RyfUI^1>6)00D36!aW>D?bcy@3{$uFpzE*ViS|^>ruNERC-FbDg^ue*?eQP~!d4+m$u zDdJ%o(3alfEKby)gg>$paz}&JWvsd`RCV9UDO-ZoaH7l~$V}fh@RHoeipzke12F=k zQtgn(gluq-R+&O!ipi7}Doc+70>MfJis$NKo{H*wIf1TCiZjv4l;a)jyYXsW%E$3S;xmXiw`Njin!)kyD^?;+Ey)!+6u&v|yII8)p zbwt=0l8l0m@S;5G`6B2P8lmWCsrxM!{mzAcr#X9DrmFo~p{Z0ITd?CPz3Pi~8YOR9 zL@@g<#Id$W0_>>h_03q9MR7yThoN2}x@0}RvkB6#(IZfBpt@B+!Her6H=#$dbEZEC zFX{{AV)*~2$W0!k;2zqXK*5bmBY;}+T;M>AOJ&;9j%zg8|J^*4fBOtDjZGUz^e0M--U|Iczs?xA7*e7O@Ysrdin_b zYvLwtDu6#l1%GtXq{Xl-9%kSoV7h?a!^QeP-W$$P@m%D4X4b*<2-X1R2WH0DCfnLD zjjbMz31A#9lJVzrbyr7C$YbOK=RmdS?yi7^;b5 zsDfVPdIdni^KdXS6M#7)7b}tCY=SVEbr2i_unRM@S#=I_40$TYkZ`1NCqLjmxQ`#O z4&24B{^UMnJz%#T#SJG8WGgTb2!Jgz6V7bATx^Z##mph=Kr@G|Ln-GN8Z4p8sPhZK zG2n7j3YioA?;~R%0b|ex6Nrq#TT}J|dgB={7z4A)7+3%)2o7T$96I0=5DqCa(*o>) zT&zRJz(tG!tspW6xPtwUKu_ZSQSb-L%YYxME*DP2fzDw^9Olx{6R6= zhX-et#c>G^&Lun*;}YD+C0Oo5Cc&;U3C|;w;Epi~_861UO=pJV3`|1P8JPrC@M=n6 z5}MA$Bs6_JCL#ZfOhRJ6Uyn&>`Wj3^;~ANRFR<5j2~0xMnV5vH=rNH=$d6+Z;K6WmBji&jXSPcX#zgt_ppxaq&kDBSulU=-Z4q-Q8^o{3RtO5Y3h2~83h zg{BmYLQ^86(DYS|LL)H>?jejqBQXk1oKdhnU|POb@CgN+Pe|Vl6OWr1`GmB8JD&h! zf^JVyx2NlKZ*0c?$vy*%3g8H7BsB@L3kG`1D-5v2WrG&BoN&Ak>RAtmievcpY@l>ZNVR~+0%b><~|A<9KmN)f4` z27H46=j}yOO#CH1xRC}iZrIGOJfS9F)>#fQ)Q$vRVs}st!{%fa;m7RG?)iA27uoJ6K7N(XPHK{|}hW(Y!3)sgF! zlSNXrF_bS*8)!H+UwTX~xvu*Elgj_>LwpwO-Ou z>(nU1Uz%l5=_C?NaJ&S_RS8ca<7HO-4jr*Nb;MeSNb&X(#A=y^SU;$QSS>nYwd#n~ zt|L~90kKX4fJMY=Qo?p5woHnDT1TuV9kB}7j%Rel3K%u71Pg@^MVH0gv$Q>b?BfOs zX*j%@_Ml)9m(X{r-L?lIB5QMaMz;R|xAGm?D7QJ-C)>ZBZCzjcVeN;-{e0W%Rk7Jx zdsubiK8Zei=CBpPtj_^R*7Gw*>aEVcQ>hNtCcvF;%idq`ydnQoUB+u zZ0l~Avg%EqHX$_u7c{XpH(YS5c7Yo%xSrxG*g8=acL@ORu>1|4Hg{@bb8+_!dOjAY z-`G;{3}V#7#VL9^IbAQO9dgTK~Z=r{TYa1w{VCf9gMoa@sAYy`6&r7dizu z4;b}Uc!>b>&*op=@hPJBWB zo%n+MJMk;#--)x{q?!j~Mh+M?zHi#|KKgXzaF=b*zoU;Q8qXZ~40L<8y^t06O@S(r z{SDl|6DR-98-Vva5-KQ}CR_FoXj`LQY#=MzXSsODyAztvyA%4H-ks3=-W{=;cV~9} zx_2kE0Pjv{9`8=Dx_2j3`^JpHyFBksXg=>w6MA>RN{i&(Ny1p^-ksnAygM)Pg&%kE z0I>5RQ|Rw9XSOBGnY@sB$Goby3&aA1!?t&fJUbv1o%L!hyQ{TtNV#Ru-^KkplDb+@ z?g2#^QCC}(dqCv_Sd)nQh>*)9hRczTt+u@`axli(Mv*ocu=?0!*2+K|!GwlI1XR`c zApi&L&+FM;Zgv>+3at@qUj@51zV_Uiu@oAFH6q{50#-q$aW|C9wiP^+=_b1F8vP=? zyd51)hxHA( znDrM~;TM|e4NGz~BKu=wm#`;RTa{aou32rT?wD3rJE%Ljrzb_8o>V1IPc7Lz>Pw_+ zs4vme6B~Py`?}E6^FjLOhNmRA7aQL~Mlh2VGH3)(G?GiJyg!FOq}|Yr^8Or2_vb`( ze@<-NfonGIVw1?l7Q=-mqUtVX2;?W@Pgv2abWCIp1New!4hWt#t;&$>?}Fh4WB|jf zgk$r$)jk1HQ+h%EA1Dq;xLI}Lp%u4)3cHEqDX|R2cy`UcJg4zVnY3Wu2s+`f$+~=v zwWD=eTeL2@66+FZ&3cJ&J-3kt zNU^A!fIBFiL^cRUXx9|ZC4jMtf5Eq{W@cUdx7R3LtbuGDT*4qWDHf z-78G7W30fKUtqe=G{d70cw)ew7;u}S+>QVIIN4+2?YJAr;<+k9& zWn|DVar?Ct4n$juP*8Vn2IX)PYVi0Y3_e7I8e19CU7Auh|15n&n)m2qh%%aB6c>s-QA>iw0hND67ZZV5%HQ@flL;=as8 zSyBkAT%<1(D=F*CG*a1|&^3@k2_l6eFvDkjnITRJY{{?)~U~>>)vj+-fR~I}- z05+!yP->;I>`km4j=PwuBtV}kI$OAF{3=x2CcrO2twAp z5Y3OGEgxb--ku&FyjskL?c~HKQu7;c+hGrmgR*^megskYu z`K*qd&+EupF*L)X`p;rBtV;Mi!fg`)=6XWTR%`|$XFD_lk+Xz^UA<1&0ZM96{q-=r ztZHsQzFeamGy6re--k(0wuQ{(Nd|Bhgzg(A#M}VY9#@BPdEswT{FB7dUD9Fnlm?>@ z@TL)>cfO^sGLT-IM#nJHS|D(BL~9AQzG1Mla681>sB!tlP;)b0e+dCMRyS?n%pB;~ zF``*eigwK5z;gb#P~&PMokASY^MHo^~bgnx~W@V!t_NBBNDoS+&A<0mkZ zonw4a$M``Wkx)UD<_TMj&&Bxj3DCR|3851IUa0^P+FqmpRYTm>mx$7+Un&=oFrNTP zXTnlRjr@czKoSe10HGO3>KI?tAT=%#B|}8QL`94rhrF!BGz5Yf}v_n8En^ zY`Yqt5gotrN{(Nw5`IRqVd}F0*I90HW?pj8=%RR$TtV1f@w)ft0>T4?! zB_-ihL5%gMaH@zgoJ!>3RPoD;T#+s}az!c|xe_Fj0-D=ZC31xnm~@#6jQhaiTHO`n zr~pQ~4GOGFS75LjmWm1tseaUfDQS2?GAVquiII1gGXJ&f{7Jf;h zDljOJ!MyASMHZPAM`hRJsN$m(;8QyS(#Yj3EBq3rkfFB=1rhz#dK^{A7)MpkoX#ta zk;|3FNF`}BFH9OEpFIo0B*qF|&W2!Wxy4rE2kv=%}2CC2o1v1qFJ z%E1?3Cl6Rf`I?DO<-o6$HM8ohnO$ei92jpNJ&W$k=y}!eP@@;rr~~PmAvJ16(kNjEgY7j%kkpH8v8 zIEry;Db@diq2~SQZr!JLysU&@R6BC2f4{N>X(*)FUPe*~DYl&2F}+BN?VwJv9nmSa zVVz<-XlO|eIyu_?k0Lnm(tx#kJzAexAK=W z&RzA$z0LNsL)xW8=DC!xMZ1vK>B3_(UN*#8YkcTDKH@M%4Yu;9pHxBqG`5MJ_D$paCbqs^JlBbR?01|l zyuS3klb?5ER`s!GHU2I)>jjc)XBb1vIR%T3|7?adyq%MV*Qs|3Z)v4gAZduj)G+$TfTd@RCm_fH%l2A1Ak57{wFP=#4%m4D zs49`mQDLLY@iuC>z|%nWx(jkACS?5jty^AeUj%n1Z_kXG)%X$z|Iw<(-FEuuw!m3y?=h(E_4GHezp!266VqAY ziOf4@)#-VC$3@)fB@+N31wvN${Hk<4W_sz}#sYOPSj&o`pyQi70cTIUC*Y6+PV5N> z^n|svCkFWwX3`Gmkoxq>mnzITET*nRc#eGd6P=lD;sky~KE;7rA4 z)^JFuO9{t9(&*wXk%-q_l~+J0#3fjMn$ccjksUBuL81IPJbf_(Q~grOWA zzlH@Io;*`_;0QZe+Q9}J2VgjDd-mc}yxI6#<{hE`otmt;;}f`bZH^tS&3&Y1y^moe zxoELwLl&oceJ0=jIM&0lsdumE)AY)$bw}+`?NH;VtQ|@&zmyf0!!wUrGw+#I-xLKU zPs20!o`*YS0uD3fX4pw7&&@3b(5r3q8eSW|gSRRz_WmUc__)Mq%5gmC33z)}djf7b z;KiYE&z(EJJdSfm)F!n%eO+-vi~9U=s?Vk6H%5vPW0I9$hu~h1#R+p~cvv`wuV7 z9^G;1|ExXABPmk%e5YoOicuffGFt+r;*` z@K(iS>6krYU_aeQGNXQa)B;`n-DABUF?|LqCF_U!okAL8~0I%P~QYS=4vsV4WPRmtzh&=2TXBH39BUdhJ^A;|_OX>n&_+?S@*05oVJKsK2L&0?UHTYUl(gscU z!>z}1IDGJM$w~MoOg9Q+f}2QTO4c<3voN2q#Y_^i4znP}^I!kOWg_W$>KnJ3 zk`vG{_$INb{J(&3cs|8cm+2oqj!6rp{9z}h*R1E?_VaHp{_T1Gt%0q;4UqWu6ZO(M zc0#Rl6;5T~Tmm#$?{AZNt%`gQDu547k33BhnRot>x5Ox+&w)sTaH2eR!d%!?Dxov+ zQV8W#R?8m8j9VMCY#*jkPdXlJwzFe}Q-p!Czj+b!2y64RCHsImx-n^m5<3s$MfsIK zpn;ay)o{rO8_XOMGE=pw*D;y#0Oqso%fU?Rx$Qx>spn5;GCNA_&IA-+A+cSQm-$3t z1g9)p(UO~W${61@>+5j)MwW6}+54W~VH*GRvS%?Pth^iZ%Hl|d3L~DW?aO5PwUgf; z&f=Xr<=uF4Rt1>}E8B_Z*>YKMjS4O(eJ<`23M2gM*_2Db)Tx3ibI=O?xQ>~b6e*md zbgp#%?t$Tw`#JEn#eC0mpA{a;u^v}_@3rhizMH=GS~@pp4mz+^dof2n`xWMTTQFaGUyhwf zeGN26!+L2${`Adoei@u^fG^*HLVy~&;cFe<^9~lpw9RzW<1nU2^8eF9H-)(pWhe9Z zLivtOo%_uC(?T2J_9s(LxV?S76wUYEJg0;mPM^53^fZl+`Yz@^b*d4Uz;Gus+SiYrznQPm#JuZ8@1Z{d6C&UO`Zsj%uFSTymVqza_JAQcn)?@-5D)g@DU~zGixr=OGNNL-*MsnH&_vl@?-$rXtVs?E$zR_O5`A*e)Qz*jv*}Mvbm7eV zu_0*U)$H*Hkr@QZJJpx#xfv(J4JiC~rcfALhWc{0r~5a9p}eIZ9<(K=$>oM9ATlfS z3;)a9zraUTU46jtnPid-VZsbDK-4Hx88u4OXrd-gv;jj%YsAStN=Rr++i9AoVx6g2 zLr9z?WH^}+Ek(45P_^}fii(1Ym=H*~gJ?BVE~2%1V$k9ZQIYx9f9-S5%p^hkJn#2@ zzYopKoPG9vuf6tKYp+YWoXIYZTaH(F&v3i-zRu#ln&b|6FQ>hbd_~t42U@m!5-Q3< zuLJ*vJZH)`xttwvHqPsr61&6CPtO|&1V=X?@Fvh!rNgX;zIeya3WG2$1m@RRdxfh= zfnm1*`{D0@MDK6T1;cR*OY-ce$Cv9a);SZm19l0#p8ttjE@jJ1mJG-7nsJtBP`(%Dh8e_3RD zC#9>{LL_&7K`Cac&q06L^hc?&hFE+`u`w|*u`$@swEv~W_jE(i?9W|YM-m#}TbQ`H ztqBQOv7+#RK7ECbCB@*0yP~W5=AK~dBL+$MSVqh*x5Fgk8X@T`X?5|=lzUw5< z#WErqx$Gr`0eEWoIX9(NDh`x7l8Zz8Totr$U%QajtCs^>YzH>iHetlS#%n!PT%vAz zFK)aR&X*TKEesr=++rw8ZZsXWTw4)l|UD)-ngv6aK``b9pGl9KB z_Zff^{8Np^G_2p>k|BV$*w| z7N>mH{lSFp0ZY>dbToyU-VITuqca1kg^R6zk0?_e?CRg%zvEC`G8~V09e$50-VyBX z-*I^7;r9-2$B8M?nW~6Lts}I1P$gO>MJ#<1V9CJ74^R-@9jt6VT5zssRnU`-86ypyYwQBBRq=k z!ixIf00$r6L9dcIF_ozP^LDssF2 zs@2k6$hVmCL|KF(6p(>#12nF`NC4QR5_gtfq@z#+Fnzc*^QB=JD=d~L>M$lMTD*Z_ zOvKpG+7RIPPO9F9U2o&!5!;ymy>NWNhLPcTVi=D14u|8^k>NOP1URnZH|bn;p&G=| zMb!?k)I4&f*5Q>-JlsuiuWpEBuG-wM?%6%sHp^qu8Cy#h@k07VOjCy%S5n(6h&-w7 z7lha5J%eq~k3E*l;dSR25hwuS*Zw&s`dOfydXIr!pJ>K^9J*kR9CUt8_-~EAF8BVF zA-#GQwuVW^f57?f3Kc7jxlqMQ74D^a!JB`oQtN6-1cVH)rgivgR=yaunux_i8S^%Q zI#LCsQ;+&Kn|+@cASNBY3bz~`X%TKYCZ?Yk0tvZxMg_Ng;eA{lC*SJ^7oF$Gae?vD zdpqPjlEw83IFwJB+E5lh?U3YtV&ST2CQ^wTaLCsieRLdNjUd6;fhwI+YbYxcj2!RR z&`=_TCQmTyc@BX zLi>DOIbdt(@IDjR?`9)thntBD;i4~zqFk_QXu*P@;Hl26jx0&D9K4xe+Zw!?K-m_& znb2o)@MfY2Qi3-Vq}qcw+c8_dqjTu2mSwn~Y}syUz7xx85aJ8W8iC2Nmk>ELQ12-C z{85gdLEDLlG#6p>Tv3p4dLCi<92DdjKPZN{p~*M_3LSG0L7GkiDm51rG}Yh+7cP&Z zJZ8|>atzBlKolHwWm{{iE$!^|C+-M6S(dPuE?V-+7+&&JUUC{Yx2_!-;)uXLLUd7; z#IVgE62t9`OQ!FIb7UXK)oiolvy+{nG^PsS00)ja6lZm(Td8aIgWC~lk`c6p6F8)_^c=I>H1+rygib(;@IUvk_;C!%9UMayR`3+8`<=8UP9Y+)0-X)5oeH zw3gJ2SpnNyYjv9+PPfvOSi$6D(O4=;s}D9fuhu|ZB!WgsjI-nugiM_*tv`x2Qzb`+ z$5O!jr6O`SSW8Ed_=V$UnzcebY@CAxoucWnfC99djD?`LL-f6Xf2se<#l!nAe+rH& zFf$WV*=BzjlKKyxN(ErkWE$P} z+o+>^lq;`K#F82TR9f1nGZLlhXz8ZlU+JXI0>TGV{B!L|J+U|5G!{+XFeLfWn>c=> z$=78nqQ`vEv2ob7(~K_V_NQ2)4;k17>pke>&%!CbZK1Hxc4+%I4{0bzv_B2!YK6<4 zNds2qr!q93kncuD2+zb)St&1dsDggJuo4}r@gxdlSgniX5bBoN8pQo(uvBeoHGp}#BX?`rzXA`E$z z0ibCuE(z@e>5#6H4!)xhT2#m!oms%;oiiY_NRVAwgmQ>SfMDJ=hj;FNHR4nLxg5&O z9_)IVB>}u!8IN@__rK-vZrYGrSQ@ZXFfKrs!BRS_==)F{Bmw=~ssst=+mbS5JMevE>Q#s{jUZ<&+dbEF zgfHW(Do7W6sY#^EnJ%vsekfJ4!D*Y+E;rFqe)gSLMlgUqmlEasux2n~3_(U>Psf$Fa24;xsc*4=Wc$ zj(hp9)2*}}Mos4J8J2Et*yhluQI&tz#wgRyaY*4jOVvESqxB-dX@x*;C#OnMbC;$0 zJBih5E;0Qf+v=}#SS=lcfP|2o`INXNu{8G~AyRxdca`6&(dlmd7Ib=oe6GNcdz*R= z#$ub}ij0l~v;);OTBp0`r~(HXzcun%RtUTNhf)Nm&9okq{c=BTNE=T~xjW8mm)y%I zfY2-f3eIx9)c(?KR^`&R(Na+9C-4xSB67fsssi7i=TSemdM<)S4#L!ihuS`oPW)O) zk^V9kXQGhI*Z0&KlusUrO2PTBF_eKj=OVW1UVlC9ldDz*8cGlPj;58AXB?<_jkO)>iKY;-HO){xbW28JJn(^Fjedw> zCI)CGJ&t7O=R`>48c@R0{3onDZN0!$i@dMtJqw-NcgFqHx8vBfDVq@}+v9ciEVPv? zXRS^JW==uf^k*Bq#n}z|WdR~Cn}-JskNs;ZxVjXf0!5u_m?WEfX^hE!^JclQ*T_E$ zHycGjZgc&+bU2JESDstND@C17p$?`ge{FYwOH64LV{|ZP%C2-!S}yFVJ!eBGizg^= zD83~QgtpI(9e|%w@ccNQ?u!w}(-OpWcPl>Dps*=dZQ%(iMau-decQ%KgE4CjmKV1b zxmOS{C}rmgod&m6R*S@LVYaD|FxB#8!c^|mLiNo+55IBIn{$Jc=}n6EW-q;Qer{>z zYoa}hi<+^#xUX0=@!2_Q6O9k6*;k>WJ<83Tt;;2{?bNdETcT#WQ)HXQ*|ZKnqGj8l zWh)lhzU_-_bb*#_zSigzkKYN}@ugrw=bSr(nNT5a*DZEL%L-Z)gL{C&jQ?on$ibY!vaSyXKi{O`8) z!{sk_903`jS8Re%Od)=sS zp%{oOsnj~X)HfGsrLg0v)Yq>?mTHSEb?!eVtEIN+rH+XH^+c9}G$^!B;}avuCFS6m zf>?6*i(>6jjV%?z%6)uYD_FM??`*kRI(l+xQF8^iCU5KC{@-zHqVV_M+fowZ0R^I>8WZ1le7j_i5~2IHL7$*2=t?+<9cdI_vb%+ z(^Q-WYbUph!b-jqR1C<)PkI0uc^EHdxDyfm??cNViA84eu0-LBa#xnyseSKpC zGeGGiH<)V;f=})J5!d;9;#W{#m0sUji`()d>!b2*=B($9%_~u(Ctp-JW>|$@QK6@k z>#Ni2yLfS1=J5KCw{E7|tXls@4Xf?~QQhy<{zcW7db;}TT7BkW^}S_3eSOKNt1nlp zZ}hPGT19=Hk?J#_uD(XCzJy`*T_x&sk5r#=r1}7P2CcrNVfB45iR=6MNcDa3@M-Fc zIemT0M18l9RNwo<>uX-gN=OBwx^cs*n<1+E74%z%!QZ~q_0M#AcsYJfEWEEchmlq~cwC*TOJgQc5OSdEN-wpB?H3dY#Pp|& zTcHH@nBCIy%NXq}Y5W_2{WY__jb8%Crq+gPqokMVLlQbS3i0M1CFLoidBgHzz7@4X zrK}EeTshZ-yQEsF2oh`Moj-#jH4BRW-e4#cln%wE5{9mo4sNeC1h=a)4~SLutF`UW zKtM`=fr&^_hpPy(R+fk0da_?!RpJseUDesaja#Ow&0|-uGWA*N_#bfidurL3n?tlm=I2QevN z%2DLD;b@~d_iX~zBlWiS!~CG8;m2;%qvHN%QtOB^j_FHP8Hd-VUi}&5WvD~W%_$D; zI}oR;Pu`6#74;|8-F2mvH9$-}m=s4X9lGweaYU!({!a_8dR|ORlxQSN#YhmUg5k90 z-=UD)?!ZlcwQf-Gg%oK}fN6oqiI@kXKUvsL-&W7%P+dLOr;Iri^j>#xd?k^U!qU=s zsGkKb*1?Mo^&gCL<+CWpJa{%ejgZYe7;~uqKpqQKjKSrYOJ0fPkBDE^;PS^JJEZo< zNE9koH*k++Q1hiYT*bBKw5#l z;a}kZn%eNW2;GYq`N~V{P5w{5`KaC3lQ-8Yy z_Zb;oL{B`BZf$Ef;w7Ox!gNRd8(F`LHHWEdYZ#>49mfD|G|r9GRID<2`FD_f$tUEi zflkE?sws+Kej$NLb2HO`}6E{Yx39dj+V|D8uWqxUK3@9`6_7@zRz+iQfG)fgwl~!u2SnY0tvT)gVA$p0S7P;tcmHKuYeVde@EyD^XYb%)N zs8gqO6b*xsSe^1Q$nP1DQMk)pMarf3q+(Q_UgH(&suf@_cpP)pq9$e51YR=0qnehW z%$P8A8RM0V2~o=!r<^A;VI>Bpis{E4Y6?w8qBC3fXmp6l_z&dYzKslNNork* z3!ng!q`}`sf`v#S=0|{_-^!0g8c6zCNr)0mdV-0-6Tf<=vQ}&|ZHWlLG$`Kj40CUa zrQ0W#ZkNdMrhqxYUl9E-*6+=T{k6`q4NvusxVr9hSy}!E2WKT}D$n_q+DsC0+!9NmvrZ#{&ha$B z>=iQTWs6fG11>-#^p`vMGnPT;unanDxD0xs%rfW#A%mvgHC&+CudygPFD#1A3yY$& z>fdcS4pH=li}^t1F+%N$I$x=r@duVi7c`221KzXLv5ZmT?sQ0ec|p^faiGN4Hx~lL zy{)*pbAIl*NY2NJEsv3ZVssTz$z0|6rx?oI<+$qjo0`%4wmcmfA-diJS}senG;gIJ z*+lWkNhaUz)`hVpgK>mvvj{K#_z&kvwGR21vP5*4P9GeD&K|X27SCvTn{wv?BI%UD zn2e8MRi(ci>1osa)~ja56-#AI}J7FR+Chw>R7j z{=@jKTxJhse#eUwz66hbOS=XiEt~loyk)!B)Ca-8G&okC8r|Ws3WO!_Q>Mx4~%7c zn>H35GG9dQNe_F+aFFG0?a$1v*9}O7?B6iFf${SwB=LKrQSmn9j zvrHyW2T%iswnD8E7I<9-n1EH$mZjZ5e>sp{TiUlDC#sYft;WA~Q9IxHHlaDG;aYdP zN;WHh96~my-2k$Aj9(?2Eq~q|v;T@^DBbv8OgjIdu4!kKq^!5_Q3H(LC(@W4US(lC z`50_$!<<{wmPaB?YzLnFQjcBF_kG=s8&;aHp(VK#b~SS6DtaJJuKQG6_j?JkZ~5Mx zvA_SeHD_9@^*M#CC(NTMR5m)$*C5@>!7&IsBX6Izo6f>TR_m8HR{mBGr& z;MA&MWmRzMoM7dg;MBRn%DKU*?qH=mIJGWVSr?o-FIYJ*IMoxZ^aQ8Q4_3|(POT4C z)(4w#I0u*WXA|q|F6YmtHvY|@P3`=ff$!5`6Zo_#$AZi8E#Ctc!{5m&TpEptFlpuyn7*aWb!B(_{RbKbzL@Z~kmzKv>S7O}y&M`Lk&Q z|K?9NJ`XO(r|<_f!M7R85y4wddOwr>APZ~!ZEUd$Y`~tFT93pCLkTOWj*Fn<-|>D7 zsJFe!*?3x}#K8PMC`8V;y6 zjHOiHRzp+ILEus`jqMvxgpLj0XrgT6-0E)6Tx*7;9btVWGFmV+R3Hq>cPwQY!6k8b2hi{ld+IjZF5#0hD;>uih&!tv04S8aT)YI459Y>)CcnyIt%dN-Dolxr!!}JWdC@l2>Wj~#RnO@1w;qI*5hFf->rwtZMxTAHU&IKm#NT?7pN({sut>DS zM8EtUI9l2azU`sL4)$2$@4)2w`8a|$sh_96trasU_YSpIVEL=DXITGAgaL#}^bbyR z!3ZjQzX^qit>{2k{3}s$FTFA-2}XyCwVgd}Rl@gf$saY}yDNEAD|u8ac~qJc-kZvV z6W;1s!}_KD)9+*23d8p?0w)|zqw_Mm(otq<`DqMmRpvPdEzQ?K1#LF*@mI#cL`~D# zOODj3ixQlqQc9uni(j&eS_#QSD#RLEoXPK5?Q>6Ca#VTFa9*1ZX90<|24I%+;cRDuswyUIN-^9#R!vlX z#jGww_<6C)j$bgJo5IRnd75(9x;uCZ;j3x(5M8kzdd!?1rSKI7O4=C5`JT_{xkKh% zJAx&_?9;v(r3zODV=}hOMMl*Et>V)VoE62(d%R}Vxy1fjX)salw2R*coRQ`jab7lF z+bLX92%B5H`of?(`#9~ns`p>uXTm=}|D2X@5y;HI5>SXmfsTWWXpp3MZuu@G-^V;C zb9zC>Y{8X`-xh%`@wehrUIOaG-+@p0zBf5PJ}oaUVnT*5)fO)SIn&mG&&BsK5wq-{ zBGUoRgwGnygfG5o z{iMO;Q0^j}MNqt_jWX1nV%9=u$rC|rh032aD`D7H90!}_F}888|xi~lIJq5c1*&*YjsXblXs*{ zlwHXsq5XazHsIhz;y`udA-1npaw+e6Q#EF9i@C4af;T{C!cD_&<@KLxO%p+O1Qn+2LI+sD;OEj&{{AvyXa$d&{!I$+ z87T6XxzSIIqNGR(5G2bBO}HN2bW8`}36vpwB}h+iVweP#JMb$|ZwbkLY)p4dr&gzY zYd48qEB6iEx07xgzHcwf_!#^- zoNbv?wXM5bIs0p|b+<-sUG}mIx4Ro}<(e`M$lP&f--0w*`OU}3JmrMW@AK6u`ec zkMoKAI8cTEaqQZ@CTuE+pRs*!0H501J_XWTVfZ$~5MiEbVCo?uUYh`e)a` zU=cvepd9AUOTdR&S^@wr;>KWQL5JJHpO468#oVSczjA`*&zsvyiMN7%Tc;#sW1;;_ zo6@Pc2*(Tl^(|7=UrL*(^I~v#mBy0565a`$PfV5s%e_bWmB?XHTQ$D|q1E8KYU3^Q zbk6%7(3`|odI{kRp}`*IqMwh*>faPDwFLY+XldAc{A@&a^G8~4?=J;szejofXCqEU z|E$=(3-U*d-O1sgbiHY{68|i2nIT<#$Qo)@MZ+k*lneIV@56 z5)00hq?6BbOFND)wl^YO5U+GLJo$1@F`=| zj{tdEUag@-904sM@3<0o=aZTbpFfMET$~H?O_~|5YO*zRuF~A$h)k!U7ZYMFqwkAV z6JIfNL@&-7EE;Z6FZwClz8KE|Y(} z3qK0X{-jb)vHHhXa*EBLRK+RD{_%4-CB>gKms9Ni@or9W_><~5CDlKE9;c-F#T2>H z{p06zN~WLH;bm8re>{9gtmc-JxLI!M;ibQAx2{H8{-ovfQ%*W^OBQuw1%8ijoSkKT zcoqHZi_4`f34pSsJY*hUK%X0sDE1)BfaRXwcBL^by)k_q41V@1nUyr(>sDr3aINn- zPqzbw9c}5AE!KLAGp>@p7begWtiOa-xo;$KFZ?fpE;apjf0`@Mo`~9&RJTLYE)X%_ zO-EVs(d;nOmkT~-Oe_m2RysbIEUo52o7k0;JM}5rpYqho(7vWE#CJ$N-(P;_2M{z^ zmA9(k1tMT}6mbn6+T$fPFqJhoZ9&c+(TG(pp6d$j4=4Xn|33M*sQ(Yc>wms_#QHr$ z>nHf~H{lXk%pEQ>O>}U{?dy5&w|mZy@D9W?Z&PR9{M+McfxPQ7S^QX|dWt z-H)A(>n`QvQ=^y+Vy}w2&BG{poAO4>Sm6PCo3_MXCu`x8k)yO zM$!96a%Qqi^9NZU=bV~ekKQ&nVVc5lr-mj;ugC~xeB83V;m1&@B>GG40g3pH+`dVj z)-#7rt4U02LlTwtZ8cCC=Ro~7fc5MMxaRvAEEjbm+qbb5wSkvv=U+qXoTx69MU;`7 zIaj1ZhsKwLHh>KSqrp4xK}PtDr8d_ZhTos?HJ|Ub<>%m-{FGYepRhewB+sdkyw{~9 zHahPQ-+b zl^gd>gc!a)!FR&GAcmNNhIDA(TLMt02yEnK0`)IXf|XRQ8ZctaGCzjYPH+W=)0ynYg|iIPZ_`GvfLNOxp~ zDy{sFV&3irflZi4TnMhfR5IPe+1Y0p=XVjANM#(#N7u#Suy+*0^YtM-2!CrSex6Bx z2g><2J;EI&q*~%V2Tc?!KSEDT{tmpM2K>t&Lq|Qtxw~A75ii|FIazK+vb>wR$r;}e zU7Qo{;@8vlEe%%I1aeOj0LRogoz*w_O`2`8h*2PXlG+vC6G@Dt4*aR&zEa z)l^5`@2HFHjlY6Xo^Sxamq>Vc(A}$?bsK=18x*bU?RHXyHA7^2T;C%j)%V;yQQr<+ z8j1N66ROvjJ3@T{cDEblKik;X2xO0cy^=JD|r8p$5m7zqdlYFumJ=VRjdSv zvW4#gLY~udjneV{v%6?1BGiG@^4{2tuAZiDg}OCx|KDS!q4lduL~4$&{v8|Pf522u z7?-_p5P&eES1!0s!}-e2TgOKFJQ4E6&eO;ji^|2zxOB*>soql z7%L~Z+y!)Slq-Kb!EO-b>1H}2rn99)pkp3D!Q@g#5m7!Er#yEPC_Yo@b@>GDJHH+! zQBL5Y!Et@laepCg`0OY|jNYTvtSGK5K%pDI0J4jaIqKfnwf?Ufx&N})Bp(p`t6gvYN2OZF!DZPYO;=LKv~c`w%DN_Ta$Yey>AXOlda* zQst%%QShn8Pna>6_JMkuc@tD(Lf$5~ckr<`RyaI?`KmWjzG<%9%;`gcpZ;x5iB6E4 za--U#Y4yF9lLU~A&*-XQ_COT?!c#`$DhaAia2eEcd_h8WR2J$5yJ6brJXSx(-Nq|X zbsQ^Ul9!-P+4yXAO>~#|SY6^Qr*t1l8r>b$0d>bcx<}n{%riygP~UrhroLC{n;h9Dy#4R@ zTrS_+KN{9v9*5a?D&##wZfBaO)}TCepON=wd9h2>qs%@Y8||S;#1AO12t122uh>B~ zjG^QDlA*_Sih5kzTxSY`bjN>f_8k`kEpQQy0=gV0u1&nJw=c+lgWs|QjOZiVsjHISa zgd;;=ufh3w{VonHqRl_QrUBKkN*rzDMuKO_ld3vwiQ7kD!k!WJZcsiCQJ?URD23h& z#&v4mUp^T*pTzmpVe9QBKx92XbY#d5knefF^9HB_%1uY;hsQK!b1d6msU7a)WELPb zAtw+(B!m%idgYQ~{{P(2&sQF=5;9Dqg9&Rh)9w@KXjt@Hj<#WpJBuai7>-M&3uZfhJzL-CJM_T3_) zsa+>|mA-hMgM6d-GK()pgHb9sBh@E0i~1V>5B0$xl9~6Ct5z!4MFGE5CPN)zxPu{e zKaO@979_}dO-3BO5Z-Ca&ioqnprRp8&euF)yW9tXc&+`DIUh7ZWv)SWiI6a4+mkwaFT!nL z`;#iOf9a5+)Ssg==HubAwMfmVEWFbwnW!>a47=ntmjz@#ROffbVtvL*p5(yukGO7- zAbI^onL`rV8aHpYN+N*H%|n#00T06QPWGMYFcxjNJ#ceo;Bhpj>DPE6Uz-`_r;dAg zwc5kW#7K84=lygfpZgq;M9F_!okXSpt-d}3f^hYuIqyube^Be2QSWcWkrlQtUw;Yv zAG3yi2KllKW##e!VYYdu!8pcI8C@;M;hh8OdO)7)ZK}Bq%0~rqZ@^{XhZ#x9&g`+}c{0>P=N6|i&&YywLeoEWTOMa$rkV2w#`ontWo9EzQ-RrJgA zQxUV|`Y1u|`z6^7>I!`msPioX?S|1BB@Ne{!-AE(x*Cl)&9R0;--c?2I{ZbvAs$j; z*b0GG?!0h9a%jJ+TycC93c1^!2DA6lM{HvaL;ZtSHd4}|QirWS$tA8)*pNf{$IpZd zVmAG_cK*{$k(my~&BHHbsE)`WS2?GiIvk7-#^s4V(URmj(hlMJCsvF`9|lZu6MhYU{zJyW>9o`cvhcdNbDHX{3{KJ;jYGzRg?fwz zhw^}^ksl6T%MH=fAz?@3P|lBh9og8~u%st29LE#p8Qnig%-jV?S7&dmJS{o2k7ln- z(P!@?g=dcg7}z2>Yn5JTGDY!69N|d>U6a2q33R~BLg_mb2dp;g(A(xX{92qQlwP{}_=vOJ1E2UqR{Hu~`s6x*z&3Brhs|CbqY5pJj&6v;9{Bsj`u;9=N2kgZ-zO?QK zD~DaSVfK-DI$J@9scmK#c~-#AbdrYBQ1L+v>_}VxOI;3@6aJ!KHoo*%V$dq z>t$&6uBumMe^d|TZ9zn5Pu8Rq@$?Aa*;8>iq-d!Lct!`O0yEkpl0}E70?{}`a@-#i z@QeXW1)$MVhS&zmbgMB!VyfBp4fuK9WTFg^VKznw+5Qo`?Tf;;+m(gUd5!`-|UH;Aptp>HmvVbukNhlT!MV*=*|MP2>B#=Lo`&3W`! zM}Ho8!wdFs0GMtyo&m)C$*+Gs<#qp3vD1X(1z720Y(?*I1MnLr>8B8(cSE6{SaGpj_5RvtQP1=I9H@iTx(FD0!tn9 zc<9DrY!7-&=SOpKzf>f^OLgj?;(0bLgfpRLM(R9aonb&A1cYT2l?UU}Xf0C62A2YQ_Alm+vrFo+2?Qz(Qdd z7DQ;JgWDM<7#=W2g}dj{+T}%wE7cycj{QlMq8ncbfk&30prB&3ND++|gc}VAM?tus z5VRB=vx_BcpGGX?p!rb0`d^TUXy&b~;H^dr3ZvzzuZCQjs`vS!ggZvb##i5;m1P|q zQ{gTkyso_7!%RKPDuobWkFwx54#}NMZ{3;Ca6%mt(vV;m39~4nLiob$>``SUHt{N# zUeyeVUm!OXjBp>+5|Tfy&vb6Wy9H@-Ybnd%94EORN>y{5@ACPIeMV|Q|~`k;_u zaG-2%&HNGoUi9E!5j_}sJ%RCjopC&SJx0=S6}S-_gmX4sp1erZw-#66RA(`<9cALC zfy>k%o;@ofoHOLnjhn=k?Zt9ZrOINaxsTEmGbe0w?|KGo(&PhtF(vMlSqcgHqxULW zhYg29A>m^_mBR^OHt)_wc<1Q}35Ww>;IWPc9Mz#q97+LUyfG?zXNa za5QvJ)^q_YG69#hMh+`^`)W?hLPirZ_IHh(@u#P${UmnF!>1`Uo~2FUa396x2oS7q z1CCE!>M**BmCHkX*BR zH$H^MsU7`K?ouWAR)7(guHRKNi|-)_9pHPrV8RYp}hzp8%&AlVbaU#oX~5j9^@uIxV$RbNK9zWGDz)5mya=kTHJR!)4N z7rj<38a=eg`#74*mi^@w-ZSJTRYWzz*#u5&Fp)MaJ!UzlnLQaoT$Kjo2Wje~`KLlI zW$!E6_CIU5T&kZ@i%HmJ={U@HG&iV-rb=*$mvNw_%hG%c^n#iMe>c+r@DvOls1i(8 z9hR|<`MU@a#Edjc`{xjG&q%i{`AC(EzbB1AJ*pBN_`*Ec9VjIxbGzUJt;}f>Curjz zsX>X^Q5z^}{7CM1<9@!5onAVMLF-c`8ei*Bx!F{emrYYS*>shU&1Ci)$MRgq9Fpx-mQTJD+U z+mgH4E`@*TKY0E*)|*%XbiE|lUDp3@Ia~0nTGHvkuKxE_BY{l%{V!%%clK{)FQ6RS zSA_AkG`}i^7w%a}XUzX5$dMER_FP)NoI}KZM)}Wg{`bLHY4GC4jSf4F zR`S|uhfa|1hJ*Fm5UaoE5-wp@vx39qsX^qu7sxXUU=x?pU?M``~U0t22rHauRoSN*Md+Tevfs5I-Yao8EmUDdX$kvw)^NDg}M|a zWi2PV5fqMDssXtZDN~l4>WB(+Hgyt}QGcG?p$H z-!Jf8Bm}IBGy$t`13dB&r}iX3snI@xeMpS*Wl5Hnw$9Kv}u}At*dqnh~b8 z@wFBZN4<-h>C0egW}mP(%ymD8qqO+a5p7TA=V-n_ekrd%4_RYf*uvn z#N9d@!9fw#NF_x%vV_D-zKXIS6lkfMZg(%G6vV_J{M5UMJI1F4wI#!ahuAzB`%PvPj?((>TYtz^b! zg54QsrSFBwh##{z9(}ljem*`8A~A2lU~N>9T~~?whkf+jFvUG@tuQ=!GHt}pzkZFj zOb)8D_c|h9+^)5_t+@Cd)(x!Hohkq34I;$&w73TWWe_gQF2&=`>+OLC4HN% z{`;^P0&1i%lM%H7`$t@XnUXPb1=Q-J@9#`}3Bp?7G}r$GNEWCd`P!5bZO8tdt&R29 z74oJ?nCzRYOnD&^Cif}_Zu!5!WKC}?LnLi~s;oySrTe<)y4Rs!7fu;*4Q|x?HBl76 z{l^K`Q`G87a%)Z18VK)g;zqm~qBVn@v?jp!th!Mb-yU5%8P@DpnE z9kq_2;#NWHv0d&DJ5z+|r7#u!;;K|BU~5wEYSP2P(w!|pT) z*_~(!Ex2ApS=poPzKCb`EV1G)xBJ<2Ynk%vT@1nl*j%deg?KSjeendyG{XD!W88Lr z@sC|X4KpCL`CgI&#+N)d$tElzno{Oo6xI#-BQ4=;tQ(^CV#Y-ys5Vr%+$Ywe?lX6Z z6Df;8o=$Fma>J$0uRK2rD}_RUS{p-)0QPy5=z~AyS#T(nzka`by&%=D2dT#M+VU9g z`$7GVUpk9e%0EK)hQ&`wM`+#Iu1ZdKAEow-+y~bJ`_5CQJ_inyUriA5I}Qx&Vs*xB zO`n6;S2nMOilB1oKZ);&+pr5;!y*@@yUN{NKoTrq0Q2>E{T{VSHHJC85(K2GL$v`x zh?V06MCBD6f%EKr4oy8@UgyZ9w?yTn)0ekOL;$U2m;D^P1eV_%{Zq2D*&9dRRtEMv zq*^;_vnq$^w>-X+I#68B3A>PxG)Lu@Mf6&}cG73NQvf`%&`n#=xoCM(SUzHDo`EB9 z6Wg~~TCTw%S(fo}CmDiYcOjxuxf@<}Z9o>qE0^X|_Q! zI!c?}klc!5LLy=FZ|q%xO4jtQqA$ZrymFr)fJE5L&n_4dnI&JTA&s--Rx~c^O7d@P zT}4lZl}O`WfG@p7?T2^KfQ;T9VQ8PIrh5L^`QP{F=NF8K#M(0doBp6(w)Z@y^+z-e zQ(IMA#l7)aXv0mL3vD=4&Fop#J@O_LR?P;-R?y2z_bSR&ge=xx;Vd&_ge9D6x{2~Zt- z{s=h4z2P~`@f}ZUI%XD80`N8f8MRuEvehwy%9;ByE}cEJG>kI_4ol(EiyTADp}F)b zF8%EjLrNnE=D@0m(m!#G)bma*{iqW29vcEHVq<)I* zC0cgN(yq2e*rUGJxIs$`m=?E3RIy_6h)}%wa?SGlQVkpnpVPomb^oMDPqBWbORH;b zu|sm_h~If5*ITRAyHKy!Hmu&q)~NNWbyXHSbd#YX7O}M)U?;JGe6yw}Y+}u|d_;L? zCE;G;3Y>D!NcwBed{aXr{8!L?Z=fTf+$irH;-CLI2&HD-Oq|QW4R1vG`>i!8pIxf$ z@G*T7v}XG_Vx`2qN^QvflbT`r-hD(OVoOF>^LEdK5+a?e zjd~Cra~Ay6&yw?v{!4p}sqCYyCQ3Cf!oS;FBl~x&Ahz5LKbP6nY3LvN)nJ$Nvt;uI z=T3r*v2qNEIC*B4KjkGzVH(ovkI;MJ;N$i*p;p}E?D1D;`K#0X)d-`WjCU2{ou1Ct zr^(oeO5xcKDKOL8OV2UNWr#eHZ)VS2$(m=bv($=loK~Ct=lZLSHK%tFN8_8aWd!3Q z;4Oc>!{ko?GTB<2SnCKZng^w0YAKaFvf?tL6W7CG80JCVgC4~xPo1TA&!}`xYOnHZ`swWRo<;C2de>`9gu!eLrSoYz0Cm|xgSUHi!JaIud~F^+#~0Ek zL_RcWIRcG$q1AgS}Wa z$iC8J9q#=-5owb0K9mS-@e@!fDQXP_}s!5-ppm{7eioV2;qWQ^B7jJ^} zhK~B*r8g7=Ho<^DuyS1-eX{&Du%z*LC?qQf@Buh+-< z3#!QS;<844pwDqaYm+|G)tu0Aw_c=E6!Gar7E(f>wOKE588sW&w3eP<#U5w>(g|D^ z-j*-_N}uroLAa^we|(f`3@VzrKOR1-cEw#oP{BF;Pj}IS|LHAImOmR4unf{8(TBgF zbSnfJqBfapYt%RVUB|i8&3b_yoWKmaDDe^}bga;eJccTkE!RtAaT9HNfmCjyUC*D) z`8)OeCeFW9&wt`eXUH>JYz*_Ikd+6x4;V!!w(li~P6aS=d(#_(y#mYxMl%#G-7}3;dM}bm;}I;0Ctn z1ya*TVK#3~8_C=>`;`Wv+XRzIo+d7{yY>? zndNHlOmt0tomqKf?HDlrbmo}ZTurd@Odl%e_BbGmI|$Vj2nSBR9?iYm9L&6{jJoPi z@r@DA?{qr9wSe;S0G)Fwk?v!o+|#B}ic8rthTYTJ**$Fm+|#C5nsFi^V9;sY(yX9n1b(9T;h>l8BKL zz%Z3qpizlm{7Cq3t)9Ks0ROG&I!NU7$Vs`+*}!5nt#u9!{)Ck+oC@L?T-%dIYW)v->sNFQZ=K~!V@pbiYpUX_r-00&F11(k zc+PMIWpEch4_Q~0QudH0>w=_9jz_Tt8F~?yJN0=8LayWfnzjCNVDxV+qQ40PH{d>W zJQtZ}5_KylS5DSi9?Mn3ol``;R<8Gh^`hQaLD0CR=UK8`bl56_Bo&4$rQ~l+WA!YT zYFRFFA7MGtgX^OvG!Y%VS?%DcfhSIdLc(dyKh|qbYY-rbUXB(XYaYxEd#pinj3Qq* zRwAg1a#f>?=;!bz5vV0u$(T*rG}Yml=~ZmL``#W^vD~2yodI{Vzh_X+n6MDbTNTw*h+Dt#7}IQZ>pQ_kf+- z)I{8mC~zsOyfa8&tvKP_ZYo>uZ_R=*{KN~qNYw+Ey+3N0{Z!*ipm=bb3p+DQ^9!i8 zX`Xsj^mhoNo9b9>&e#WplzP!7@B^rB-)54n{s}D~%imdA{umNY#T#D&El2%?+r#h(-ijE(j_?RxQ|YtQ^?zv$_uu5~ z6A`UYW&w&XQS~9Td(Ab|{d}#pmcAP-RI)ZkWqRHm{C{V_KT!u99^?LqG4_VX_{#UJ z&pN|@IX=$}F~0dT)bTAg{QqctHPyXM44b`8yt1=lDo60@i-K3S=xBP(EUw!g;F)*? zw!qCD;hFG7z-v0@BW>t>JRhM&u(x+(Nq4R%oCT6bUy~V#_YC=8|!HzBg^*mhm38UX0bG##x>$E8<#K9jxrm@5aSfK{=KvX0%X}ktCD^?R#j$pj z=9-qHu~(G1H>?AN_{vjQFB{(m(OlB5g&DZkTqBx;&tc)nJi>vK?`h&D>m7NJ8Aa)l zz^8~mS;wQeCXr>GcVPtcj4j7Kx0MG1EZB3s zLDVfU$3z}Nd5JrVhj3yI4`HxM(6XbR(rDR2mR)8UJZD!BE&KYFW8o)o8*{bFd{tiK{6E(L}$gnlFV%Gx?2 zznSrI`72;IE96gEwpvGfv5xr7x?%ig9r2r`5(ikdJHHeBX4QYmZ*E;^t57Oz#6Q*% z^G9{l-w*Pex#J`G&AMoQvrp$Y`$ph5zk@&i566Z1%_`zI-wsU<^P5$UGCAQyte%3{2Wlv)F9ylOejGdBM9-?Crz!B|L_i6w$I%3sS$+*WS-(Z@BQ z4Kva|GlZuuPPzWnv8 z_dHQbHm>y>7mMFDea5vMg$+1~iu+E4JQ?zRcqQ~(&38NZ&{B8P3-4!Y8NYe0mS5f> z+n)8?n#3EmvKsBJ7LOFuHJWFu$1_>X*5_qhKjtfq=4(KmuYX{^X3L9siWc!umgMTc zCvTQBn=oD1==)Khud%YLprlxlWo@R;6}8Aqc&2(qN$(!SBVkJ-QF4gaVm4%HIZhq+ z{5VRP^`MK;I(;9Z?}ZMbHRkADf|q--M7bQBB#K}|5@3-{2p4dEN!T?LArvefU?Bp}<2_T%6XeC34|C{Q zm4)`RDZMT53OX6gHNHyZARE^Qb4{=@3WXNJJV3U<4jyd&5M5nM`@K@w8&NAVBsiOS z4lT`qFxAzp_jjsq1VcD38r1yz?W(bLa{nBo>L4Sfe~yW*HuCt<&ZZdMhj#=wAKn?< zet7f2ZEU9i%k{Ja`9{r-B2kGSlZwlESW<`^6idtX?0|^-BRBDE(+Q<(E&rLdmM>D@ ze1BXf=NQjbiB1YzM32rZ*8bRQj=>7!K6+3dm>8(tq@FFIuPvu z=~Lp`uv6 z0y=NR%FKxxfsi9kxe+R3YUA@0qO!QbXCvuh_CW*1Ib7>oTH2V2Jpv7UqE!8y>VLYQ zKM&I9V6C$i-=3d4TRD}V5KszF!!;0Ese67_84$3=RW+O+VQ;I(qwnCBggye-RsU~! zCB9{D!P#$Vfg~jRY4iRsp}UCJq71h%#mUe>Ndki<5frV zKh7R`)BFF$cwQVe(s+IpIi5u$jAcO3;WeToH8Q7)e9U4~UQRL$GZ-$HpXHu(5e?9g zEZ@UL>K*3nYdq%wy>2!-d+R@QD-5U$A7VVk2YpKywc5Mj-+}UwbF=>*RI=a$Ii%L$ zx!V}9-3@emg|msf_<^vX6>ipX54f4&E#rW{I$bDP@OdbQgnotcD1dRVvJ!z4`_gbl z|AD|?tx&}%A^wOK-YpRV2(X-^q)3^jr!0enp*B>kT*kob2(Wm;zmg>sDXj|FHpH2LEH-AE`q>QD`)Mf@+p-(_P*gp!Dn}xZQf93mJK_!2# zcFwdnN3jl?9&>=1X84H^T&pQecx2^w{3EmCc-MK7ZEpXCr^M@n<`KcJe1Q z0hBkEplNDPwBDBNUkOg7PY7nvm6ciCIvX0WMZ8cv`zB6}ELF;{3;45;GtCv5&XEgF z2Iik+)vuyjyGvP1Q&pjKYX#?WkxKro;!lu|!lSi+WgXiKd@)%HeiB@U`*7dhP;lj1 z&H-nH!MHDGq+5e4*TG2~yPxoH!ZmwvCDaZ4o7e~ZN_c&4TM4C#wv`+C_dXu`Do$Ys z(kmb2&y}1GU4~v(U@*|iaZ(pow1q#R7ob?FQ#> z=CUXGQ$ndH5&)OEfZc63P6OmDM+(laK*zSgG{A0af(oc^A~=-Fdc11H&l~F{weYed zG5g(vKp!NH22-|fC$H0$4>2k=+1lJe z8sUxDA$Q)@-!FsMXdHUyJ(=%={+s*mU<@cUSypa@2h{Av9No+rFNNFG?UToI?ph;+%_tsWHqVM0*Ltd{RbB~q{dg7yV+o*EZ(>cU%Uvj z5y-LC;v-OR=DDTQbIJ#aCeqkv^a?kUoFB=@*=PjZj<}{7g+#&=FpNtRrtrW zo#Ydc@r5iMU>Mx{ME-%}Bj&fnX==GHT0a-g1^8rLHx}5}opKWk5YX)F`v7$}Mk572T|xUw-3mCZA%K1%5v`PK4KlB?Wp=Ss7=Qb=@N zZ~_`nT(keibXOq2&RuW#ORN+nVY}u(lOrT-DMvLi0Bb;$zY&ppn!JA&@P3S>QGyZL z7Q-BX68_2U9nPOr0snlg@+U#slLdr>3h6JG{_>)5PbH<)E2V>sdkURjQolUFJq4EL zRRZ_qo(A{iS(+O_(r;Zjv0Nz$UC5_RL^FfGU=)e0Yl6;?dCvT-4h+%KncS3At1xR7t-*G2-|42!Cm`^|4C%M=Iv! z(HjNk{p$~4UQ2VMKKim#+UWIm27mee2-ryz7A*umkL6+CkAKDZzl$2bK*IY6qLHwB z#d?^>8firH)(GpmpP_`6}VLiuyCGNZ5+E<4IRjK?%E3$`+K)QqGiSxyVA=!#3ps( zw=af5a;)sKmO(2re-#!re9pm$+OFE^(1OCgRl^ys|js*5Xxy523;aW1ctA z51RoIC;nsbu5CWxITxF6L9(2L9^;sH7C9D-0^U_tBA7O{4}aVQDD0iVW@)Y;O`RDg z^^=v=C;lW15-iO>hX#8)yU#1a8Qs*)j$CiNjUBl{bxCvND)hwJuNl0wG&`VI&p}TM z`r94*gw(PyI9&)Yrwg~6(}kN&PIQRGRFRk_5?vy(R3ui4#43?EMN&0n^C?}gt&|-;x^zd5`(rhSkEMFrrdZA4 zW2K(Pi3RBvUZorYG$|(vHLb3ikB>`|!@g1MCLW%C0^|R91`pJg)w@1MgP;}}qfg-vJEsPg%kK zDdYR|&FU1Wji`Ph4O&B04o@+Sms{)4Hx?nR8hZf*dZ3>7=i9g$NaTXTr=aRD*hP=c z;&BddoMz6X?vP6Pay2m`&jo$Kt1$38^ zXvGEa=+@kb5?pkv?n8M)ui$>tTF^!oBkBL0DS8m}^oV$B7Eh{U+BWgDM$%ngs!noM zcbBS{T-D*D>L?e!1OQtDfB!BNI`mH-1^*{(WvsB387&Y*%XmZve>^9G~$5PI+L-B@>JsTVwHTEb-BWjhbQAKXs+?A@XJ%;3FZ(5x>V{i&gahytX) z6vobR$7XO!8vVhCmqYj`5o1`bq6XE&Bs6F}#NdMZu@|}FLUS-le=O7;r^@$X!fE9B zcHwU-MfW##f;N=VH~ zkXG+orn=ILQ?mYUAXqiuua+1s%Pd=a{cVs5secmXyrWmA__&C{>ySA*Z7^>6pVC>A z-39j^W$xt@vW1%=c0is~^(VE)pUUG?o)Fc@Nn8HJE;m5A%VSj!VOIIxO2i}na~C4q zeYJ0Lw+kCI7aIzf7BahF>iQy4THUE0;hxcI>I3Ux&(eM`+p^IVjnCNcI}u}9x|zsj zL_{o8Oz#_|)m<%Wxm!6NWlL9mhHMrwwzhjxhsFm9I|51)(JQsvxGs4-YEswc_YOIm z5Dd0WvFZwb^l!9{?~-S@$}taLw5ap&oByXxbE8*ibc@>J{q8WT0c5$#Li<;zEuv1& zceI4$D^Ij$S-tVT2I@dv#`b_Oi%7MXnZKIrkbLt`N}gC)2xqC^^NrdMllr5}>ifvx z{fC@S)7vn?xvk+0!g5=fJYUczd~=Lz33)6r%npQ6u60poL)=4X%;=~vX!49g^0X78Mt&jKy5(ZU3Inz@X1t&-f!)M@p? zCu4BYE}D$~q0o7e9}h2!5Zm_QLwhki zv~18)e-mW1E!cWww1zGY#ihD=b1}96`B{t36a-t}AFa}#h5a4MXzrp5p6TTmQ4VhM zbpR=K!#x@z^0|S$$~EwMhPxCzc929&L^6-fHdwY5e~vCS^M=bJMQID6n>1al+ii%yz z_}{RU5x%mLa2_r+;(uC)3gv-=p%C?m1sCWr?4aNmA{_8Sm(udx<<$j{_c;H-HKGr*ex@W!(L9rtvlzkoE zbHM1|nHz4D%gq5J+&D_H`$qC%?H#ShH}IPJI8glHSb3@u7*W-q+2&8geeZSk&u1JU zhB4ph+-7MxCPox{g2r?X8|h;K7915Ewl+qvw$2llW}q17*D@uaew4R-JZzMPA*0;$ z^vI*E7);RgwIYXWO#Xj1-sf5lz}oP!K^_zhKAx(V>l|4DGv6J?b0}fr$89Z|TE#y9 zSQw4hu?#KC(!PcWGdZ4JOcQkI(gHg|KNDq9JzI{j_OTBO-y-6rbkQ!DHB4&RbTlowYTC9fPC*rP(a^_G? z-U?rmNZ%10Z>Jx@@eck>=g&NRZV!%6<)=*k%;nEQd{UV-e#+v{9R4hz&%t;qwIjG) zPI|v1%bIL1Bh4U{+z@ad77 zm#!Z!qNE<5As2B7`ro9A55iUG*kBhPz(OqssSTRZchqz!7~Fmc#NqZsOPQiR{I$y0 zCm%|2DA#>;p`<+%$Dp5M!q5M~?R09{55K3jC+__edIF`yfq_(7);sCY!C>4r>YmDr z-IYP@(ZPt%zX(lZfI=! zkPpSpm(#8N+YeqcPkf*67Tb(gJ$*pD?RK5M z*ule_AKbwcRDXDTWaqlLdk7n%_lUMc7q74ek$*gkmH<<)P|01)A=3{WrGpKJf@1bd z0g9L(j?TUBe>;*L{hv_3=ogJ*H!(UQ?udB5BY32Lht~3Do^jDS{b&|H)aga^lr6ze zh%f<==qx7}+`^Gjb{rniLLY5K=KejL&?%-PCwN3}72b;lM|wz5VR%p5#2(3!qatjb zgM`M^5}lGB<3deM=e^LyW9SrJ(c&+0+Nv;v+pqG#g2Cq;c}y-91l8yr!tru4S9wIO zJRRLSDCjG-K5R;AK2>dpAKSG^OF8NQxvY*X(hu$o?v$sS)LzQ7ZOsSN$!-(FPw&^o z0FOoWdAUej9_He?+iJn-R+^740shozftj19Z#^Io#m6*c>`mcGtP`d=*f1Phh2xeLG2Uxbw@1r zA$4LA?n#6~IfzZNLtQ#TIa?_qoxZoLFwrIeFqe__QV|n`iX4JC65F|l^LGm|FrdcO zuZdg>`n5n+$^Duj7XY44Yv)q)q*9$K6c(sTsCG(HS2$S}^&5c&>_5`3FGw)&zzb^%v`FPr&DZ=IW$SRydVV}I!Vlh zoo9vhZTVMf)J)kA3ee&A2h}a0W;&?vV$R94Q>e~PyThi11}p(lK}F|b)~G0lhl$G)P*xC(%N04;4oZUFzmy7h(-j( z2(mo5^YGT-t|1_Wf``<|N!tZj9o|caZ*Oq-5Wu2yY*$c)PQq(DzJK0S%=dqpx$wrz ziQah0ei6fnXRZ*NkLDte1>mNJETGCd>^mx0Aac#a=A%Lgkw-+TxXx}qupvi8>ViB_ zw#o&@3M#_EcwOKR-5ZF8WjUK)+)`01K=(raqsfnf6sQ;*;1idQAU%ku13g>f)P*22 zi>s=IR|z-bE`eHCKKZrgH5P_?bBg8rIPgw0w}F5~A-MxObD|1K_NEkRDm!xVoLWOU zk;<8&{f9(b0&TM-4}$YFwG{|mgDH*@=)~m6u5>-1PXz%)ZoG&=LOjAr!A1Z?OvGO( z_Z%jEjQ}iO66(Av8-rLnn^UB)zQtW5a4v;rJ}&3dSBdCW#({GAQ|cA2mR$trYUDy( z=&x|NuUzq!W9Cymi@ucKypSlZE&%URiB_v9&3h(#Nt1yGw!m9TnFg){d@-Mb z&Yp&KF%c&V?ggsuSZG9H)Hxa4_1I{0;Z|Wj8b5gVWQ0A#P~kPFQu)ij65(OC4joL8 zn;mX5WK+5Krdvx6Zm%^2w=*;b3VDf>i-I%YGnTM;vFbP-)Ria&KPe(0Kwk-lXh}WvtEebwa~$ldHQ@dT zRk4^WmFr1yR1_;0e9C&`r8I+cv>7a3mnhXxRi%`m`}1qsQb~^yVVnt`{x%GrhWaZs z>;4KaE|c?<<+KgXK@W7g#yk@Wl_~uPgp_DzvgWwZT;D&NG71^SOX784W{ST$8Ip`@ zD;yUpQ-29*RdTd9!uA82I)GKr{XOmossEzu?t{IN_TDxTmq`gcKpXzrM)-Nz9m+W1 z{LXWc^IOlP2>oi^lmK3f<%Q|aZ@q*5OHg9JqO80H*l6* z{64sWv+Pk0yiTwhzj~CUOkFSvj-qPw5+(N*dab(qdtkR!a+|kO2LC@VR1JTHdmtgs z!o*Mb+Q<`M;@+?UihE+}p+~JamqC2!sSV6GIluMSJ8a%D?)D9sbi-}J_}|^UftBhE zZ^8US2pS(Us_`!rxhGD_7P`;WuBC-7v07cM`MfY?xK{L_8ydFqw}baPTk-6vbhh$a zeu53Rzrb!`Zq$1Gl5v0C3>0f6Gr7uogDzbt*z8n!OS|gFOSj zS^I7ienpux^uU}Utl)iS|Aiwfcn|(T9m4ONTjfoq|HU9a>>p_m=SBLrhCRcuu?*0r z995w#d0T9LPE@0w;lP491Ck8ctV_;P!U7}iMJ+8Sd5m)~nr|0Q9Ik9WaDXy6H{qwH zc`yIwmz+W);U^ZK+9Su3(dFwlTJG)os(*C1OR@B8kSEM#(je#boW&aN?jz+1){21J zSZgoyXDN+zibJHD&3({?s!wyjUbLb?e)l6m+89DDPqkNOG%vGfPhDJ+M zoJtelYrI$*X{J-^pMW?0mIE|b*Nce?+;!4bYcEl@^kc66@unuy=H5XREnn3AVO%Wt zL+!$-67=ASYVLtuo~Cs`p(?0x8JGKou$nWYA1>+ww{KtsrTqML9X+-D>f2DL#O+~R zr`&uG4v#7o*V;HrKvIc&u6XOj+YaoWBDV{8udj83YVo1e{FaWP97cwv_-W&xKlv@& zROb4&q(o`t*?S_-+Z+1A=WWwb=vS1K$!k~_0~TNhve~?(!gHqFiiqiDF6H%^s@Yp_ zp5qLI$4ErL8FEs)di)x3#fAsxPR}*LxXxdv3r}3WqoA#f!B0R^YkBa-QxZ|agp~P6 z*`u6UC`nmVx94oR1ATDrv^0N7<5i@`*|SQs0UK#P^dx~`v$LJZLVUc z)nrwBoELOz2o)2dkK8xI#wHat$^II%zs4Gvk?OBW>6_tTbDkOYHsTDwpw0&;`D={+ z8k2l$@&>Sg*)z<&&0J3mRlyw03~NKIe};KD8eK&_@y|#WIJCyxaE4skBFQ%;t2lFp zx&9L(z($oR)%yj)T*G=UZ8bT#TyqW*L*niejqw)YvC`(OqHQu)^>Q+Yrr+(ECU4a) z^sd>73qAG-?mJ;?FxRpxg}KT%vlw3I$h&c$9`?6=O^^38nx8877ca5W>l_fLxKb_d z6XJY!`+V|YaaW#e6pK+(;_4NzjY{WRSPv>%Z&{k>7^hB8tddGC`u2jLts*uFY07v? zqBR}oxgu)euKN-TSNS^6LYeyrlU)pW%H-d0!&Hyzk|Ea~=z#5%OEFbJp`c!D81b^$ zs$cQS%aGDW9bzAo#i4y!E8+Fca~3+_wS`N*3Cp))GnEu}XzZh>#O1F}S01Gm6gu7= zY`|8e{E1T&SLf{!K7b^Ilo&P*(5L4h0*XwgM{RG*a*E z=S09N6*By-s8mjBT*6v6!l;ZHKh>H{n391FBvzJ<$ds&2wEn#An#I`2wsg9-V{xHN zj{r`1rY^K!4Vrf8+q#|K^IMFPcIx{#t>901e}!VSrQ^rg@5JE}Dq5*7jbW(ojh~?Jx;CD0jsDm!n3Q&12nb+^2(&hD?dgbklI!%r`o}RV~3& zprX$xfKD1}EY)*8yT*@10q~0-fdU-LR~71!wYPIf`*n^KME8`JF|DRG@g5jZ%qw z-%EVBRWA;Ofa<-Y1$I$B{;LBr6zJAOoLx;@Aoo)BAJp^9-xtX*vOg$5k7w0(&ujyF~JCBFh689mt_69aT~CYsO_WRqV6ZN9_kmR@GIXFQHWjxS3i7^>NRYR`qj(#rDZu4{VQ7QN1+01~q_ zBknePqo+x|Kccv9bmaUTrVJqPB0Xgh0Ht`Q%e{!LASnFrfum&QnKFhWo4jWj7+KvX z#WBLWLGcp4r-S&uU33s111^AR(C3!sWjKNJ#R;s52Lk!2{_2#zd;!+^;S(4d6aH$G z{5$mo&NstbR4kR}<2N72Z~h3!?~kZef4+MBR%^%aOzrs1*N@+KcJPe0RpZ<}4wqVR zLc51%CC|Se6RSM6mz~AgWqx#i?!g4NeG&?lD}NP*#R?FZz`7nf4$4KPY*tr_Eniej zr(7G6C*V(i!CyaplD}^MpHL{U9#$JvWBKSxx+?JRrT)*=Jc~|+|;Y8(Ni6p ztsPp&Mo)E(5?k4NvR7gYzs7c78+lN)o#SO!Cj9KUJ6{HUJy1h)Es&lp?o9lfutpDK zGy4U7tGWa*2}E> z3lqwA=eNRIcw`FMf2-pMsbh3I;RmVXySM$9)NyW~*q${x@?JQap$(cV=ueyvMt=uy z(ZpTq7Tv^KR1lW`{s@F+s8{8H@di_(Pi>1!m_{i_yTzmjHbGcbg#G!P74`jwUmtpI z6zu+g^}IE5X=0Q=KgY$Y`a}X-qpf~{>#S2VnUu!o*kH=dL{O)CMgkswE&6pu*FatZ z0g<>@ErAoW*`Gl@O77ezfYiaR#n|f@L%^N~;Y6Q9WsC*764Yz>{nHQXaWf*`m$<(W z=CJ>{8^41DHEKkj1TYEu6%Rfz)bDG*fwxQD3(LN?Me&erDuxaKZJ601a-d~7g%~Z2zOCm z-^x+!=o*>Z(_wkcdqcVPd65pVOF)hFYsJKb=OZ9IcZz!c^PI>ZXm14%AS$_Lg-Fa^ zA$)v}kwp=By%cqGs4}kp*#ML9qPGSr80g4S!(*f>pZ*0A3M$p;7^zAVV2loHk^2pL zn=NefZ4I;8^pDeLi9V&!1Yf24<#gHP`Bq=of9sc1n*m%BqLUA^sUcTh9ogv5K)-4R zk*Ugi&qg-Po?u-LdA{A(mGXaXcv-zZ`hPuu*dUGiY)!`ObTsL+HI8TNZasf2o?Cr- z{?AdK-GqqzzGp|NP)|#`Rxh^k0UZQ?$k&ccALX0TfR-6I!=+31Rxky>h9z_99 z$-|GEahoC2&S9iSNSDDG$m1g6Kd=LD0=6d6jihJ77(oNeg8`04D|&YHX^bOJA4dw{ zV3}JVM;gY_TBqkv!80?pBL7L0U$#QefAT(_*KqxK?s!Dce-O{h9@F#h;{0p${M=s0 z#&G?dze~@*g7a_D^S5*T`}B3=`j&x*5&7%x<8^Sjl+7}`X(g21|69JDbSRd;gCJp= zb+7VQB1-A&l|GGTZ92;&3E^ZcFB*tcA(EtxLk%cv%QXY=@IhMmWYj@A8V70R(@_U$ zqH^_Wi2=`pc!iJSY?Rag|G?r^;qkwiR-kD|{1| zTN~Rexcpc)x#;-g7}cM?$dX5yi3888m0Y#4@cGMUEOCe)hm9U*f;OHHSwo&Si5Z+^aua zz>83-Ja9FUipGFYtHir1r7!7J2%gd$8;LJf#REp=*(_B)EF^+Gx;!v>#$>7afTbDQ z<-{=LW*lz`HDt(DjzV7#Zi*}AaYe<-dsk~N+RSht&|`OgRsY@~49NoLB#DAPI61bo zASN`E@!3T2TAJa8+xZ=+--dYkJ3kuOE^qz5TMKEECZMvmbgA8p^Q44tEy@D(jJ5W1 zf2wlZ7NU@QA+Tx*X#;}i;)cqXP7)AK+4d^X4P^)H$nSLW8Hs{QSB)gNJHlCn&~umwhS?M3tk&CcQkP{eRYl$gLjL}>q#@>(r2U?GxF{x z%*bai3nt7%t=2nAyFZAF^*9Rk_?Z%j(>-~NXew#fRJe*13tB5Tq8(yA486vQlK;Aa zC5e(0{DgWxzyD^lFmYa-Ms)mFQ>|cyEG?4~c98N>)UkKgAAh>*OnF%NCJ`Lp(&ab?g4Y7q4s3J64w));NdF=1 z@aTr&Qgp#mWo*%n*J4p!a&=rH_n*3=06F#DC$;0LR~4=E$zhwhg# zC;MO2@I!hSKjcK?ho^I+@WX#zj=&G;8h*Gnp20Xd<9PP-`&>6j4OeG`!uD9cgK&q8 zL$5T;7~4k&U-lImV2@D&n~T-IDZVZ3=f*P-+xgtQhN@1Zb5pAiy%|k?c{u&F$LHZV zQJ%I)8f}pS5nIGzY2KwBpAL0<*nenybiX)!jYp5XJ+63l#O;yRaPx@ULk^9+Kd$<5 z#QwPRrSCgG?R0)R^z+m4z2|4zNayGC8vqSk)@eH=4O2pYj-ltLozKrSSozZqai}}Q z{zE&&_rmbmGmpGOzIbKC9g@c9X9_DRgm=j4_QyBZM(mGkH~b&=$LjT%k>{S*_J=*Z zKhmQ2N5{2M`(y9=i2Y&L_Qy@J435|z>v?}n%{UO=AB)2KBiR_+L;GXWNdeSx^SYu4KMv|`}p%s`W*NdV16xr3fDu4(dHUxQJO6(LZsIQ0hUaq4hbw0oo^gPc5wvNSpI_0@BIi%{OcES!fJIN}I%Xp;~=@`Uyiu4?P_kK9%hAhh5vYjPS)pVHT__m5OaJt^PaKDE3C~)IR<$26DW#-D!9knm} zMJEJGe>S@PT)q9mq3sv`Z`;rKKeTVt+lPzStcX5XI_{@sC32tpkTARp2Fk?U5FNPb zr1D%Abs$02{~jw}O8HV5czs*bX+>_7T<)W|uM52%nTvWiLdJfP5nb;SUAMIS0Ra-= zj21(ffc|5_H@qwsT+`RP8)7FK2GSU`Xvd6zE34N6ly6-lH0f#A_@87%_#}hMlW&G` zRE#okhf$A?*B)2p3NEb3^{8`Aikc z(Be!L%Fz6BlX{kJQqR)w)!B`CQaeilVu&UYt*~232Wg2vOZmgIe2^XxBSl8sRHHdU z7eufe;PGza@eWglZqA5QhVFeTQW?53N<~*imWB1}GCr19@^Q$+alT_|`7M;8pH|6~ zR-H`gz^~Y^HC^cMpbH&Ir9|pNzrP0hy*9{NK&%6OZd^ zZ>%Gm7c=s_p1W4etC&n^+J@&B_iW|roTd$%)XT15&5}a7bhMGCvy6zqx;3o)Y?p7M z*}aJGQ!gWQUJ05zVs>ryPqchYlT)TDKQA~*yS+^5dlHk=|97GM{IR@ArAAU%^;x4v zp!!^f_Mf?Yvep=gkz-G?-m^xGO!BpA#7HuGNM>TB559njmOyxn)hGEPAw>&Vp`1$R z0p&#LtYi&|pOUI`_?-dlJTWre)lLw1!14y zbfi>oXbv^h+~qM<(EChygL`uU8w`2Rmy_CC=;zH2?^*iMF)fYjhkCZ9`MNmPU{7X6 ziHOK8C2a6Rh^5LgPnq}dWRCXixs#~H>|{?3u$PL8=rE~tuYg#P&{4iI;tfq@340fh z16J~0ti7$%%B6%$2u;>n1EnU1_LZxRD_{LpH0)g@FzjBXXt`%6MwfB07+qQ(0JZh5g<`ZGBh59Vb^^z8Cm zxYw@X$X{}Dt;sDd!HbP{)v=JLP~>l8G3j(Y|CQ0R$Mfomp4WN$yq0k13($47pSxD8 z{cQO;0i?T545GvePcH0rRA0e`Hm7iD8Hadl#*SLQ%sNLzki8G~lgPACKzQ zx~6N@uPbrqiWaZX`rSN=P_)jkd5ziUifBCn+`!Sa!IDJPh)r#)a-_@GMXSK4DPN^c zRypO=VfwcYq*j#FUQ$dLxZa@1X9KdV&Yp$}sJ|A78h%+o#NY|}>Oem3h2izPE~a60 ziFmmj)!)R`m(yVv{2Qa>%(j$t;Tbv);vp0h+95m=w_ebET`-c-Ws zz3W+b>45WdS~#=k%&h7hkF7@TWxQ_EtFrY7OyW9bcfrz&ywY=IKzp;emA$kEkd zRaGlBzyUlFrs039uG-XK#x;=KkW5RS5~FIX_T}^CP(wmrKEKY`&U$l0l@)%qI$Cvg zEbQ$fHCO*5lo`)orHK&CZPhvQ9#)TAWD*7%v;wJYH?o9vss|p?bXeK)LJWUP4~BpE zAytQ!)x8!ugz1wla$RiE2D>Fhubcx>0G+><=EF3y3ipbAumAKvSU{B=Qy4DF<_&E0GggiPmQgJVh4JwwwkUi%(!%lThfZVR7-0hE(#bw%kUst^ zD4SC&Ph$e7G(VdlDV9%7lJe27Mo~REY+160^2?j%iEvKNZ$&^K&!zIBlUqiqeR)z; zS>b;OY5s?f@#h!xsdQ?sv7-Ctt0^h)I1CKf4)*Rv)@ZAxshgprI7=(UldsvPzzmF5 z>C(s8r1p+wp^(tAxbcewNj3BO`IUr{Hl-yAJyTKT(;tqdQJX}Saz{E_Px)6OQ%!if z{VUVCEse0L?etibuAViJUg40cXGO0+?a$v1+n+_Mxw%Qsw`YVn>aq=U zweUePwg$Vu!RBvBmh-L7K7vjg#TuK%}e~I!FQ40Ik>~FC8 z8>j-4?oMHd2{~Z}-;X8FHRYQ8`0AG9a;uacBl#0N=en(5CRp9re{M=4ySKV80pB$cybR=~d!YzN3@c`tCt*-E}lyys0;Je;Lm)J6z*hS}MJ$7Ia& zM75@lHUw6zAyV95gSZVT6BYARBDp)}Q97O{C?|9AT-h;=pT{dX{Jubawkd1qh;gSs zae z@qX;^sk!nYOilh@PP1<{tbo$&;WFhf>d=jxXDEU|K8tdn0|rB1n6;8)Q1ZP8)Hc3g z6>Nn+k8*^$_w}4f$hqMGwT(wO&oIU9tZW#30qvp4Xl42DFsIwp=R_r$nip0^cpjx> z{lO>+10+0~6y-AH2a>>Zg5qws(lm2~GkuyTDbxJ=G@q-S-x@K^@#y3@t&`Bi)FxYb zFbiFSR;Y3>|IATaiB;ZfnJl^Ozmu%Bb|OKSK`WM*IYxPwHemQoex?h_2IT=t4m23U zefp8|berBMyK+y9+9$Rup)NJvubuQZtwS|^@%3xct*1X?<=vw9&o_feGdp+Eqa#KuQRuL$j(Zb)T_cz) z%rJW9IeQjbi%j$=6--S{^h6>9b*A!rI$3GT=y;?lhLR4pK)-V3V7EzB&mL zuvfWiszYk(v*r>i``B>jCp5J;YzmhGzA!?nm@Lj2Iz^PM3&IiDyM0>?z8<6RJ3Dj6?Q?NER^_nHmSvbm5Y@joVhZHvVb$&Y%Kr*pdM1G1 z{u~VF|J);pu2 zo)kngr#@qNs*An2I3yGu$LF+;P2P&KWsc~SL z9rT~W#)s#iwMY*k*IuYb0M^(zqOa~AMqhz{Q|_i+(HDP`W;&zmCEkA>$6(sr`UQU~ z{CO0g9mad`+r*zIIbGu4W_)&}^Y2XlOy$osPS3rEFUCy%);vzl;?EqU_MV2jJ6(Ul zZ85*eRjJIDjeXD1$!=PrQuf@=N^WN*x5K?+qS?j)&<=N~Ik~qhxwk9RIHyU^X+X|( zT23DIN*?t}iF0CpnCNAmR~8xNJ#<2ICZ4#Jm^h8|Rn~vXD9Sv7Uo@!PyZGDpS#DY<{>nhfLQ-0 zM(DrBX*WWbLhfjEA9(@#L^4UQJn6y9&?p5FXkY$2E0_v z^iUF*Zgla}Ct0xgnWK6sF(Rapi^E9mR>tA3RD^&{K2Hq+E9RA>z-Hb#S+ht+TC6he z58`f?pq9;~=GcXb2giav?$`7=1OThL9LO`oA7kUSp1W_iN}h3z*Zu)NyooRMQ}+hG zpzi1@V3qFF*H{HiofBG6JI$s$Sxn3Rp5gVKqt$2Q`V`x*LLpJ}1JN~?zNRUc^$2`O zXdXhojq5p7uje8??;Q6@wHNK{RU8;ahsnLL_btrIR1#>Hl?RsaWFA-)BbB?~r;-)Q zBzq|Io8wHDZ!y=r+2YE zz11Be#8IUDhoFk)*nv&H)R9VG#YHdU)s%h3fjpzbrOb|>EV z7XzR&i)c_yTXN`aPoIJ1kyBYZ*~909ufZsJ%PZW?;|LC}^h2T7-_|&ez0UgMnPOkCwQ|q^#XbM>geo81KuY@{P-4*AMaAy{1>W0-UA#VzDW54Qh)>lf944B#mZbt z@ju9H-h<5MJ;?0bgJ9>vE$8~SIBMze1g1Vfe=or^hW|mB+4>)ZaV*fvYJBK4{ z`L=~nh4a$|W8_9GiELa8W1sW2qUCB?ccEUiAgX9I{waa-Chq#+6$dDs*t%Fr*?Q-Q z5yA%-sV;Fm7dF0Di0HB2bKQ@0rwd-UM5&Hp?SY!=4LFQuZMKO(vP3v|vy76Wq(Gp1~Kj0@rH=Tt>nGdV$}F0@?89nBZms{jg9zS)SdX}wlOX0Eq+@LyrqVu`f z<=)i=jCaY|nEDi&5Q91)(bKH`BT zhq506|Ld9S{L(Xrj;|bf8=r;AwJ|u^L5I-xw5w-fCIq%bC}-1ms$!T-=c1{cDS$7} zquM9d+Kc(=!}IiG5iZ;Ur4DOxS!f?y#~6L5>~vBSmXx2xmV$~2;v0!ze{jbP)za?J z#HJ6iQ+$1QBg$B=$3FAXib6~ziuaWpL70}-6B>!jamc$VdGO-LAwk^5Uha-XH;ju4c;W+Q@{h?8c6zL<^R zJE%A@{Yzg#JuUm8(uIC`QZ#*&>?`CaxZ!av9|Lsce|V2lRabPOGC~Bj>q_OXx<4>$ zBQnMA@okiGQV>1C-=1^jQSvK1x&fm<+vmgRpjvDrFvbw6h`*V$%8i%}!af^dz0m#P zsFtq4?@fV?lW@+wgahXDM`;QkQ>Or$?Q48I1u{ll6PR|g+HdmtFbN^Q4{5X48fHac z*$t`K-}ye?-vnM}FfTUcmnrPpBTL?vae%=tg+1@5s5`}>0$mn>jezc2xXe~^C(-&Y zb>z`IqHjaRtR{QcC6w6g$U!1gxhYCA@@;e!Ajwq0NpX`Hn-l_@9Cv=(kSrIzAeGdH zinxxesSeug&adhf9V*u`>Jh+cb3xDi6z0(CF+11e__DP83DYlHcJ^30Aba+mh-pam zorv}Rk@ebLnr=)zZNP2Hsu}ET4*ho(4PGkWXq3yOA~hS%yn5OK+mt&gn@joENyf4+ zY5@arX|-vUl^~aUE#cp$K5Uo@>d4*V$Pzk3EOo?DJ);Y5DX|$@-=rM^Y^iC)# ziyFbLyCX(`jt`H=w%S7ki0|k;gf1dQIMO$O5VBl)sKPdsdAKx zP?W)3Xi`l>Y$S7NAICb#HFG#1GgYKU5zDpl4_i>;pyJ7J@z0UU``sqFZB08T;3S$V!ycG+S>U;Q&SD z4jwO}yovy5@(l$Cx69Y(BKRkV|IDm4ARu(Uk%QypARsgikK;JWSBN;&1PFN|s5XM~ z@{{~Fb*fn2*By$jz_0ETu`c;~BQaPT2^tmm*XIq1RhJD`Mo=7l4!8>H6|B+?C$a3J64DpxLNF`=1=eFJkL-hY(@S`HW8Aib2v z+ywJ4ZIy<$>!q6S&^k?Y^(m7F;5CcONmmjPV4TQx8#fQ(pI- zis{V_g%-wAHNCkpYE>T@ydTg$r<3BVe)$+T8fZB*>juepD)Hq+QTJPc(lZjBI~KH~ zyjt4VZQu$9-xl~_;4deJ>dOoclixNS1(PU5nQ~IIM&;HT+{b9Wp*I8Sl$LmnBM*G{ z#-|yRg!-BtShy^>H}E~UH_PXz)VI-bA%{LyfYj#>j10Ur%X2-Vj!kcoYo zK%DkoRX(_8mET%F))}{!z88+_U1R|PFlb+$HycruRjgU*kf)`G_R+EOV0YURWRY9p zTdB7d9+T(`VlRXL=~auguOFX~Zw0fcHS-ChmymCb?Av zNB2y3vWkG`T2bXy&ujMR%@1{>$umZ(*DmT!(dvCeIh@1#ZNIjKLRuRbP~Tmlkn-7` zqa@!ZT%SvDLXNw0=4^yhTIF9X7dJOE&*Q&*j%To~L3!C1lN&E4=#BjPb@?;Wgi zxMm38e5V7B$Px)S`{b>(0_nyfrci0d;U^+EY}o~W*~JhY{boO;ORhkBB1@KL5;G9Z z@@~kQLkHrEcM?5NE`J)JRzM%bQ$9Mo&C2dGn3^h>!<9HP@$x5^@ym3Y2O>uhO5R$O zae&_9WCxAP&h!Pzkgoi8GPFNEw^V=vo1+@M<~hvHZcup(*^dj&Q*k{L1Qqfgkaj4^ z;ApOlL&VA7^mI-e+p6i8#P0cwoM6lT!I{D3r0?-v&m*-_|Kb3K=oTnI^@1 z7A~7uw3W|0T9ff${vZP|m1TVFJ7Iv9X$#J!?B5wZf2<&Wp}{E6FgdR`HT>53o!6KV z8k{UILPO52{(Pg?=)ZI@-v1cd^gkm$@lqZSXH+VST`X7Nj~1wezDtb7ZqsIBxaFyH zXrX7C12aw%37u~PpX|>!Q^VfHHU1~LvM0H+e4}%#r_R5;Q#|;6JkV;R;4p~h^zIXR zw#z%^B2%`@)L@qzMJK(V$`kyLbAfxf$}yspzlp2vL|XN1^&QV0zdcv4ehB@=bHy>N zZ5hXDKbO}^@=oQ0GeaSl+e5=rrhImu>Sfzv)fZJ=w;eyXe!>@eNjZ)4n7T^a9_)&I`W+^%sTM(u;5aw7<9eSwUmU zH_opYUZl3t?HTP-#)=jwLkG9e!7X&4(BN&(6Q2K(pMFzs6GmOzlGFefLzJ-JrHcGq zvG5qj?KPaE>MyiVzqlSX0195#D=i;GeWBy``W=bPFPwb~eiUsV$YuMCO{^n8<@@T7 zW`u~he3~1%ENrTnS1VPN5JU=}UTk;dwW>|$u%`5P^{R6V;U z>KZ+aZv_A6y%13z2#c7M+A)k`fOmWw*>fckwdco3%trvFXp35=ihp8{Ik~eHmUf)e zFj3=`fpNa2gM}`Azdc>9?JJE$^VUyj@=T->v2N-;cDG_OQPRn{R~W91wx7 zUz;bEbA{kNwH2KdDZdzDJ)w5yXT4c5Bwx;BAxq0%Dbf&?R<{hRdm^LDdBPj9>}g1L zx$8G;($cd`EG-o=>r(|2rgk~)aU!3y~N0g?M7|vMYx@+d4NfhitA;?Hi1b$AD!|*P38?=l8cUc8o)nf7p~bw6}B7h@s#d}Zbvn^_(IZQ|;gC%~%wJ#m=)Aod642X`m&tg$#i^rZW}q+0vv zw}ssD7sI+^9 zpF7jnZ3M5;FTd@^9ZUCnqXNxW@GXiOBzzW(m4V9k^I-V(E2{e-To= zCjUdY<08syIZwm=JYDzmbS;5mV;~DAhM+CC^&rD z+v_+uZiO7IH4(725Hwm|JVCUX@5BVlqId!w%ZrVj?;EWlM@t=jxwCrGttHCGF`B+p z(-u@j1e3UzYiBp;MO4){=Q`9hnjNUZ4N!tF6kjV@mTuM%93O1$?3cSJAOrM@T}tlT z5P^qluC-I?L9>H($>bBt5&GHEMWxYm%b&a3XpQMCFjo*raUIGh2cv6{MF?QE3FWfi z4|%3H<**F?= z=fRx-k*yNTqK(Vyn*wrAg5H{ zHDUwjn~BG%(|9(01CN#OJpyP2`$nzPpV1Y)eY4*Ae{uVcj@rJE_cZSb>q})#59>?4 zUdxA8#(_;D%*5Az4{I&ycF$2NQtAE`j!G)6d{aDGLQdJ^aIl(`i&71$tu>ugr%{ng z5hmDh$Fk5?u6*$;q66z3shB``X&bCzEvXdNl1d(~C3Wc6tR!1HUv$0uxL%IT&SGUhG${q~Btu+c}mlAx6xGa_CG)m`E+otVL{Pymys=A!G`_ z*=AzPc3bzq<6;N9|7~`{~p1c9uwfK<5U& z)474I;0CV#rRt!j)>dw&@Zv}$Rd4_A@fR9tf+Dci`n#?mqe=>oO1Y_~KQ z#dB>!Pio+LdJ-pMo}&<|;BQZ)UcvMCn0OW0Lws%EHX6}ibAEq)* zTk!;?N=5kMwRC(KCvM?x_MM2ev;^ZA;tCkzhFqYXk>+6c(uKp`oR~87YVED(_+o0n zFY4PCQJ+DJ4y|3^y^PX|ieR?4+*TwxSz3(&hj|E(xziSD_| zO7w#3U#5cRXa&EH4Hx`4HeB#s`Yn%Ne)sKG5Cud}Vs3U(A_A$~BI~>3MXGOiEY09U zOls^@whq!L6=L@TeD(zR?780P?DmWm(meSFqevZ$@lU}l`G0FvZdF_NPnVQm#G>2# z_*I!vDWymMbd#V)INUZoE?6}MDxcCo{6;L(BGb*5=3MHGAV<=f94V_%V*2^i1T)kp z4jop!eC4Lel6nZm4xAq==`_lX<|5x#E4G4(W^mw>7$81E^e#GXO5x%P9)+TgxH(i0 zj(|92LLvKnD0P0PRvnLmx*Bl7Y0kJp+VJ`rSD{$v@EKRhu8Zm%uEAM+#uf4zS1|mH z`+5$aah2K`SI}VZ6rq$#!=aQ)!{=K8oo^v{ANYF>qrunI^R3cf?{F*k-oWQuC7*Bg zjybg&I#EYS^x`-cTo)7x$Zm47H=qJ_$fJ}wGg+s#QcW-^KfZx=T1(mGBa&h)&C75a zDd&qG473^8zN&mb9dz{u6+kNu;B21}cSZ!tmgb)!Akm_e6A5LOYq_p42!M$vfGKqp zDCZdj(qmjVo*ptj<`d7>FeR|UQ4lBOcS5;}ia&n_0a(?V0^e3cQx6iIbq-to5tfCP zvfR7?a9>bFgtmbCXI~+x2NTE5KQD!M(ch);F8Yh4Z9lcxPd8DqRg#Ju>ghFYl&+SK zEgfvPD(*$In=WYi-+XAa{(=4y2aMKY`g{H(qjkCZeTx1j4H~U2^tXxr zcKt{Em3(5fetgtut@$LJ)V<>;5yi~ofnEv%-SgG`YzFog( z_f*==DuQk<+szOIzk#bh>JSjk%Oa4c#vsgKK>xGcI%rUa z1Aumi$@%%hB+c?~;5Bd%cNi_rFXG45{ySg=c=ZqYac!-N2n|ZWf%>D|&y}n3qSKYH zey#@Mo}}iJ{I{Ap0{0C04zoNZ_H#&{!=6&lz+i5Gk5+h`E3IK4twX|ZUkaaSi1cKw ze4)PUMQmLg^d+R%W>hDxCi4S$+6U1_6-&ewbY zI!}$HJq}hB2xL8}T6HE@9vMknpEO(jv%{ij-!7tPH_XONEV)^WqJ1^r27M40OHC{6 zx<-W0-lO~$%@%SSSf@3!^>w0#5)1;5r1Xgav9(n2Jt{#Qm~+r`7Gc|3XPNS19U3w@ z2SqsTS56Z3(9ZX@QOE<@(<{s zS=SndS5QK$UFgW9)lXM_VWqWIIWjZ47PqBS1#N}KOcB`gKNawo1^#h^>b%awQU<|; z3sB7yq6O8lojvR+Ce@unl@WfKA0uuEaC#^TOvBXJ(R?{iX>U?mMkpIKY~(kV4m&kk z=KQu{I$Z4gdJV#tR+fg2vJ@B5=owwlIuYdn5eQ=UQJ34y7cXve+@EfxzJCC>T7;a^ z+fvfudL82DI5}^uF)KNcHr9KY^V_g-h}Y^i>Qg?jgdG)~=us2Q0}Tdf9%cib1yseZ$*LWBE}9_?nGCV{GUG3+SuR6lMo;aOkw)6)LlccIX1j{2~aQ-pO) zvGUK?xv!bH5`aCX$51mI6Q8OC{;`y4{%{f5cAm3m)^N|cZ*tCmmujAP;Xba8@yu<9 z88gkL?G?4PqV7)-KkAZ8N3frTRg9%&3Fs!bF+E07{!$i>t!ZirX?R(4O0@QvF~T!Z zg<`7GeEqXI4ICK7$AL_EEjLQO2z#1Usy)p<)t)9@v#05bu%~ItH=+6{lNy-chRte5 zvbm-4nf4C%zf!>N*P>Ll=5JwDExW3!<#EiBo7oO4V>|1XRH{+3I4TzX6|^Br7R`e9 z;%E)6JV%vK&#D1hE6{sYy4faWwS7oLyXP#qb02*>-||?q+wbEriwSZw|0pRVsH_Xz z*hP!57sr9}o7ai2SW?OQHaiJ3_4}vvUC&A;^3wHCmgbwM!1bnDNhp)M{3|#el$7!$ zE!%3QgXtKOQ{|?|{y*gX4}4SA`8bXzG@$_k2@)V^)qoMJbXslaim6(#EmbR~|BIBC zZJo<1bL_p;q1IANTN*DZsE9bR>gG1LF=d-`RS{bXE#+@z&gDwWP;tB1igja%QporD zbM8&jhN}DQ{eJykub&&uz2}^J{yyh9&-0w;kDblK46EAl5pP`cC3kvNXw}Z=c<6%0 z&eDZ?s)*`K&!bYWnD76QRk4`f`L@8t*GTVP!dL{() z+eD0&HaFW~X{gk51h6;TKQ{(4A2s>jz4%pJuRp&x&U&)31D@(E^%&b+y3GoZC1shb z%y^ZYFYTkLEdPW9ueBdB$7LIUcBx;DSAE1*F87Z0=h*!@34yifA47Q$4`Fh$xZH0F zi;uUSF!*J# z{vOx_%klVFhqEma6iDL$UFE@db72u$5q|ZMr^i_ZK+;$L1^rF3aZg|^xe&0{JA*D*8 z`UjY7$>Iq2bA?exQ-Ckic8p_{tS105nl0oaQ=+=24Y=UeuvBXH`a3MqmSK zbmx0AI@u$0^(7G|#JWylKZiZQQ^o`&Ued$LhWt)@Vwxq<{(4Hoz4DTd&O+EIRc2|AQg|GY&8bx(-0YTm zbb{ahfqB)=|9Ny0LI`3%dt8^4)CgEY0W6Va_h%)rA3$Zsr_?^xpC;uQ1eTj;U^F2R zI^b~)qYHE{-D;Bx>~Y<3-JiaLRtDX3b~k*4;WGh$>_q(WQPmVY9iamlS1beFfa{nC z$pNs))hRas()RNNK8vWH4pSu&QF(Klh*_HM=T%WAV=_ZZzN9D*eqdtLO`?HrB@1c~+CKOB>x=}*yy-MIF4sKSvtXR5YA-K4Y0$}xQ0`a+He zE^6rZUKZx;N<8a#GQ8!Ys=qnf2Av0kCz7AMSt$3zHYxFJ`fjpSS+|Q#iab01KnCH3 z$_;m8W)sw>{7B6#T`e+aLT1B9mNkFWMc)G$LOm{}=Hg*V?jp}p8uvTz;<&BRs%Kat zd`TcFMHIQ4ntAp6mmp%)u{s%2GG(|Ae{@Xw&=g3Qy?oAHNOOklAeaR@)CbkXF&5yVE;a)!zi-cnv+V zt&Vh^^e~dH8VQ@}OmdT{(yA^ZVS5BFn-0UOA_e2NpKua^ahOtTKM?o|$lQiy z51T9v&ob+aAtxRAdD1RrM-h_7K%wA5&NKKD0;MO{4&cNS}TaNU;X z<$9XCU0MZ;v3v&-o|Gz}41SI~Nf1%J}a~ z$9lRlGKl5DORsm$anPeR&14SNZK;0`QPMmE9L_6j{uOqCl{3SKH2f17c{6eVQxO;E z6E5C_?~0^F4o_Def|MdZV_W6S-%L6whzR8|Jv;!dPnUw~o3F!+m+SI8v#;GO-(-I0 z7)9i{?_2UbgM5=!eJzmZndF;n>T70HbreHfn(k{eyQK%y>6ZTh-PkvD#pAtS^TIQs z{>ku3cr2(d5!j}&3RMmQ(hqR@$et9JaWuuEMOja+ln&+z?Z&yTxH1Tdv&}sKcB?5}({akg$ti1e>v%*WW zvKSsyLc*2vHgX`c4YjkB`H;bWNE&e;m$?1gNW*j+YM3e~GF6`>&4`9e@|5>7g-?=_ zRh>vA7ArlO*gI!Mq(7C`74Z$$=y*=gza7gz9I$={W#RfW(6!(g3VwprzpiWSjxL~9 z{*phQVD}Tgdc)9dMfd@CPepn?7rppMy66Im4NrFAGs zMZX_1InR@4puzMa1KLY3GL5tc!m%ON!*RNmiLDyg>drR>&}Hg@%paH(=%}3a|>R^hhbnBF#?Hxuy!N(G>u# z8(9Z`{Pre-v>E8#r{SP?avgfN%m=)9+Z0r3sUD+EJeU6km%l=kpM~Ww#PSzXdGBP2 zwSrA~nArNwOEiOyVy@sjn^+6UYbU@$e}L!Fm-PIPTbw!p$|rG3Vf%KC-P)aOMPz08bw*=}0-r+0VqzZuMJ@)(%KUk31raGHATgCyM8Eg#Sh9 zEmO3ASZ9g!*~!&NoR}>2|AwF9DwASd0OZKp;Q%301~IKk1(FPVpYi$PWGmC$z1>wh zxw~piv0UcFKtMJ*BOyQ7?OArgTyka;%y9_62z^G~t;tYc?j{@a3@e$~JAygrBf%+; z4WciBGV}>xfJgi)NzOLGqZ1u1ptk^cP8Ig)N${M8pTl!9JfrUfClO?k`~c9qLF#tZ zm{nbf$V-Yn>EsEZyh7;v;=O5hPApQvLhAcrTH&m2nC10b`E!9v`3F%cL400|(e3Kq zXdh1$&we5Tl-CMiW@EKbJx-7^NJ>@dJnLajeeIRX=ieYlO?QcSLBuJ1;rIFY+ty*ey)dSJa}=wifurN9;~&KJ>WuzWL{PG zEOHK?qL^)a&484RmeN2@+;|;rovv&L9ktmu{vmxcafnOpfgGUqIe0~enJy4>Or3hw zcsscYn>TsK`^~Gd51kK8k@Bp7D9o-r zGcc34K>6x-2*W;$)itygdy8F0s%;_3yS)p<{S@CSn7mHL8{hvkaO(pmX>XVQJ31nd zJsEe*#R-Xf2R1i}&CMt_kEd#?+scO_T+v=Z$sa!TCW$7Uz%F8RU7SD1Mr@BseKV5! z@j&Xsn*2kWn2WSyo-J^%nC{}h4;|~INif}xC-5Vl?c?Rr0y9r{Pa03RhyfBA7o=iw z*ghH0w_C#4U)$I*&S!LIvIw1!$cxYeiM$B5w_RRjM^`3`U=KUwMUI%~b2-r+Rw+K^ zWJ6o+!h*n}N`~i#lQm}7-~%S;bigo3*M&I--&|eo6zOIIY_9nRU|shp|6Y8Fu74uR zrwsYMMxJL;BQpF4BN}mD-3j%}b?EyAYc%4+IgUv# zfATRpug>T5sydEu98bIg>^PiHP2-)K11iyfE?*|jw?aJMZZE*H>1Nnt!G=TL8`(%& z%;;4y=MiQL@I{UxKM&^Y_jiQn>`k=bqB$!N2V&u4c(Vv~9rE5$k`kKxBXjL7coS(y znUJ{^mJknHTfQ=34xyt2_FM-~=hda@yc1nsXTk&V{>(q>NUcEcy5C&0V+qBwf}>ac z6+aR10f*eJF%(xPXB)Tk26STFNm)|47_v#+ztM^HXHht==cQ?NZXK z4If$Rm*QPCN&%&PO7Rt8cYDhX{l;w=Znfbgs+*B{qmJnD=zy-?`BG=J{>yNM1*dZN z8$#i1|1gEK8fOB*I|a9NI`ZX-GS73nskhHx zKFm2F_Ll8e`=CKrnX~kPWvJIw>{iTbb=7x}WEDRD9)B)k%VLB`zu}4gG<=|%@a+n! zYh$%Y%Cd%^_&P{%s>QfGe;cV4*iht-3;h`eP_eA>v6Z&efpLzKB7$F9bukl3 zGooX0R9YlG+3^T%fghkG2)4j?aSME_7`MROaj*rDc0=VHgWH3B^(5xfu@v|&ak6$9 zj(8vgFrYUYo4__lBErlrl#qx(V#KQ4uz8lwt$zD($tAjSBu`Dz_hEOAAnx+WR#m+Z zLzkNVS`*q;qFV#L2fJGhAws6IV=o4Fz1Aevnq8*>xDxc-JgvLcVkE1dW}iN7;7Rf!9}%p$<`$;(lGJwkcTsnhGQ*^cHzy#4Ov3#6} zXFli+2tXJp0REGUZE~@-L@qW5YMpYiLpmTAC&=Y?<<-|_p)Yv#xFX;1{so3kIJCO5 zb0SCub$#w1u|BH?QuU;`N+Fw(XV7}-*>!^>bim3SLfh!|yx^hntl61SvM07Sr+ z{>_v{7Skiir-^fuhtrRk$%(`+d*W(vutG1&oTPM?vz@Wjk2kQqYpI?ERIuT$!}zCD z&%oZ34tS?1HSp-`4$ax+U*pU`zer}~UP$uS(_7@X6oh&VJ%y>^Km;|sDohP~S4doe zYiEtow=dL>f95C{llQJOr$g?*+UHGlDVPaml;>+F=p=jKR=q3NxfBJSwbm-SI$NrI z^vWzD8^ycbvVy#P<%(2N`t(S9It_MQW9%U6eZtiCXGirhDYwB2$EGnEel5*s#<0#a zb?bq6>QR3j{1ApAL0S2$g!@XR1qLB(Z+~y%gdYDiX{A9jyp9Rdda9xQ$ixX<{_Gt8 z-3EZ~z&yRb*yLJZl8&jdk5jnkWzy}Ws_IP9Dx!!5TRe1#|pkSpl5xo~76Gxuo*mM?>5n62UYUfu3B0KtYLg!xO-3K^EEvh%{ShVm{E2N4(nsz|+|6Chm43`&O?}?*G^4Au&Gab3*ip9ML0k#u!lz z^?OC&ZgX&N9`ua0Qnl{$#Tk^TC&4)lkME{M5xdNi{KHrT??S~m`QnR;Bv5lp*hAhD z13AT8VtUs51YgN7%*2*fwWAb}N+vj!f8Q`3XP_t#a!WW%nS3LrWg^$=;Omq~*tKQ* ztu-c>(GI`0*G6S+&Ec#z_^rJl`l|(yH66Z!-<1|o`%$Ei(T7wFxUw{S0|lRq?~trY zjMFe)^~H?XrF$|_eamRnaTJnW*PaO-`xKF3b^?+Pa<)BB&UWCy5pE(O${hhW>9jIA z0-_4F`35K(22rKqX%@)PCis54_##zE4|pk9E?_6Qf{-dGe_jlHIF0`ik6s{uK=-h_ zm~Vi25n_)XDCQPx^4sfSO^S3vq`wdH;N=v{^652*>WQa9>>?4x1ZW$G-WCa67H7hI ztjObgfK}U31d&ht&DXC&?lDuYBQf(nxel}BIuboED1<3nkV7-JV5znR7v#cxEhwew zT2QIY;{x1%3(CY?-R=o*zTkp%{xBoRlLCb^Bj(I1Z=_Z+bEAQu?D9tJ19V)%ZM>Xe2Z$_OVF7fY(3z2s_ZOtQfPYZy!q_5JELSrh-@Ph4WL%c%DSdSTyYz-PFWd9Yh_O@)Oi`AYY4gtJ>+ASTSh~ zFk`sPgbJndL|mjV+wH{2ZYQG(JM>vH6`&GZCUtWQcJql}k`_DWrV2sRIs+3Au_a09 zpTft2B$Nxm+q2Ij;w1QCeU2aVaEjgcC{RW`i9Sh5@MMT{RmkdROc&JJ$8CR&0|K zdh|P(F*bIi)j4;Gz zL_;hqHi>RL6Yj=y)Quc?dWoKLd12tamB0qtgXcExEIjR?r&4&@MNeh$w3nWi!cz}D zRl?I-^yCQ$MJgt-5QpGexJk-Mp|4B3*v_0dCIxsj^vkOsX3l{qVZ?m`$z!UG{&_k5 z=OH^5NoM5baQFo2=1gG7C#vH9V)VshpF-gC(V!#OX>BTW+DXF@)%bv#hqN+Ct3>51 zRI`M1^beH|X_->o$Vup|r~Vi%4LxJDdtcY*80p1QkG>^iP_Vxk6Q*E)GA2Tv8^H3n znu?0^m|9iz-U+BmP_1M0gPdA3YftAW3tj{OC_)w6%;J2v(scP{NJYAV(Cai@4bT|) zK_$sxfgkfabI@rwE~}!ObqO)LmimkEgkt&cMUpG90P9as>rYVr@dDIOD!ld*s6UbG zcT)XxS-eYAS>v=Td!CyWPEmgQbD~1+=us~!+7FgP8rzZNt7(guQ#qt)l#7=Ux1K57 z`~n$#{nM&A!y;WP-D5y!S~JcExH?%ZW=Q$aA!emv5LJ@dL4GQV2~Zwj_YCaDLA>=x zww{|V&So!7w|d?F^2T_b8|_Cto*)#@Q-1zO66AO#a*<)p$CGH)f=hHB^G|@0BU$)9 zW%&YB*-mb_&I%(368@P7&*0s927ndnyBL(v=2@y8G$zC;$2q|!62>Tfn6QRsM5O7J zZcgh7r|FbGavIK37pD;>L47)Og$EOcJ5a+Uc%pQmrHg=YgW~0uOGQGQ@@-Bqi-a*s zJ}0aY33_E7x4Db^afJHuHBPg0KaQyVc=`Jj7zVXnDi%lgB3j%H_~?fyu0C1&KjawYPhV)mIho7V9ELZ0P7X^w# zuI>Ij+XFT!&mnmqGz4r9BAj01nrEvS;IcweGq>DOQadN*qjgfYm4sK5rSwEqzhpsD zjnTCrxz^_E(ED~eUFC+Fpzw)rMl<$=h7hpD#nyoNkmP;H5LgEPV2dv>sl42Ge*@5y zWdK&L7-0BUmkL&H8nJR{I%vm3dYg{K#WwYJB1xKxmrs76L^ALfHrCWkc&PBZP13|1 zcq(7w&oQIFdowzr@GikW@Kvt(%B_A?sC`u${gpJeOq5-!maWvvdWM%>Cdw{X%X;wv zi>{y)GfWDXjgr5_vL7m8hV^VMdR$65={oCmRi!JXGgV_yldBVBAHz|!OL<~IxH(I3 zlOBYfK^ zn@K7*l;q_r6P#qANw_gy=f45QBy}v-YDE9eBjtSvL*4#Kf$U`eLT6WY5{Z$r6H%w5 z$>d*{gNKtJsLcI}`6KenW;$00z;o7*b@4f@X`hYo{e(Mswo3 z9o37Z>;%`&l@t7TJN$Rs{CC^?7r3(R-ubWzv+WPW`Li)P+b|C`-?d_^#{(l2O(s1k7nt`xBpr=IoZ*q5TV_ zY=>(nUT}SUA;Xi_p91X9LRW`W>o|9p4qg=9rOA?QbyRmgcAY^<>BC$*-LhocutfSQ zU$#xZ82ySHqV-2Cd_4z{Z8-J>97~b#G_5DrdY7bxT7~r3bapJ77RXX zMoweEUQm?s(Oi!cLxx!D$+Oq32G>4IJziNqi`=Oz&a&n*J~@+&NhlNB=Z}+6)QAfA z$$iGeJrTS_hC#3rIpo`kZtvN5Jl)9$!mUj42+MMKf8>DIe9AN4UoNASdQ8SSc4j?P#jzebw=QM<>FUH;?ECWiv z36V~6&9m3EAqDQ+VMm0wN4eqMPzX7|5_mrN87YMK;q%+5+PokKal7;f6JU`A2Dit& zM|ENPr{6;pKw0c1$)XL33Pz1wapd`@*=J9CGi;x|Y5?_T0SFjN+W&Yqga^dXOk{Db zMQHv~!Zx8RA{P5YfZGz~^!aS*S?J6u@ZW9q-wohzf)zpQgB%-^(wU^|N=q$eIDKs) z->I?w%}#VKVp+X%Ci&E=EHqj@%eE37mDopSCYB2qfU0V>>PBG1Ku@~g@MZ6$$?>0oN2O2Kb785vs5f~ev zjRfNb7kmXU{?>mC#`m6&3-r*mVPHI21!L2H0>*{YM}n~-_N?u^aC9)fb0iqA`(pTg z_323X9h&hU!|xq4aC+{V`c>c;`P(r3rhKH-%!r0yU$#L%5-tDzX#v2;V*#+N0&ve) z1K{qdBLUbLdnv9Pr!)$tj%`x8Peh4xri&>RKs6GeH){Yr zjKJ_u!)f}-c^W|H0yNv&U|s@-b+DOY0rav>Bsj?w+v&GZg}RjpA)he=LN4@%;raP| zpm?S|6%EhB$&A0M5WWAaLG-8Rja-xX*lXgMG5VU65LmDKB6wc*WF$PlF!lcgo{!jY zp+2$xSK;}!$s^~^6npNH?EjnaJmV*k@VwaeAH(x2Q*fGgTfPcBZ+apMp8KW@gXgb} z2Y4o>eF4v=X@H-zfAYTr&zD(8uF1IAYqD|Le+8a5ZH$EH!{_~k!J&*tIz;nsOk!vy`_L}UP@?U}Hy^lrlf7bsKc+Rj8|2N^k3eOjqN6wx39Q@yT z|C{i9@A^o1er?i!49_zr;WS+_{;R_czXQ*!$B$f- ziLuw@l=Z&?&qI$y!t)Ii|4-m~xfvH~mFd3<&$o^nId_v{&)rtb|1&%rE|1H`%Z(Te z;jbQ!gy;SV|1mt@JOQWaj`#?MFu)99e!#ry*hl(dqyvHGghvEuJ|ByxZp9>?T$s=({;u}?l#mpHF_gp|P#*hxD1^6TU35G4Ap%vc$@Mo&Lwqct ziu)`EObwf?gz-38UmH6Nrv9W3_yrxo)ZNxEJLJ=2N5Yde_K=rNj2v=#c*qx|gaYQP z@?g*~=4U^M9P>XKK7v6M@dyGB48g`IQcA|*AUsB1@nZCnoQ?r20_L`W`5~nOYXF9x z#k^NP9oxuKE_1G+5*#|X1}#W7IqM)_Mhz3v+k_nUs>GS7qo};wa?bjeM$}iyp@=<{ zv5eD}Lt3ETSq6i4WAn;mSl6yq?G(HBx6If0xhvW4Mdn>WRlP&ej#-lz5+Fe=dm%=lJ?zGew zjTio~N!OR_QGU=o3-uSsXC_P+DhXjkN#&RC;#GmAe%yFfW#pPep`PoJwXL3<_B1Mp zSn3DH>2$}?nQ)X+N$Mwlt7Q#dfw_!n@VxSYldJhUc+4O461STdxV=*O*{8E&m}Cg2 zH&uGKG~a`cZczVleky9UVT2S6`8`>hZw~eNPU)-DWkp`kN>0aB9Vb@B5!y}t=c|3Y z9@&`c36XtSJ3J3Y1gk>12bV6Z_Vng!=+pO-sE!ys5MKY5J zAR;u$(4`h;Kq^jXpRu$k+3N0X2f!8+Uhmwh?uA|a-lo3q<@e3%UV4V#x2W&fr{HF8 zSm@hndS_^z0B_InTPeRi&u?Y?wu9f6@|#%pN`BkRZ>af-+G`jaJ9M!AkfnZyQJb_# zq;kB^*mf(xpQ#rOuaYIVLGI=W^rIhD!Z4^~i>`Ch`A6Dqez9}Q!=)&$tj|jC&MFY>gr{qdk?fhP6OlwXTB#FUIBcL_fBlRmEvOC zN!PHDZZd5GnxKX%UGJqhS3GtcasrMW)tKElOzu|LmP4Hb=P+B2#2Dkgbi}yrqmJ9b z<4zUhPNHfzaWYSVGdu-}vB#ambjriy#I+^0OER-c6SI-tOHd;8t}}$Kwo~X{ zCy86%WHH!Ob#OFK1RorioyIC37Y60+WKfPux?+q_dp@d=(>NIvaIB`-V>N}xY7URp z8hfmn>R9dP9;?<{%s^wFOzJf+dIUTtM+`L*o|8weD*#oyrj(n=tBkcrD7T~PY7+N9 zLDOYT<^&tTf`DNjlJIt+OuL|LYeMXGP5;lke$N`bI=jR=c#o%U9l&1aTsM>b779m;B6rIvR}r=G}ao=3|= zk}RsqW!g#It{YzEaxL%YBdYXjd4AnFPWTb?PP>Un-$XvcaTR)v$*Axuwg&pEs8{z` zVtBiyhBq3{dMX{6vatzG2p$o$j^V_l1?)3-0s71(qO#$|e}_V0%}!!5ltT}a+GR!; zCy>Tg7o;t&SOTl$)rY+ct=fSRU^=AG>(>~qkAB@~b-Ik!6W1H9J2H&c^*0%dNkzZ}LG6|da#a<+loB#?Y;mq}-Nd7%;Y zS=-=QDmLZA3Nk53!srJe6)mo?^ zP;A2r-hxM8rvo+ky5X@x^$gTG&lsg6>KjN#^A0l2PYup2-d3X*W@;IR;5V7{%PQGFXrs;Cet5Usad@xXeYm4{*WvAiLvb&r!!}2+ap`g!8$C!pz1{5}iolI> z_nd$B6*>*=f1lQ_SS}4gi>v;w)76D0q&6hxK4kTKMI~*)?!&wLjCQC%Zae(Gy!&uB zruF7J4Z#l&hZ}2yF6zR!*4?0%&+QA@H_fj^S_zPE@Yb2@ku&T?^u~LEcW^Q9ik01D<~DL>c~WV9 zW6rF+^YD8SGy2itZgut3Mce5#!ft_a*<2+_n5!hKlij-udx#Vz!9#a1^pSe|uDmC> z@9-X)4Kh8G+j@8PE~9btI|>ph{qTON2gV@p<5A!k=KOP%QHbhBj^gk~VzH=1#B(Bl zA%=s4OVX@w$np|8l3OlTR~t2CvgaGUAf|9-5`%;*696p=3V|Vm4ctk&PN?5uqYj~e zmqaHLeJ__92%D9fw&;C3lDFG+eL-x#Pc9c4eNOR<9k6}b4p8L@T)6`(zov~VkJN`l zeQdAOT2d^1rYuL-M+=frM|bKTz&E!iWAu%8I^eM+6@9cGfX6^Z+UsaAuKIU~!{RSP zhOfim`$%7XJWx@}1z6rU0qfWhmA@h~e+pK7Ph|enXwJz+ZWi?yN9DIh=2QLGM&wJ6 z_o}5S{sxBD+C`K8u%<%SGBl6|d2^gk=>s$+U%BvmWgjz~)h7-O|35nr^GD=7a zC)`R2$>D_SC?PeRa5*KUg%j-C@l0Bc;csv;E~nSE*K+~oe2f(QIC==eIp5rlA7_R? zJ_HGY)j8tZ+xXi;@$F&A%#&A_qS-r@{wj_o$uMl8xq!_7t8{K<);>5gn}e#Hp-M*wX>WJD4rP$Z zwKtapYFDh2N*s_ZKFU`<+6y1Whg00jAxydbhVj)cf73;t)=0^Uq6dT7?-MqC8+RHK>9+b%cFdWd5fIXAUnB_Lqiq z2Rad_XAOWKQMNCfHDK(M1ZkNwBY>g2(V<4V@WA)#0ZA2fLKU^%it| zbuCP_YF}EKqf(Si4J+q)X6f7oo?V(2@F$Jr2Rh{t-Z9WZ+`c>>qY!6XtFM!8j3!K_ zl{p0kRL}13LOms(I<1~xaXtNG(c4G4m8hnD$|X3gT$+e9A`*YPg4Ftxqa1N24PT$S zdks48qu$qP~nyQ50*%uS?aHbEdP&9=ys57r1n!x+y!P!J-S+j zuKwUroEA&{+xU9BS*d_`NDfvYF<6}@HPZ<1$#F+HiQA?8<4zc2v8R5oI>gQf8e+vY zZRMzV4OOLnlj^X?uA{$}oI2cYb}Ogv#)AF@*6MGFvXim2o2Qj#bOkq;p*2?$-5j2O zI=RAza=Z#VP&r%laz#P~Y64MktMZp24=Ah0! zVw!krGR$14oKm1i$|M1@S^weV87*0*=rC(_0ZL#JGFaiU!8l*=6yOq8}x zU>hw@yv7fBl7FtZR>f7BOO(7{e;)F6;WFHNEU=B1DL%TyRS_lRi9k-+RPA*MG0We{Y8W-lcNBK|asdZjkebeA~@(eyOhw>yq;e<$SmN zFy1@J57Tvxk$gs#z9kzs(L+!+K0^<^vhg{3I3gRLr-!4maWg$AvT+MN49Lcp=wVPc zw$j51QQ=1PE-dQYNU}PvcHA6Y&zJKz^+3OemDnCO=jRzs2%nh451% zKjub1{h!H?F-9>M2(cdHZ_rd}E3+47wT9E$s zQjS8;J@kAO{`MRCq^uPMZj=pv|KDVE_X{KmCzk+cGZ--3(|*L@YqKab zkIzO@QQ847z9WPFtTyFv6A_@F7RBpiT`*3XShpn2Dle%)LggldykrGZDmR(rC6#hk zjl9GlXRVNznB=TVd5Kw?fsJe`M)t`XC^EREthW3ry}KB>D9HI%C5bfW#9>n`n6K z!KgRNTYsf_nt=1exeRNbv6Os91OgtwIC;>9XHy=+;Hs&Y=mL)+^OVnd%HK+7;jRASfnop{PcD2c zFCIj?d+`aR?iQayvU2edl9d;r8`#A_t|iEefr@j;i_OZGitrE&G=yUBbpXm#Q&+}P zV$-Aw?#*om>mmq=!oR zsZx9Zcv+G|y-&p6-vm8LhOx<6IXJu`1B@;!Q7!`HlPnjRvC~27^bzWG61l1Zc9dgkwxd<4= z;2ozptjXQM{2_j-44%&qOQf4J0D8RN?K>2`#v@vT`k*PO654@UcO34QcSEt@dr&I) zz9_UL9Sbdi33c~h)EDeKB+oyEiSvgraZLYsIQZaok6e@?Wx?!b%2^rG#4RZIxYr5$ z4L;A5GZgeVb%H!ju>(Ks%>X8zVmDL%+U=+iIjDue=+c)1>)1@@`+dz7OvuviA0&=sgSXB^+|rjy~jau^61W@E0%Z^4y;1 z_ol7GQ3_4=kMHa4dk03`4fB!}guNc@4m{>%D98`)0o)RJ3|9>Ln3Y81 z0D#Ks1#UE}hup+vrE+R2Kmw*Faw>b|La0h2RFq1nC(m+haN9F|OP0PGha_TFQ{NKk*1EnWN9C+FeM_uz*809B1F%BAHsHJm zeC=l6hk96Bc?Ya!C-rRyt>vPDzO1Fe-F;aOOzg|DbLwDUmh~{C9u=~J+XgW8MD=g` z-U(2b`hxDhVBj`b?TvYI`9Rbi`D4WD(kL-(C_8*6^Y_ceuz-3<=UiQ=iEc+UjO%-KLT(&88*|k(QO_V*g zm&%sK?!$wJh(2#nQTNB25=DU z4dX8H2*{>PehLf9Sxe=ol(XVg9>53VUQr6Z?FoPTR@AqH>bED5E&9OEbN`9tT|`tE z-_B*y2TMpwT_g?oj%G~BB)YLMTGnv!EeI{iD3u%ylbgKIdJ~pN? ztP+f&%9Xs2-gXErDK{X*y`o7dL0lo^2P>VZMDK>*(ujth3pez9xS`GPsN}pf4q*es zScqMT=$hYfw@OoqO}x*kBcO0{j@YuBwyxLX-YY=Dw>Al$Zic6nLojtqjbwb}h9CQ? zWUGIZv(QLxZA&?$41O|@K;GosIswh%P+urJNy?}JzPQ?u($+uDt?YSh9G>#7QiCNR zVP_%=g!(7Am0xoK>6BDY^`lcduDkYpX+5=0=07G_J8T#&f4p-=VL9_E6BNSqfVoai zcqm|a2-3`S*L;-~?{JUHZum9(_yaxpyfhy};f59gz7B8E@FpB!2f-sBeiOwnPC~wj znpTCQZkxbbtYQN2zb>xZ1e2uPx_`D#g5?9mwiG48UC^T0#c+~+c2%@^SZWB=yRbPHux=~4|0;jUXEu(1d#2W(?A~a zkHBy3RQ!f31pG_E#$V|-V%>T<+tJH5KP=OjC1*Q(*)WF_Cd=80y=;p^3HQf$Hj+|} zI4x0=2asbLP$4+B=5LJ&pO$NgI1;C&KNs)=W0deQif2#Y1-vecK0E8S(lqG2GZ_)= zAw=MvBG+$1V8tC0Y(%D}e+(?Irux|@jJ*Ig2p!Yvv4}44~wClD_m}$6Z1ydOsQuxlPAJ(NOP!U#6 z>NzdEfpc62JgqrY10L8M&za8bJ~pBP0&KylJ~p=!#8`YE+fc#lZjDAL^M0+a2H>uG zHq0p7QT4XVS-Qg?hSiOO&B&@AKBm44a}Hhy?eI6=XP|>~iOvVJ?}VKvxA}G&4(~+* z0N@5uA1eQ{!a*h}2;?A@7F;kOR}7#6X>h@yTrr5cuE7N-1@XFH!tE_)?0x+ZSx`t??M$)2$ik6Ba9O_$|FSm(9{O}&Pyjz^# zdBReWkMMRnTG?XpaJR5j>(K6#AzdO0566>sDhNYG~9tkB%2w@YET#CZQK^z zi=zmJJIw>u08A_agBin(25kLv`uh3~^~vh!wLMF7w-07wFAzt2NI=|;keM8Y%>CM8 z2Rp;l8{8S5)8G!RPrIon(J_2zz;LGIYGZm=dumJ`F%8w_|D7qo4)Bum6pZT&9-B#W zS#bhc>Yw|69U5=RUz8fcy$v=y3}4uHZnILu58Q60h9$V|N)1==Uh57m4q*yF zWh0^a@a|wPV0-E($7o0MuP!> zeYMHx7P<`6EX|A1#C-$mwFI(P0CRzC2wLxA#Du^Lcz2f)TBjZ*xx{$oIx~dzY(BbJ zUubscD-Z38Co}+^N##8*%xIU(^{N?djl7ct)casG?SvS$t?CI=CEh;Gs=1Ddoh{-0 z2j6!G_aAN-TUJyecK5PAw#vtIs82JT#v`bY`4>z%yzlTn*w;IHbDeIoi4mxP7AMU3 zi}eY!{$h#3%s<1EIEnxd>IL+Q<7gB4#V05eN75$eQGpAYR1x!Zgx}Km?I^#|jAaeU zS*MsB1Ij`*F|Q{RKX-5s83omo86Yf;Ler5aneq>}=LuFAmPQa*R{nAsp0Io1VX0nB z9y80Ll#NR*r*i}z&`1yt^xagB^f$Sjoy&s7|U^jsEZrEfzy#l4;=@3NG zK~OAQpD=pgOLgKD3BIh4@td$LS}GcSCgQB$#F$>2SO>p7!*7V;mCtqpKxUQUEnU`9 znddIJo2-*~1_6YRQuFz~0pyx8SU;7AAB-AknO+3{Li<2L_;|ym61db-o-LkbjRIPs z7c{IRVw&pJZ-mYDa-z0s#ELZOw5oT7H$s%8Z%w$!-Y}RCllQIDiKctFh%ojSP$Z1~ z-Tc#V+8?G-laSe}P_gn}AFrN>c@si1Jco4smAl zJ!^S1)9YOLKSb3aA7&@p*vjD`f z19{rSa8N;1h}`!k9%zULAyL@!Rzi6pD_O=jgAu3^AO;=>N8q{$go*Ifr$L>H1Y=wp z?gZG|Nsy;q8VX#yr;kS~HtRDg%q=37q9F z+@DZ%&H<#l@L@kZY$jQz`6xUhp-2M_<9{S?`?O84h2O+bk&>hl)zdVoqpo+p#BUs( z$v~q$p^kPa*1a%qjNoo{*n7`4+TAojzB6U}T^{SkzITM^x-XdD7YuNKN`x<>*f*u4 zh^qiMAfswpw@v98M99}u%ob@sB;@b`{}vqq>ACzC4Greqa$4i z3KV`@ikETUKY&XRhAj_tqD_FA3R}z6Is0D%v+ZznZ|9>wV)6#$?Lx>#4}+ZCE<|!P z`|@s8T-V*pv2P%`n~raq{YU#`g0-N8TY3jV)?eWPwVvj@Ev7fH#`X+?A;S_6nU^Cx zsIB@)(0eNiruIg_R6R|d0KFh*48{hwb|4o!kX)e=?b62Fu8z4QJka*=RRN5d#z!Nk znTnVj81Z8?;PJ=fJpT|2E-VAZto`g43oVNAg%?xjrHM{-|#{^3q>PZISt}>9= z;i?Uq*nvrZ%`8rw6~ZY;K9gWo27#j8>QT#db-h#E$3+~hz0dK5pYZCz$1EhKa-ubS z=t81_6Kzq%j5&g)!E-r$=)w*VCvGlh)(}Zhr_)G+`tYGFPSqxzAPL9?(TH;yx1%!n zuxjg98mL{7@)7!*l1dzbHA!q9xzJo?$X7mJAFq=vf%+s|Uv(xwz{CYdXy%eojF?|# za4QGatA>9RwkWygcb(Zrl^3WG%z)qpgno1Dx`_>u@n}%rIcLI|kbFF)t*P-SEM8ON zATkDxC+P1f`is{QU1PlmlC8e(kiQwh#ow$T&}=wDe|yJ}u67Fs$*gNeCsK8tjemz1 zeR*_5sH%|X`M1v%|Aw288nI^IsgN28$2&=-CEU_{q%8Jpv2G?ldM0$vST|yPO&4IiL*8(z24D|Hv(}^wXx%a6o7S7N&G4pmW)R=B&iv0Wu%z0Tx2UAr zmwTwB+7}PLX??i^-YU9ij&TahqwOsyN>{_7YQr^ci#s@05C%I=rO=ZNC#%o%EPqqg zTXQc&rBIVJO?r^xZI?*k{_@a! z5#_y3v`Dm;NS~oWB8l${zusW2xyWTKh2NFa>fZP}RWlL81D}G1i!p*QOfSZ5ue-h) zwF#hE?_{9yLRA#sr7B)|e~_C*{DM(xDYW-Gho0>o*$%LaCeS_<3Uv|S4X`UpoG>UxR_t$`M-umFmEFJK}woU5N6 z3I!S+;^X(%A;>sIGeti)=~i`O#i*r!*&3{vEOYX-1s&u4ny{W!4L3u=C>f`ds;$x$ zdr%^&yu50h4yU_POgGnHwbbL!vs)Xkvwp8_-$XqgXC=}$tn_D?cxh-Q9avf*=tJ;~ z@mPB&CZawOTL+qZFQvGqZTOB+TH{qiQi?1M1>8JGc@m8e*_%9RVh+HGGV!fAU42_c z^^_`Dt}ZHt|0%|8yt)EFeLD=HBQ&2F?@0p45;ojuhl=N9s7FjY<`=sGss8N5`S7KZ z^ym;8IGo2b(lyUZ)o0%~4l^x~Ic&Woj#6#Ic!n5JmV7p03?vGJYs+^lw*-emVW$dp z&u)hb-+&Hfe80h}e1D-yx7`*|p|lE@U8+OTw z>T%b!!3vtuZ$O^gPl&Jp_e6rcApJEIb_BnfK(MqRBlt}RS*b3_41N>6U9b}rbT(tA zzvzAjtxb0v-Uocp4zfOFH690<0mDDmbt88o!t(T;_V>e%X5J_J+yrtYBF%IAg$?K& zLOo=Gx;uuE?jG_n685p@&@5`9U&6(Sl}gwSVIRKU^B3A54tXB>OR>uH(36K5dO9x| zU3zE%v=e$S-1QgK%aD(wEce>FZFr*SI1U>&)=A`m{tzKV;nU{69O-7aTnIgB#$NZ& z6<>Mh0;M{D-iH$TGaagy%>6Z~mN?ljQG#;w7I;6-q=opfdNwx2ZK!u;)BA#3^zze4 zPgN`y>nGF5K9-!!h4^;-}{LSi{74-;6$ai@tYa8_q&>6@A85NJJ$W950n4 zl@(=pQ^qY=PH4_t-1p8F{o!{-jmqJ7hu2Eg3`<0K^$n!LNmlm;oqU%1_jMw?ntU8R zkzE?Hko?7jf#VJ#>cFV1d1&|FJCQVSCLZC1v?;S@qwxwrcBVW74#^DEb)y*9@**@d zmKUKxvb@NO1_g_3Xhg8cj+*z2642sckpo(shI-11TyBy|3~?lF#9(32Zf5VqAh$zq zYm~qLYPL>&fq{|lwcc*+?k}^?SPBpt>{j2o=5kYcg>OeHjQpB5@qqN+Z(`WVRkG$G zxqL`tmk-J4=|H%ANNp-}rU&k>^d#p|RNPI@I`}|Y`_oxESH}q5)IV+!x~blY1s*G( zyp=bT`-Fr%50>%dyiE~Yn~%%_bZ@DD1A*I!D4b6lgiBQMgZ;Oi_P^1Mu0Z+CCL0U+ zt%K~<_XOWVa3(*siFD`7u66hdAyH_!Bne#=tGH1?vv%}!i#F|*%6IOXr3(}~-TBb! zG9?~8P)~=c@Bo2pFAza$AO6F*uz!O2b~;Jfg~a1O9;fr9^N{X;Hs+i>`{HcvW11Uj zK2R~<$yfoLYYBW1_w>gzVbkMDsew|G3p{m;=Hj`l`wk|rFet|t zm~0bk>OQ#z`5N3}Ey(65me^hTy4IzuJ?R9n$_G!!EIC0d8Bexa7|Y#v zAk;)8ewh07ZzHyA8`9{)*T^KW(5;Ld1Br4ZxZ33U0R9-6Cm zET1qeBrnqUriMd+ld9t@Fv6`tR~-0prKj+1(*FM1(=oNb3w{gGG=<@4kJlnSh+=@^ zP)~UURvt_#QN9R@3ToXuC4y8Ri~f~9U}8Or zxZX%zWV#&%GbLfdT@dL+dO~i3O4X~Jq}LwJL5A6%Ky9UG9aelmxw=J+J;xx;3y(c* z^K9W(&){j|lJROuw?Ue(mMm~9Nt?y;dTOYk<*^Rs&{HvIRKHo%rw80ncfFV9Ayqye zgr->6C>1v97~G^I;o2-4(^sbw0o*?^;+y-I*B`20mIgSh`cBVs?(7}hfn**QJk3tC zduJgi~Z$_hivoYewQzDFX}FD)W4|8?Ew0$2rxo-pezK=&ijJvTX!Eg~MB~dM8wdFEG}R_s zeeIRXkGMhyR|rqj>e6)H$x$A&D?>eDPdI|#DuHT0Q5secq3W&G@^Tw8Lw~>R5*^hX zSXimd;F@P@6F41fK5uxEA?EP(7Bp~DHXU~;qW&%ob)C}fd{e3Qw57W5jEUY&KP!OMEmF9k4;L)yF(!Y z865I#?$E{BWKP8)TdVZNSlX@JnS{9A2COamb2_)b66L9nLZOsUpqW^P8mFym>}F^} znKo|{1$FjsL$1ib%}IZe`x+j!7V1Aanm-h*PNG`aB?@9ya*6W(1LzWE-_L0ZxdRwQ zSEsysW}=QHNH&rn-NgZ$qvA_P%f}>Q#iey-!a2yBQ*D6A>Q=&@jYoeRTZTfs%-A*0 z^Ff}*zdeD|XsP!h7fDlz)3_oEa#E|}@ve+`;BMl9+x32WUG*-pzqw9BU_%Z{Ns;>v zY`_cS{5H4T6AjT*rRv#-As>>!}ekY5F!$qmGaAz!%x zW&~;1FuX87IN{)E{2(5h$Pen$fx{X^ez1DL^)KQFuRwfp686^quCZJHoNPt5Prh|j zuJF~^*{whL)w#LC$n`px{AjM&99Mfvvw7QPY$Pb3!b^ifo$?uRyj_J$)BQ@Wnj-37 zHGm=k<6-@F%Z69g-Frv{OjhKu0!42dC2r zU@);~gAs}F<<;lI8J1z9WeD~7>67wRJFu}1X>!0zQU!GEuO^EAz~Z=X86JP#H;`G9 z5}JE&B?6D7880A!*!JxU^^cKuHH2y=y6&xfz$DFE9!Na}XzQdjsR{!M+w+y%a)6x6 zFnC@92zEpNDoI>1{X(*7Z)k&I*(~cj{L7qW%6UmZOCR#Oq)C$A*EWO@l$Nj5{1X=Y z9s~X#W;NGYSxnX7wp5Zk$@Oh2eg5Y;6baMn>(aYcR9e=28)cD8Fotzbg|-vj!^w%g znk5r@k&E^p2P>amA|0b+Vkh8TpjEPT(mp_|L=f}~abM^lmG~Kz2vk=Ro|pFHS$)-p zS-O;vs|#>Ffdo8_it8H?*IQoxGa!4{o0j_V@PLV@*-Du#Vl42%5^%TyvT9j3s_&}UTCv_x0n{? z0_vlB`i(jxQjyLL=ZhLSje_UG_zE%y?PoVvc^(-ZD0Tf|V->_C!r=_788OTON z#U=9bz&3p7Jr-Ew#FZqmj4Aj71Iv_5y_aS@LcFzAxqAuhfJU=W5}&XY&Zzi1 z$;kLS^bzN^Ud) zCGT03BNfQ&oy*YL;t)J|;GszT!VgLriUY`#^K%IXz<^)rGk=-W>)+&DZy;+8R5O8; z6)22Dq%1b9vk85I=Q?xqJjpU_jvrJCIRbq040J@fu}8JttZkVsjy~9kaQ4+Z*Fyol zrT!UB(twLRfkX|tLa^YD5;GJ=i5W_Tm?4A244EWmU=3v!Cu9n7LXObfMH3keno}rq z$Q(44S+1PDgTxGF`3P;uCq7j-3omK0BY3*)|JgX=L8U(-7v*1#bQFpQP}W`*Du89( zHL9v2IU43yu0$nqHXj9uVhWv3M0DSIh7etHQ$jLhH%I>@6+=0dzv2h*kT#?G zGK#1Lt^8=+IRqC@<#{YVv4*vg@*MyWUKFM$x``Dl{7hZAmt2AA{M+p)#aeB7;sJ>VCL4C)OeZ#tB1#26N zPMi8C_r0UaIkEaNeR6>1P*juKGuX*8V$btj({i;Yo9Na3llx>@33pK@%aO-PKS0z1 zG)dr3BEd!%fV?5T|Kw~f09l9F4r9ENU^={L%v|&=10GI+G_6WsfSe}cW&*kA804ZI zs9p&G35i9pc8J=w7&T2=CLDTME3gYV1l1pL_e9)scA#+)D?_3&c@+#B=q$|Mw+ono zD_NBFyKeyP^BC$Gy2U}l7k@g3PJK^?_nAQ8;X7W8E%XhAPewN(Zv%_C;zu*k%(x`G}aH+f($fGi##=8fK9n0>B;1xVxTBr$Ln|u`VG_z zQ8&oadC=V({lU!)DY?q@dT7e)OmKY)Bu}Ouzp&I}C_C7+*1$ZA)U)b63v~5f<1N13 zgY)NX2cpO3t#);-v=uAoZ=8*+q66!OfA;=6TpId;g&MG9ZJ>ovgnNw>m&Mqv;UX{B1yE{{mcdW~kF44S&AO7Ge9 zU0`~DIF+bqH?#d%>S8EWQc|U7p?{&%kMA_@$E?OP~mUG2x4W`04i7(6t~(KuAL=aNJ@)S+RsfU;z?Hs z5RdE;{s(1pL>V`*Sh-N9P$#i-0BI{}Sykx@JnP_S2Ly$wdf3a#?kT)XudARrpV9!89Fq;SXv+;!Vj%a*)1D^C$y4sI z3TDbu--4T_@XAx38pz34GQv5i_arTYsR0xJje356=3P^f%x5luF_ zzI7M7^OQ?rQvkG-bsDbG!Rc8FA0_!Zjqn{Dr|@Yae5!ZT>HBN=G^Oo#w+MSmbTtfT zLR=4QmnQT14>e)0>$|J}j@9Eq++mcnB)W*eQHjimw!k>9=CAp4-DHfvs zmYi`4bpaQhQgs0bQK>KE1ZwZS|7`CQX+YJO>gMXa$-pN7Z<~h}fB>jD@SB4+**VDd zV}>V9Mx<_j3O9(Y|9^%p{rea4()&71Jns=p_>Tmd_k6TB#`RmKt=|<7HPyseKPZGT zX!6KYUOvu#VJ8uWZ8{cSWGa6cN3jp_I1J>NURTE(U{k$q+W6<4@@+Q+a}?;P?>LS> zc=HfC)*1?qgP%Kws{3$eKGIk3Olf2^*i+KWg|LfoTF{tSg( z_J)uYwA3R{)&RUastN*Uf4jMQk+i_%YF|0tf4kXl@!xLf&v7jKc?jPH~c>~^t-V`x4L#p zkZ`U(eeiHppRAItF|sd%4JYv|{KY!P%JG<&u*w&&m)Pg3t?x3lXI0s{G&pi7j$E z1vXcHK7&bVf!>Lrd3Xj9G)I`z)aV&}57RTPr>xQXjwHb?0j~4BM}|TTA#Xg8ao>1g z7T+#R@?x6QRUH?|t`yEcCl>@-22j0?r#Wfs*WB^BZ_}Pd3jYm zJ%lB?Lb8G29%vR0q={%SNhW_Ys;{Ga(hcYl3J{mSnLKj1&3^K;OPULQ@?;?!J@EQo0|$j4=+TgHS7;Dhor zp-}XasrkyEIm63AZe=8PkpaW!NBOy+UoTa?(u$DuM6 z^NMTo>8SGTQuBxv{%jlMwP5!|^IN&TcehceCd*km`2so1fbM3qO!8vWL(qDC+mh-j zX{>Z!JvI%FHt)iEyocLLNjmR@x*BJV&eE_J&&%v25~UB=$;o7PB6^vWQTfWV?j0?D z#VlM52f7E_u?!vgFU;kW)l*0P>CATNmD{kdI9kl!7h)474rS#R58V^eKI;Zg0j6|M-?j7$CvO*0HBlu zoL_V*98p5wY8ge^r0S2Ov_lLnz>>8DBuG$K(5XDCo26sDm}3h+4?*YJUJ> zwv+~cw?m0^Q8#c@SToj{q1O4?7*XdV@PGzcWTg6*uvOP1b=x6owMa&M-Hidzc zs#tDUTik%{^{VHh1W&5fk~I0rJ-($YNI`>jrr$p z;n*3`LaL}Pct&eMP1=b`!M=BnZN@T_o+*~j72t~&I-6oKX#PY zE_&^EM6JtQT6@vnWO;{H&9C8GL>&m;G{@nQ>|vl9z2XcQhPuZ0IU*{~!*BP9D$}8| z3^KTkO8QzXfa-{<|Lfds^bwbNp9Pjq8$W>xbvlEpQ#<`OX2{3h#GhVRD|M|a<7lnB zqt^N$w%DnyHTsRx;H5e-3RbR)(_kf+?nSz(21Yj#?O4O@a5?mSvAh2+YM@IW)gJ2A z$@^Jv*LdG$)Sf!PHxPn@)LstVopiGgjs8ohIknicT&!Cw#;!I1vm+-iUxXxR8quk8 zxq(Pk#I(`@^#6;+%gu7RmFA<|2H%z2*P~DXNN+@KA0+ z<7H17SCb43m5e6sgqV)1q2 z^XIu_7k!>CK2PS7zlTK6vaR&oc@Hbbd>3g_x>!E;O}$v-(YA&QO6qsx|OA_O~;%>7uLO8&&-e!@(wK`CW%!Aj~1Y8D{ z;RZ%^{?brSg})JHF)B&O^4FI~$X~Wy%TRPAl70+x1+Tb z@|IY#4&R8Q#FGDWI~qKh4P4UcCeuEw%->77cD5AV82~ zM&rTZ06}d36WSP`u=0~xegem(^uflC(%&FSh)#TwVGti=n7BOJNjyO!5q!Y-+scz? zqgYO<&}l#(Sk=1DSX!kcg)DDe!~$qDGdWsvfXdJ#We1U~%JQd%kn{=g%OT*GOCtE? zkd66eT%S$bR>W-ayr>q*>M^p(3bjokXuB;DW zb7YUQ?e3V0&qr(2i|q!YiAaoc!TqX6fw`d#4wPgXiB;O5&_S?E`CXn+W+9URSwXpS zl}=SGfG>~>4TXBDLRI?y`Phpj?nT<&F%|#I)n34KP2RK7G6@;JD?UEgQwhoU4j+L;Cll3EzF&mlhZdb{yLSnj49VZz^y zsMZ#2#3Pq*yIpQ{PzowNlkH!E+$j8h*y%KA%+U)R-zMx78<%nlhmL2bXizh`Oxhzi zmg-eMhA1r*PScdo|9RhW2Aisz$>)}oL;j%C=t$<~G z0~ZUh5XtDrHH5sybvNp~dAODl@ZKXe)8^{-j>Dx)24tp@Nz!~zmA(Ys&BD&3T_hh5 zLlqHGfTBDmUe-_UqV}h2b^;Uel7sF(et|}@`;`|T975_RF??O&+mRTJA;@3cNnZG$ohBa4?J0J z8Dc}iRGBR8N#@srhEY;8(ux3JcBN$zuAr0f{$_;P03`IArACA;PVzURAF&bVeu>w{ z=M2V+UwxjcTPe-AV-g=W%Gz+oaCoYevP=L`a-)-agtHi=Iim~IRJk!R4pUxYO|CS# zF^S$=`8}P+Wv4Mw)Z%ox)T9F8C?IHM5Hv9e+8G2B83X|!Q57$jfG{~wZ_4}%KxiT$ zv1Da(u!r*Zr!%48{F>lu*M*wLd6cN80uGqjvpGns)tI$&?330TGU;un%=W zvIl13C4WQP1F4uRxea-go7;zaAc_WMtAIJ{sLFspTKQuW{vG9i0;z0C+_|`wg^(G2 zWhc`;gX(%AL0n!TqBqb%li*bR_>oGqA!R0X?_@q648k-;x%%WV+Fhl;=)UJiCy6c> z_mv53IT%e-x?nHt%_EvJc?3;4Et;m3_Lm_C;iU^L7`Qh*>Xc9(Q39woiMY#9d zP#=x1%7koo!n8|NE?=p;;u0k2svMXR2N^(oAqE_;dJw2G66l%U#QkTkvSPVIQ?Xn^ zta3+sBFg=N$~j_{YmF=?QMnb2SJB=FSa@W_@=2q%u}-Mc%8m8>WLKU@jOcbQeO@1{ znY_qmoKy~}q$-E<*Slwv%)vs5NGw$iD%(Zk=eA2I{_^&pqe24k-%%(9N&6z2aVtq6 za3-Y<$V*Ev;4BAP%;G+p*96xh8Ly=!Jzh);t2thqrT-WT0UvQdx$OK)be5OfTss*K zz2`NwN&CI=%HRGO3iTV6oPUKv1yv4|-^{o~=T?-j;RKjaCsrANl&bMnMz`|d{+TKR zh=Yc_9xd-q%1emIdr8Z?h4LJnhh{o%YU)vHY9cg+B3ikHIA!u!;iv+R5|>CiK>7tO zf>}-imKaanMOxJ#@58F=MYCAz^^ga%k$edLRSlv{7nekt4at@_cEPkI!^9jSqJe1G zz;i<(vMMZ|OVeAufMRS|#T~$XY4S~kXVv#a`6j}@>bo7LdB)#}*nDv1d8FveS`d)% zyN^PlaDsYXp#9&evEFg0n{e!G{uy9wl6V+WrvL7}HXNHRa4hb2uy=sAFIgf+0_Lj11T zE=oG6&ID0%JC$^Zk}u&KQOZ~{yA(?%ijsC}Jx!DxPj%*qk~P$Nt|)mo)#(-`x4;{E zc~aL~dHO@#5m7W0)5~bp9K&R)hEXAFbir|NC<(lPt5g&yuPg{1WM3kxuPWhwDg*t~ zlp;rBgY8c&h>4UEGZM(?C&Nngb%ues;|x39U1XSvDA%d3tdNg}y2ztZ(7`-8G8w_8 z20knd^kNcDeQZRMWzw^Q92+`?W5Yzj@gyS4LOE$7CnF;8Lk_tY77kE@xz5tVYKWgi z7zk};qMvyWBYjLeIGQ(A$LohKH;X6vFkR<)r9A?hAs@@Q4BezN zCKKv2jLO)%q`+k&1QjDQ5rB%ZnFv2c=uD$0Dw&ob8!?rXC?xK>MTpSL1WH2|hY*^w z`-xdP>Bf|gTxa~-aM!Lp9}!Hk!JY3h9MhPXX#u!y>^z{4Kr-f zqfCXw<=Hv@X7sTg-g5pI5GkQtBvIh7>jVxfI2wh+7C2Doj)ejBO;PV0-=@|*(Brxq z=M=WRKfgZ|@+90&Id|Vd{TeP$T<;-Co@bDH?{C~nG(Dm(C7r*x9=}Ko+@^Qkj=q>m zYbR9^IthF{Q%6=5ZiBSIP?Yah_IwYc(i=Jrm*a8fzbfUUelv>&4=6Vd&IIH`cSzh} zg{SLJ8`12OZj;zf`{yB0NhyiU!#pZjbWqMd7K? zq+uy-t}`&7%VCI>U75~hWayad^fYBUmwV?+T>-cLA(O97U*BE*>74D763Ei~PT5pN z^JPly@tLR+?2!&Uq;DGIYuEd>>wz2kr?eAm1Q^=s-%M`utHVI_ymqKKh6sr2no&TU z!Pruj1iw@obLHN-6J~q5l%FWl>)R~r+p9u(h+SE>>{kAsI8NB|RCckSe!}lR z_-LlC$fF}CLrQ)q9;u)?KR^Zn!4sOBUf23(VxA_|{h|YYSJpM7@wNf&q;xS|s4|mv zQUT_a-0ZP^ZU3nDwdlhc?Q4&G9zW8)_Q{iB``S0gk7{50OziAd|A*(cuWem1IZFSz z&X3~^r?j?3=Xb5b0}cP<#8%UJ!~MnL{cVZ3{!z-VGwD#QIw{(>rgoJn_~}Zrq*gtF zUI^>(elt5ptfPv67bOMkz_MYEL0jF??Y9O;^k>+8@qKgAbtO7(LJPfRbEEH7TOCuP z>+?jvkLsURx#*>#P@oxGQhn2Qja~mC+WVkGM^t<69CoukYJJgFDeE2>7kO8Hb$X1u z^47sA!^f}As$HTv5IWcyX0AP-9e$}38s+>E{#(_Kg;2I+QmogQbi?IIk@?AUN9Lbg zMU7YKy zi`S-#f;?NMkkgWPhvIdr+n@0It(Y1$AJ}{j+V;Pv!uQk>{*Ft3KNOM`Sh)ejh<=*2 zrbcr0CEOVwrN8Xpc!2&6(qHn?xPg3UlepJURG6R0uck(HiUj}=?OzD5#e#w{HWR;n zC0+dYj$wlJ)~k?U#b%LU{Vl$c>qIR2^i`;Lpc5o``<0~3AV}~(P&L#h7ByTITYq5W z{q?gs^H(eXq{I+W&o8Lku)#hhWNws;~wYba34H&!?U#qr`@<$dq`r0Z{>gd7S zm9WH-_2-rEz(&%P=ut#aw+-vkd6)XOZ6TeK5;lb#`t(ewq0M`<)Qrinx&pcjtMmxj z%`ef_rq?1(CgTJtzmVKtNX@OBVIA=aEmkaTeH>_PZT)7P5uT+8ZT-A_($;^do3!<5 z)+F+WDG`K_-&&&B(R`3ZUWHB@q}scv3$*2*`Y!pIeT202@n{*Ym5-C<`BTKbkLu@4 zfLTmom{|y14!DOx`1_AX`FG{|p%9_j4e2obLFvE+ZJ!3+%BeA7CsnPcX;J&d^71Nl z@(%mo;8vba8i4>-wVUHq{NPR-!}Xj=&3&;)}`|)WU2o(yl8%l zpW++$g)ThA*d56Ki5p`^#x%M}H^$lbvm4`a=*C#d`OZ+Ne33Z69n-(yy!>zh&d@0_ zLu;-Xc3yf$d_QcxuDHUa=|qSV=nWx^Fv6|(+I;CDgM-rlQC2>4V}8$6R) z^Rzr+2z~eLv%`#}B%)97aNr zj4H%)%Bv_~?tFuqa-O`3A~no6sruU&%VXpxHgPHW2`+AupTJT|PUVm`A&5VoPeUk$ zGk3q&tolHry8*gZ;b(39J<;QDwa@qp^wHU0kED+Z&jNj9^qGl1>TGPn;P4~Jq)7OJ zQwL@Pvp-MfjU4Rxq+4F#xs=3Q+T^S*+k;)o8}JuwyXMtc9=AA4c^O1a~Vmz z^f3_AObAehF~N>~TVMl{fEEs88IVY7 zAfXKwM78vJVze{WKl0otRy zmP82FiaVnsi4d$+nyw<)i8Iee?+UGd4Q;iVb$k%If#GhV4r(2Hka+AKQNU~T?b7$h z`Hq~GX!E;;bKn;pXNUW@ZylU5oW7tj5`A%_a|8h~oZlRngR>Q{>RthbnOgc4=tId$Z3^$Td{MkYAS(ZhdA+2fWkIL^Zi_yL*qWz&sU%Gu$MEg4@sqME# zw=b{K_%GbfXJ!qXp-BF$?TV4~*SY;6nuR|^gVfnSo_n!&05(a}bJLm7ba;}-Uu}g? z;#oyY4YLuwo~8?i@n4~5&{2x|F8oLS`OEl^Y&#Yo%YWpro@4)!I}Wq|$R|G2k0vF# zFLrk8i>JSg|4894ZPfnuivC(h_BR>&``OQgzsn=ZRzG=^#%{fTYSdouh@IW~ouSct z4g3^dO2{RFA60t1uwKvsPu4K4O0?H>D?cb8N5*!PgjRCN8?s_*K4x)A8gr^x5a5c=Cm;>h^fn*Rwh~QZwPsRtod#7Iun_`m98xCY@uKp zh~XM`36g18Y!#g+lRAR56dvb0=8w_UU4IYiTv{4-=veWWqy>qobPW7qAYNR5+K|-w z12sDhkZz*T9lNTEopT8-z+K9-wKH{^n=_I67C|(IHeKQ5dp@gdySnhWGaXYQVy&{Sw6ti@B3c zypwCqJp0`W>|%gZ*T1~qO1lu%QoYEjniY5dES*$rK=WK_SCtvqQX8?b=v@gN>fj{= zI^st+cui%_ulv!?bXFVZBJ$BCcnt#E0p)BRxt*90 zdH--gQ6N>^6$@o!j*yVo+fL2~Srs?Zx1S@*x1Uq^_Op_2KO6b>vy)a7U(DI)evq}Q zk_D8c3c4eWv=hpl>C$8%R`OBd${oWCdR>ikg06amP}a92H735h>r3Hh$I+PUHSTF% zugyn!y*~TbsO$Ak?CjRt^<%%(dO2mI^~GfCvuVItH?+xxKXE4rJWqWxDs0up&Tjop+__*2-F4&TAu{#JHw-&BTkpct zusBx@rGZ_g%R^H7N9}$-r#Olj3D(-b4WfDiu8YbI6l(< z?MQy{)oGFPgU$G+iAP)Tjf4kSa@{m!&=pZ~{&Zx}Pl%GK(~(Kk3Hkjr`?=#2UU5;R zJT>8>$oqmx7m*&2D5<+Bvb}Fz6xm+kMUn0O{UWtJ-Y6bFuS*~34kvQts|E6O4lxL_$1{~B@iTs+9dU*zIb zh!zePPg_d9qHThT*~Nt)3>UUV6rKZxJzKfR#dBktXJsu=4WrCmB)Y=?`oHek%wHW# zi}{s>zxp(Ymkw~E_~}#6GyKs@u|IkzTXS*R=2=fgnGvI)-@Xeu+?+rYlb(VVc^uy}cKngG$)wAk7 zjUaf82!e5v?J*<>U% zk}RfB5V8%#CmFbLw3TCqL$-!!jGcl5j){WQSu;_ zRNLEONBoG*A$}Y?L%UB!dsH=q61xfBfHjo3rLlSW(zo(@JWJ_+|z69fk6MI;&G-ZoDVwYreU|0^*wwp=4n3qK+=AR-6QjI+#ES z_4p`R&yBi0>nvAWl|R0Yy7eYN<0was=hnt!Yxf!&Le)1*&A7qd2?*5Ca4*H?Uc zto`#b()M=c;<)XwRDdT%Q!IoQD_5niT~XUu(TTB~Qz9~k-WZGi73uTp1k}u_lNSf7muT$rpg%XmVW&sMBx)h_os)QnvZPX<~ryGf|;X4kG^ z0ZdQP3~~QAJSY2iW8vgip3Y_JPn$Z-yMpun^#V>*%}i+n4gp;o?{#8jNKE3EN_Jz4iB0vb9yR)!2Iju*@_4fUnM^qC>N#?p(%cjUNyyE z8t$9t3;J&Vg1)0pohL~B=h`9+_3c{UZE8umkj7w#-z1{^3Rxa`xSxDz*@GN?*8{&! z?Hdum8p9UL9n`~iwf7R|dqjxjUG2xF16Bb*n)7D+dl({l)q5BnnF1wGuu+I)e<6P& zw^iwa-Bm1HO9<|EbCBc`xsBj7MyTW(Qm|KpBj?EN;whJM0p&T)A~|Vo=mtJuvNT4> z%l2N&67?SVtxeJBeU3p5)^c(v^yWK!>|n z`V5F|MT(c|P$3RYVhf$<_#4g2G5A)Zd>n5tP^t{jixFT8oy!{z)_xBr%}e;fKhN;J zHpEhlCn!xkZV$mB2OrdM!92r=BL43gq+KH^6HhB+nZF!VZ%YcJ{jC%f!R|B{mjLGa zq=-?%+!LshS3Orl2u1EF0qHK~A1@F_Fi%#ergNW$5w_bA9n`>9j-qp(`}J6~rp2p> z>YJjau2OkUNtIgl8mzj&6V#B+T~Kw2XNUIw8Z|}HxHXh4kZuOFAq9P>L)G7axpHW& z)aK-?-50IQeu36@omQ)n2T+7}1Bf`0Usyyg_w8;^9Vg^Lf;yky*%yzbOd@((=P(Nt zTOolX;8KeAh^q&b)ebE>uKIzfjMJ6({=!QXj{P2%xAu=JKQRmW$oD4< zXFYtT)W!bj7vYc6QNpl>o5+twC^h@U6X$e zKl#>BYGz|-oW&#KU#CVaRU7MO50}2+rKl#~x#_i;x^j$c?bM_T$sH8_1y*s|>|s7%y$u;THm;m*=6cwN*NBAY;D~j@H{k>fyNhTQ&+U&9Hl{C;*uM z%1lJrNfF|ue}M7gU>uT~LXBXbjk) zw4q2Hx8jsG>60`nLvlhuKL`MB<n~@d~i9+Z54Y1$P(@&dDGGGAa0TLPiqGV1&s75BS?+ege#I?Rv!0N?M{{Bzj- zHtDqDWc=yisB^V3c6RH-v%dJomy}%qX=kt_-s>->iMli#l;!-9Ve0s1w10dTlOQOZ z5SlP&FN!Hma(+$S(MgOe+2pgXi|{yN+fKeWiqKf8DDMz<7v7mdEHKu;fs`g$g@ptL z{GEP;zDq@U_`LKC3Mg25QA{+@n56dZQubB2QV7#7i|H5HI<|vqSy`wzy zhnYH$jtgd97<1*S=4fQocF(CPShVSJx^J5@yk`DjE_}egB~)q)g>}`2dc5nJu2EYD zP;%O_SKMd!M&4)NaVj45pHGQvW7nzpNWAA8SQe=f-ie57+bISU^d2U35dP8*bF*tz z`>v_b)l)4qb;r=JahQ7-&hmQLC4rN@5jfZzK>~Xtu(LM;8+#+LvNr-Vdm}K>Wmux{ zUS$xn9g~pln1yV|D)h8$LQl&sJf|fHw+h&FrjP~Y2>l9*7%ELgRiXmv{1S<_0HGkg z##MWgE|_Z-SB(c*?Z_?pP)b-5>Pa~?w*@ZOuTN=fZp13H57l%eiZ59%0k57$%x z7%!<s`c`63P+u^) zqiU?!WA6^0331Yh^^!(iFIC^^$ZIk6ox1Pg^;&k4DTP<|jJi@kiJjfL_VO>MKPcAA z9k{m=j(M?KMJ4|~vRcw$!%3BqJfO5pkGWDg+Dd)@Rc0ik>@tdB)9o7LF6L#s;^W9= z6Z&aiYQ2(Qjk#Vks(HOCPw{$<+db-feb^m)y>6WI<<`sWyM7s*wIS~`&kAka)vw^V zO)&J1fWhe&3+ZbI{#ouDUt?#t-k$WO^z}yQgZ+9iK0+UC(_qx`$?^l8>a&o@l#y6}ym~5BM~fsPqlVF; z9pl@}N+kZcA%sqHhpWqC5A9u{P(_qihp6!=yY54m9OvlUrQ$?t=!WY3Bdg!9ywVvF zkZe?Y=r^V?TmIMLp->brM)pw$l#emIWaBd%~`n8F&`*jK7T5-!<_<|L>16oL%~TY2T; z*;=r57<%tS3=ppvFI07s$#w>%bQC{YnoMfvVS-oMrF7pq6Gdb;cke_u93M(|!fc(p zplYemJ-lLu8E3+`BcX%wZQUzmqgy>e37y3VsTe#`dYJ5QsWCR*=0~#Jrp7u~(O23H zcLB8OWCGGHm6J*ihOKf;Ezfr$;-gppAr`do9EGF4i&sGnG9m$}amEo&L{Q zV(Q&=UKpVhN$lJKwnbKcFuZd9;IRuMn7Y0E!G>dWr|==&DSTu20EB`Mb!vb+RksbB z&{gNq?UXtl95;NqK{+ugX49t=W~;$RY%Fo1;NsbTif6cZ%87U@LP0_3d*Lj*Na|K% zS=)>fgl*JSK&CdY$#vFJe=oc;fg}F`c)uV9@BI!Hq<)4M%F?h3{<_Y3AE(E;o zt{-|$!AfW$T5TMnwKptbAtNEINVTmtS|0jTr%h>Ju=>aUm$NqWkF2K$IAfHCY>3*l zRtV4{TA}w~Gqpsm#F!8ky>@!|xL3<1vX7O0{qNQp^uDUq?(&kIJIfE+a*1 z+!)V|kBLuq+ry}gZ<++Hua{U>SceQy{uf%NPkrx9hwr+nDl(aUNd3 zT*KAYCQaV(udSo5+*`4;TZ_`Z_&!CQqq=di@4u%<*elT~(Zs~{NNf3pJ!eAEH!6ak z|5Ec|-5mRV=}w-H%(0R4@&0S0%l~6%w|-~tm!FU2rkL}wVcMwk5ox`D!-3fI@tZF- zAG+U+n2&2ok8Ev*Y~DXm$(}t+@}dPDS?to+0ec?Ul-# zW2rlZ+#Ptj1@9ZOhM#um4ohhAS@Y<`o8T5P%B@BWmx~v`hr*Y@PY2c0JWo5A$63aOZJ*#eJ^EP!kup=v~yK7YsF;#57StgpZaVLqecg_-Ij%QTAXs zyASosjz{521WPmkjpIUd83N8k;`x%gf?EAS*Ptwgtal6fN*e*+4T zI2B9O9L2JD7gu|EQOZYw=bTiYB2~rx1?zLaOT~RV%`Pfe^WKsX9&p$n8&S9?m_oh6 z7}E;!>L~~&y@EpRd5-UY48yQ=*;E86Bb6N#W!+P$-h+l=)GTV^x)u4n@SPl#W_6I@ z3%z3^e610p2g2k{j#@ge+QdDa? z=1Jq4E+5`e>q@LC`8>ghMbu=DswtBc`mA}LB&f*apxXRwt@Q8tf&nBNpTgGL|-P%QJaOgNLo=x>XivHShGeT3|^Q1zjIWHd}YyQ81l-_RE6<3*+*}t2m zlTsUmW$-0Y2F|p1t-|?-39U^wz7YzQZ_(q&=h6>x6cnc%5-KA8GSZsR)I*wpI6(Lz zAG{cEJx%%h7}Uxc0Ym1=bb2%6nHS*-_W+yKDmToVrDHF>2l!4@*~B%uiX@LgdO#Og zpm)K9)lMitjU~JC(c?39Qjr0vw~~CfawV)0%!>m!&8CF8e%R1U`X`ZhUR)UE>5VgW zbD{U%TiMev%rHhO$alC=OkVwUdQ<1VHb_po-Uc!Bl=T;8ssG{@&snj63c>R)V%~cdUJy}V@i7CJZ zOu_~HzLgd*4HmG|FjogPBrXoGV4~6o#eLAE&N~^lOQthXdIX)Wxx>-J!iFt;K6v)x zKHL}9*SKL;q%kBJzwA?XVrrq&F3d06=o_U1QlT}Q&O1kLMD?LwqdPmy&2aQF)Mz^_ zGv9T)e7rwPK8|~u!)GJ!kcu`feZhwAYD3Dw{tM1+SO1xBI|j{gAm_yDN zN#jA~0R|BRf&+Fl_x6XOnX$;9$0rWg3=3bIdp7Z>*V3mIOGbQrhxTzwlw_EoX^v2$ zaeFqN*I-pX`FXrj@)~#!s;HFh{#O;FoM>0n~ps# z_YSF^mifP9dxoOo;|TfvsPtL7s{i_I4EfQX_6Ysg-;J*S8aumnT;^!9ouw z4im58+`&EYH{#@hrL)KKoSw_LylmPD~`QNWqP%y|I=0K z{jaVPjh<&kjQ3n?m3q##N;~IT(K-af+81fTum(nYFsy}wHYPIpX zm{vlLl%SVzD5e61l#U`5NGpYZD6Y4oGhj$hz=Pa94Y4pBon?CEH=oT!f+s!VxJ*-z z%QWRoGa%8M(3Bvx#GR+yFm5&~Y-iAMc^w{?$tj1z=Vhw$+qE-wo*b%P$>W-@i%m)W z3~SE9nr&F~Q4`hdz-lqh0gzJRdTmP4TB4+qE%hi|mC(#Z_}}~2nUJfa+Qfpa{;7DL zm>UqH+TWcxK?v9cdKh87?p=a4os$AvS3?w-b(%_n9e9!`u>RT48g|aLeqpoFVXNpc zOuq1dxaXg0hkIjx*{f~xb9C5t&JO?We(G>gBN;D>(cv#{HWo&(1vD%FFN53zv@6}J zZxCW;^8@Tu)Sbr%kJ5AbxRD1qYJZpN6z7_mMDM|IUP{OLlr?mmyU`>?JJEB|9$#s- zpAMx6n`J*)zc-O7sh@26*#|&ZBU-UjND#6w^^>)=pG~|YJgCBc`&W#^JrVXBF(eLV zNbJxrhhRt&U|Tr@3oAW2?qnk5f@go?qLfg>R5HDnvJR|?AcjuIdwSWI!SoVT!m(0hSpQONvum$E--BIgw_0~tKqo&m1EFn0a- zY4yho4kqvdEcZmRi*1b@?+St15 zp=6S=>>U0X>nWlU%I6OeYO&+qKnUtDK@LV58}01iXx&(-K-k42zM^QE=OgcvxS05uk&+B*sPX7x(wbcI#mp=ltIs|5QC^!3Ir`(3- zc?l(X`N~5uHORR%C8Uvsn1CxlvOv`Xo=u=m8(@5r;_8R76>4LPFja%G;WRw(V;Wnj zHny!BXl%^+O)Dr67H~xc0&bU0o1yEUuOjjUWes+YW)a4Jg{szCkcZMZ`cnPy{XdV@ z!E#oQWl&&F)cSBl+=i>Ko}~->dz^RQY_?;obS6Xy=1}%8E_l%YKI?*3+6faLvN&g@*wKhPlvhZKJj!PD*B~_swmt4#x9ayrSOI|U& zr;H1(=8_5Q@Drh+iB`kF8V{t)DWRnbW4X zG{nPdd%WC%Uwu3?*45X6Gul+4UK37L;AS1o7Ak;yrjuUxvQzjmo`RP8w{gnSV9NI4 zl$CLTTR$E)5v9{nWSf76r!3_oMB6X2MlnUm;DE2@=K=eSbT{-}NaYEMMSmoy^amO` zsX_7Zjv3`_fPG*VU3nuXXjNAdi$SNm(M6F|1M9O>@8V9axeKOB%o^&apaOD~;YKcw z^MK||HMYLd2o?NDdBKpl&q;oG#C=W<>ObaUG)%*JS6kWj8PlBX35$>pQ*Ha3>?kzo ze`Zvf!rs`~txF1H%M^&dIuPNH?6q|x{l(!#p$Cl*@t`P5`6%U3Lt9C0A;s@)9L=5N z?65B11=4hB4B4Th(D)@gU--1bZ|&DKmF}7?(!!iP&z2eL@pJt;(noFhsM_qO(1`sr zq>uXtk+F!t9>T68I~p$qaN0Cuv7ZgFNNVv&{g=9wy7=g;y8 zBS~~AB)QFjKkKu# zbciSB_R7xHq-Ul0`eR~=Uwx)dPCR^ggB&6s2Ne;PrP1*gprvbL+M_7f+>Bs;6qYun z?G-dg@i(Ipt)DKWA_E`E$NkNAjuB(>Q>0{nBbkUb+UPGy0yc!hE3wR3wee+pU))C= zg6(5J3lQk~h;L8M9WCNq{V!mH8k0obNjc=|zzYj>tKuE!R$Bi}bL6K0!G5|L^S9XI z)XO$k2a*g}a4i=EvW$$`A`OoX3$W1jeq){WnKHH@`PP^ z$`4`M7))UDQrg6YW&+utOSaWgGgWF^VVxFRhTx7Rt>4x{~*!?gK=uKq3xEY5R`KrJ;Rkhz*|G{+d$!nJ4Rg+2I6@01ez`nscV#B(8_o$@5MH43;=5;oGj4 z?Y?%iJa14gHOTW$$fYLN2D&K@y86B2ij?MmY6EO&lY%RgrDpuFp&QrZhyT>pBLzB3 zqoA&G$B+GlR)mM#OwU$riWoQGs#tkd%+afY6O(cfNs$c^Bf2hPL}Gou?06bJ!treT zL>o^^2*@Owt|hezyzv9(Rt%(&pnRhn&eSfwtHx=rIVvGP@SVpd>8?MH0l8CQmSJDj z7Rtv{x*5O7g(g#6yX-b#2dvNm7Zj*@zoIXQYPBXfms1YmwPUR#CB*mhSZCZ{^OYOA z`G&sB?1KE7_bSv6BJhVjBE;XMD!o#5`ZTI8*`ZW{^0k#iA)7!q}c0AVCuxFq^EKrhvmLOHrGMa|O`w2w;UiSz@tVQKD!o}?T^dQ-cY zPK;Q5v(RdR{pC+@BG!pFQUYGGB*blp?)uu1eRBA=o1xcyMjgr|(YY<+2K5~;;Gf7_ zR9Caw3t5rd&@GhOZfsrA!`tOJ$ag9&*kGxu15UuyuNyh@5oss2C_aq9jaKh1Iuk)ay@1JOBr)gGJ@3kKb=Q|s2#j&$eBeE3U&_)+>j)Ndo#}bfzR*F~ zP^&jHCvQjP5KHs(kX?YU#Rv}pTZ2yHD&w}t;zBG9ZtNB_kI4GZgV&Qjlup)J8d5QD zHGz*|3^F;FXgAS5fEPVBihv@t!-VC8yc(BDT+>^mkCA6kLxMyw<^@$_JR7IYwNhx1 z!aO&CxF`RWHLv{8s{HGKNhd48GN+rQTh&tnnNGK06qnf@n3(A*G}ju@cK;ZE?$-2^j8J8f$qB zHF8t)(f>|fzT{3VAo)TFxd}J$K**h_&+DGiv5-_=&2c=(vcM3(C=$h#&M{+koR1f4 zz8!dnW)+c3(f~-)P{Dw&+%hP1Nd_e19%T3nI1wz#$~g9#V7 ze~Icz2K|Q}+kFNYk+n#w&RL7(>ldm*NK`CNJtXXLyQRm~Cj4fr6bFaf9&xy>NdV^b z0+(?e{H~lL8;`=-!~z&;kMbJ>bpf4f+5tck%!t)B5Vfqi3wq|OaXNJ6i>n#1$DKGG zTB2^A=q^gT_z^&MC1V*(?BPU&x;`T!m)>qdI|O-sMp8yU)@N+`ZnAaNd-P$U)7rZm zK9zTe2aVU1Rb;4xJ`7<2f_sG7DQ+Zt@ZBK!v4aA3;mMBjgtt3-4z4}QOPVaNM!pIb zEr)JfR}b(Tp1^6rKask|sjp%x>WX+yK!Qi%)aTA;gI?bNDf{ycm(V=NoyeB_Xl$U2n>bE~ z1S}%@xf5A*^(^8ZS9|r0Jf=z1@Qo9Fs7H!sq)`IVya_#`1dBG54<1y@4bwLj_5!2I zpH56*`O}u#Sn-gp21oh)1Kk8G!z_covvCl zAp5M^GxP;U?eVtQ*{yed_gp;|`s$0(Palua{~5h^j&t;GoM-g-0i_>D z$m=FN5GAi`U78Rb@9p?Q7z?zm>EU7{0jx<=B)dPQ{$RC1?(R4A$+E)X2+gQ6CGDWV zR}QGDOLr$?Hn%}}e^tD0yGhq4_XQ&zcys(zsSZWR96K&LB)G_TCl<4rAG)M7Yv#ai zluXSu=vg76Bcl9LQQ1Mu%WZDu?L(7vY}7>;LIw^1HVI=g>zY>K$dD;&(;lPbIWhdB zsPY`;scVtnVFgk@kXv12H(zwMRq3pq2&ZReJvw|i1qeUTXfTrK37%genQXHT5}NG( z7CUA%JB-AHnFEbZDwsgIa!Wc+y78Hd5O&irr==8AS~BRljQ(a)84CZ@l7sbhidauA zxs*eZo?6^SR0>7rzpkmJ5UXz~qQ5xga|nxg{Qo?se;6LE)Qml7=*CN~I237_fFie6 z=|cwt%{aQQM$E_mI-$fE<$KF!0#e3l!FRo~h~CMkAbOfnes$f%NH;UDzCJY4&5ZUR z!n6JQp^zs-#Ed!pcFgONnIEWf$f?3IXMQ3#8ts$LH()~#qX}|W4!O2U4Bl>#K9S>t zw`Cq~m$Pz%x0{4lUi6!Cgr1BmE+PwBr=}3jhb+5lxj@1+jNVJEz-T$G5eZ;@yR9%A>GbyQ0qaxHDa?%SR+Uokd}H9o{+!nI31!1@NrzgzHX1VMTB+)!YXZ+?S|brLJ)0 zI*C$)=UyOeg2!nEDPLWksP)?;?>zjzygR(kk!uMnXno~xi@s4I$5+v%q`bgD+HMO> z(l*3-V#7G(HX?c2X!Eo~6&(N=-G@7RcOliSLfuORu-_jRcjQg~IY+n$6ecKSh>43}-5QR9pHc{Gu_`3=tocMO873C|P z-#)_t>)T;7Zk~u0V#$i~Yuc7zfW`2EsG0vncxzxhRTANd@uYG(6dA8*!e2+DHN_SX6Yo^@Fr3AC z`O3m0ByvtBeaB=SoQaN;z0xKhv|KC z?^N^z!N-{ijxyt3cm?*zrgEP79`qoA8`87YNA<16`i_%HG}I+4Sld|IUz7XZ;p*pA zWA$VD60)-nWzYVY zQ}s!&s@?Edd(&E^mpq1<6n*3;!#@x%?GO_v`zmMZ>eiCt8bz@Kz9+Lukch- z9*3_ygAS_R6}vy*)cUi2yQi0mHa(8#n=*d*2YX+}4<^G0^3!-^pf1TSuKe)6VP)O* zSoVWMST;kHC5s#AH=;cMi+GkQzZm`dtJ*W;8>XBA2gG~(W8ziNFwzL+HT-QEt z0^)NwX+uutL`*0xf)8NxPNNuFC`9vnmjAz8fFn?$_z8B!)A19L!my#Yd^E7zFB_9r zQEX2GW?La!x{^h3C#uhcq{q-M6+fRom=7dLAg8|f@Be1@E zJjXW+9fy-q7V7CYDz`xwJzE(V-rN)O z%THyMQoh2og-iZ2cFBEeNjj6}W7Qc1^U6K3KiH&w;J_{=hM74{%|I}7J)Y;d>7?N` zv7T1G9NwN)x~8?r%6Oi+fNX0&`ynauIl36yh>7@j1>n^+PWJ>nwKN6cRXcJg42u{_ z=P!~hnm?;set{OUc%b7wb~?Uoz%7k8+1q{|FNi;3YWoo-WP}rXA;I!;BO(y2>?3+@ zE)umJrq>O{qPhcUu|%W>mi;EaHJ6E^1(Zje7S6ZZDv#+OD;rNNNY=bZC@cSRDSdg_ z&+#HYN7N(3+p13TmjiFF{bHCu%OpIa|6)B8^0;Tx9h#q7QGVY&Z00#?ckr)p^X_Oxc$sf7a7PFY6GVYwxhD?s^+`k)A6#X=Q-M45$cJMIta&>ecEgGcVav7k zee#mNpsQ=;m52Bq#*TJ#QXUFWU0uQ5*mL@f1etQ#gG054@nm|9L^4lG?8Z!&`yRe56;PC z0GE8SWFanIL_o|i07cD=e1?g+c-7cO^&XnY_B07>_F`ju8YkP@nAs%8&bBrVw!^WK zwM@F)NYxUY$c=6m@1&Czj9pm4*n|~KI^7gtj#=a+h#a(RagcqCQ?rjrgq#+&J{}E` z{XC43twuy0CETYlxBQ|_$<iN8t$+=5HB)F5Q!8NsC{q7_wJF^x~} zCMzseik>T^=oF$GyDYk@!k}ApWx8t4MIFd62b*cpbirnvCs>>ed$8GU)FKdNSVf5p zL%2vnM3E%))*_0S!bKbrMQ|L2a+?}@h&j1T=u#UQP2t;Kx*|xR`>QtK{&vVhA|mgj z90#o6UU@sPw*&GXjpxjyFPPul&Oi$MX1gp*FTuHjm0lvm*fFq%{y@{+Kyx zbMQj~GLvZD6v3E6r-D(8DUZ1w#+W^c9edoy`^d(U-TC7mEiqtUxN2% zH+{_eZ>KGQj5Bibd*P`ACs{o0Ru2^1E-)4w3N>gnl&X3x2ztrJ&GadBi|#_nlt~;( z=nq4chTS(2*#Y-cw>)7D=oE0K-stASOQkN006RrBB9`D!&2ti)qUzI^JPh}(;9WgRG+ zveAaL(Y=*Z0A6BHm5<;pR}vtQEqcHp?heV?7ewi3ST1@^8)XRkLshOhPkH)u2pwhO zHQ;__Oz?CFgAcrV$xtW~|BydGRq?bWbP%tjf3myqf)^Yi_&+kEiArEEHU9Gz;ww1@4{ zr%O}%f+}`HpAMvD-9L-WHHZlXtc2l*>MHO?t4nteG8T`1FJ9+4<)Jp?+lT3pbDNWf zw}}=e;dZA;anLTUXBCiG*ycs-w?(w4TN=^cCD5K4D|5fH^JFN*P6~$|2?P7M)tfph^$J;ptrKvB&XbA?wYOD5MmiQyu5O!2TGPaotT0I5U}=P1l}6Z=l|?|+ z-cD8_D2-@4K+*^YiXw#933e~#_d0FL|DbDL3ddFHv~}sp0A?Nxa>-m&@_*|tBATrj zHihF07n0;6Sqne1gaXiL7MJfs+x`3Ys>-C+KqIoUNN--=ii)3hLAPFjZyLe;{huOb znl32Qr}3>>P|M20__m=?lzfy*7KxG#RI*r<{4td*6(wg-$ud!L!cTc1ST6J|mdg?4 zz6m*~Y3Dl?@{UJGt-61QLKsCr-G&VK1*i`TMGK%r%S)$+1w3iZncXn_{-M;V`HOXj zFBNojBZXWg`bnsJ7KH+DPzP~?}*pwsxS06W1{l4ZIg6W zdVeD(D%SQ%>=Vg%YK+(DI~9k;+aimfeVvPAqVjH=D4wUhtv>DztV)1_p3I++cVgB3 z*w{m7LLF*<0*y!Da}jS^S^VS#Qc5`?)Y_wN=Y+ivwjJl4MZx0ekEnv(ZHkKK{|qXk z?iS~D(U1wsfAcnW&E(tm9OT4xQP=Um>bclzF5e2o2^LX(H>mGXw+5{u^1pmy0xE*RBQPT@ zHETS|Zw_J+8WnzX3P!~$w+zG+*=3Vk2IEyPKqv62k#gvM@ttGreeedkkqXf@qndcR z+(_l=zEMrIg+T|_1@NTXzoS_J1HPMO4(Pe;`3MA)Q$a>OQcy^m*LwFJv^3Vlw;LI}@_BnwKV~3415D!7&zW z#!*HPB~~E-wLEkw^q}t@KaIIB2xk^ePrj1;sY%Qw3U&AR8!@?hQpzD0&7!5H!`Gpg zXo>>q<6Wm}XINfrf8x`STRC|WV~b;Px(}H1<{|nvU>L8et9hC(Y_JMN#AChJqiB~Q zM}rfUp0)FQ6SV^*|A~0bb*5c|sss(H91K+wwdZdpSIRADLE_)A9{$eRKBwK^xGoHL zEgP})egSYTo5IW4@(hCdP-Q%O3r4^?{4TpA4!(?YdAMDBr8`wyl+CBjSa9_8t_3AB|lbY=x0zr~^V2wx*ME_O#CvA@45kXREV zu^s<4Bqkp(4kGdS3BrQD`aF-K3I4yin0(G7RoX>k#Q$X4EMfUh9#>!vS74i^R^Sj7 z9OsWT)mXF2w32EpKqF5fxvPt=ZywZ_q1D%O2iG@BJqWB(Ztq6cw@j8h0Gt2b|sxLX~@>m`-Aq z8PNv2V@P5cO%mT3gGmN9@@sa|X^v!>1$q>v#4WCsc=$ckR}IC8a;Upw;)CTp&t`2B zN=vnk0RPfht9G76nxI#Kx**7bZApT7NJy64%EzCW$p0JZHy)6J;e%AEd}PA#7+g#! zU>Kz)vDy4Bh`+iBUlC2;xL!Ps*w}IEx!%K5kDQ}Snhk9dVZp<@QGZ?T&KDu2fpx@a zy@)d1a$8<-cRnDD140&;3e&hph_DJ#?f}oF@LWn_5zI*96 z%dcX>*V0SWsXbHt|F`g!q)o!9q8RX{x72?je69WXobY8>7X!X_M8MY=f~PeaWUbR+ z4q@!e!dE)M*ZMC9U#6Jw<@^%xb?4*ff-kfeV$D5thk#gi{MrfRZ$j1)tH655h%0-$ z^W<(a;-Hz#Fc3*$3+AX0mDFe4atVrOlnq-i)$vj9hM7-Q8sXhtwG?Xa-7rB%RtjeB zN9U?>NKk4V0l$aud{|9TsfGa{|23gXr~K*<#4L4G>6Mm0sCt9L$`?hHzc*a|?h)l9 z^f|QXADTXgL@QkthkR??w`QSNV0uU1^BuQrO5o@2#4qNo-?QCoH;wx91pbh zApF|||6ay+iM~ii?_PcyR~L1iq)gZ2#@5x2bt~DSP)HqioV3iD;eR0!jW|}{Ko@l| zhGiSZTC2taT;2cP=};u&PA0)sx+3J;Jvb>3Jvu>GvA7(?Kd`y0@Tzw~^f#BPwTG8a~sJK5k|~i5vb??unj*aWKxC-oGlL z{r_=yEznVvS-JzA(2~|P8X#)aMB5bwF&f56CXCa=MvP_Y#G->UVqivwh@!GbMI2FK^n`>*2(J(koi2w)bQmopFfa!-EMV{d-TPN{^@GRE z?%A_vJtxvtb^k}*`XBed_q+GP-R2#*+cbSscT`kjY!gg>Ew78!PB9m;*nK?wOoYDQ zyzD6rC)o>Z#G6T=U%0RseVtFVopr}!*E^=q-Coe;OOK^5=!&Or+Aahknn=nC-v}k_ z!LdziJ`;}WPoxx_4@q>FdNDqKoZ#{9p_7G{x0en8*I|=`m^9rADQUWm2a5-1gyE9J z{5YY%Mxy$A_FS<3m-bv_{egz81p2`*x^?}aXdgOM)q;aGhiUhSO1VE=HB``|dQ~Qn zp_3FvRlsqFf-IPQFpc2Ah?(WqrhH+O&ZGua4z%}X!eVD;iI)3?jS*j?T4+3nJ`-fR zYM=0DfZt;dZeUFiElD?#f`!7Fis`5tu}sBGRHqn0JvqQh}Rp|?E z@A_(F!{;Qd?&$iA>~Eh%>~D4!R*Plf3+akr6U@?^lG+TPGgDjjmXL6?yZ&rr%S{Wd zF!P2+9Qz~dG3hS?D`;IwQg&{meSp*y8w05(I$h=B(c_MZ!U{Pc_a_?c?NJr&!U!>vs}6!-C+{P81%Q0;4X5@D8^Ff*1ADX+9k z5B;|o*1G5%44WBXhF$ZynPKzpCx)3d{3L&%<|z3OH%R_NOH%%;WO?t^m!jmn_1@7@ z^2{Wq-+ySJq5pZ&JuC#;t3%F&*FeebSXLIHg;oIYW3O}q)^Hx z_S!)6d>j3mq2Smsd-TWR|FZ&1((MD`a7gSw8zEYVd(|P{hU-I&Ch&z=BKL-{u{JvA z!(=XY<0Dq+C&fqiciL6gN&hCh|1U|Z>x{ZZ92dF?;<1H*BMv7Klq$QJe);u$%0gec4_W6}?7H^TU41qHLD zBr+?=33DM~_@r-ko;LFdY@4-`zm@0YuV9i*@)vaxe?#2JMe{f%^vOB6+!frVZpJ&Q zUHipeKzyd3IqK&9jLRWT?}zPQ=kR=|B;>MF{GKJbjDW~K;&S+Lo0wlwN&7s?20DZ6@m-43 z0)<4}GoAFb9N#7O^pdvdN%0c$1T#lz?Wb?%8zE}AJfG_F4TU&PnGeJar8(2-8{7@C zuGV~!!55d-Smc?>Jx83Gc(S|4ixq`MJ9|yIyLqCGFhr8@;eQc?tx=(zlwMJ%c9|_A z_oOw!-+$Wl=pCBpzrWvGq14%$zVRrh(i&e%qFG<;W>oDS?b~O>Za>ixtaiEr6El@Q zF|P$%1I6ii>csTlW6X(1g~Z;88R93La`U9r>{bg&hQwFEy>c<4cB)&0doXx{th9kF zJtf4>DwQGDu*`rHNC?OaF?&D_Hw#H$NW-|C>a3Q-x;Dh)BSVaYUNIcEim{L-M+b(# zOKtb@>aS7Y9E$PHo~rj@;YP zu6S^=?iB$1#KZyv+U3l3tL=^at6Xhg$iFJp_C@?_irW6PoZ)Kw68=@KwlCvfUbTHW z|Eg8npSM|f>_#8qG1Oom({qsKcIG!=Q%HNZQ^sqzar{Dc0i7(Q_L{TIuvax}vIFnZ ze%nrQttQ$6?X0C!t8ApEyEgD>HB`^L)oIwK31*>Avw2VAOpWX*+Mom3(#M(dU{Lmz z-CBPr+X5XS%w)Dkq%gB8dwe6vrjzNcDASI73nTk|J4+RX(&)86T6u{)6mj?mh_TS+#X_&*2!ce(mUZeMOGVgg<_2KA;cdYz%#{&jFa%Wn(aJ>zwH9 zx>TNJ45|d>f74_xKc&H3eoaGcd0oC(rpp)ig-=K1i^q7Ssl-a-hI|oN%<{!lth=o_ za{qgf_rK~uvrEnx+W%&eeBl-QUtx@VF(t$ckWcP`wQ`Y|5#q&WHWrz`O}NO5n*2HE z6E18q!bP6k^v3a~H=j1W@fR;#xTA!NqKgnNs^zxlH3=8BhHzo4i)?#`pF0toZ{ouH z!Iy#m7yH3DV?W3<_Ji^NTl;~_*bfSg{h-L$4~mWbz-{aY<;H$cY3v76jQwCn;{AY( zmv&O8o-h=ulp^VeGDXt&{2jES)s5kWLaapB<5Z-ScnjRNx)J`3959E1QJ#?j#j#|P z%4PjP^m>!nzg_xwYaQR74oi%Q4qHn(BUWW?>z-7xEyxIoDjD$Mqa7a^@x1j%SFIhN zlWKPS?wXgt@muoegi5M&J^z`{?BSr+nHDuDqHG)?1O?pU>~?eg5UG!Z?f;0l!ny0M z)>x>QzMg75uVay8bdZsM+?+n#qz{U>uhD*owxjnuV%gum-<{Wg|sI(jO-ECu@q&w15l)v4nQHg zn{C(zTL>toO^rL zG(?JfR@*FkOmN^=fn`N}Skc@Sdg9XwcAjGuUx{Ae>(Cf@|-hVI;wRwIrbjnd3$M z?d{6Qny_eV6xy0!@4Uj|8}IKR9g*)AZ;-yewrWYj@U}Z%&}+E_7I7`~uV#f&nM4xp z(e9pOMi>Q}dm`{i@$~!4S(~)D<+MB(uE(a_LW@L8GmL`DaG6&22Qo)4=Z|qK3pizM z4eM+%P9dIZ<;&6ED&L(fcqY5fhW!JB>KuZuCzF&_dw(#*a%3cx3bXZ?LaCJ7v_H|Nx=-H=Ehv{n|K1bCv2|q`JUZXE@d=8nT zPxQNH;b~J&RF&*uvn$k2(a^jVqLNO#e;_q&0diV|olW&`rZ|*FCx#XL0H&!91VGc& zN}GSnY}h*Dp0WRe{#?jWLhRJRuw(17?ZMwOzzR%ziORTg+y3MXvF_oPw*Jy=%i#|o zgq5RksVscraicIxc6PT>_(r4f<0!lYg^N+RL>690g%NcP6_AA-1+`+3&t!qm)X}EL zO&xW`baee(_8HXDPO@%Rr&)s! z*u+V4S|WaZhaG*9tf@rSG+ET7Q%AMiC#ZF~)u#R2E;^@JVj1x&h{~Onr9^bUAW|*d zt7+0|X4jzuj$n~Zk@7ohhV3e&T`2}$y77@I^HMdq<<=QX{P~OMDLUiBxSQ~F{q#D$rjdGi}j7l5%Tm)#PZ2sf+;SAmaqMzEqEQ zoM4Ie;dkM1L-$%SSl`3i=)cS2%T_Lf{%jmmh$Ru1GI^coPpNpFZma)DQr+H&V7Icb zI!`P&&x+Qieud-5Ay;aL9uE8AT0qrV!0gpFd;nuQuKWXo(-vHo#0$$hv9J(wi;duI z0IEuG4MAN>2(YEy=S#KtI|#iq*D;;3J=j{v_?Fw5PmruPY zWz-YmbpU1!cS+44QT6d(h;9;W_S;Xc-s|5qczR1)qyl;%jryp-O#w(q!&B-8)<;__ z%F7-N)_bH}`u;ET9Ekqt%Ty|gs7$Hb>(WQ1c3%#Kn=s?LWdOSH&)!OL^oYMp1Z!OD zczh@1@0X9IN~{a`JGl@1U8VE)UFL7&JK*nXyaR2SZR-;A_w*9*_cepRt77@P*392k zI(z1KkVslL#@_(~OWh4h^J_+9t?S}BKH*5V()!wY`-6}2(3rZ zy~i5V{TlAE1*4oA_8CqMLBpwGKS7=G?rJlh>#hnj)#P<_GibW$R+HD$&HbjE1&C)q zI6DDke79CBaih!<$$oHVDtyIsync!1+jf4}hjKjcVTT7kxvRx& zhWV_yM~^six9j--E`L{&a#(3)4}f9p0kDZY08VzJFH~b6JOFkG!8gaEy8#S?`#U@p z;yeKC%rw>TUSJz~Q1<{x54P^-4|9P}P-mkAw4Fi%B$s;Plrb)I9NcDPpOs%hYA7{0L|} z?k#fT=Wrh7rQTX&m{*O)Fwf9y_#eTmMLLnCJXoYJvtn8Ht~T2J(=C)ZD;DzS@6RFp zN1=0!E@W+(nM$7a4!JD} zKfzz`pz-w%4bgo%BrJaRf;d=wj?S+rJbrz$KRjse<=!pETmF{b66Oe-{yIBmrE>L4 zc#0RkZ`0Emr>l*0Z~u++*ND-$oGz5RYLc|Co6!Z!peSTMx^RK^uZeH}C+BMa$8|>g z$+7L%ovVGuOAuTF#DGgEckyYxCW$~1IHzmh6XnHO4i)f18p&TZik3@S8$G_Ex;sjE zFC`0=eQtXcn+{fk!8G-GX8(HhCWKN$4$BrH(o!&A_C1Q7zwuaGM<}^75_(RyMwil!F1^|q*QJao{}N|~#{e{E0nmIUH=ZXNWT!I%yTqf1 z@<@YT?nIisJOk?fK?FVYpYS+4oW{;(LY4=7XncU+L-n%sh2TSZdYEKT2YrP9%TtqQ zAoRG7qS$Wqg>i&=G#v6w%Y+;bb{-D2NEp16pOv;fj0dpte zldOvXXHKys(&CdJ(t^5iutNtnuv2>DU6vHPSXLE!gKS&WbIC)wY1^h&u zK@R){!;hedzwxA}wuVv$kX zm~w`&-+$P8I!v+uZpx?Hd^Uh8v$uxB`BbmZHc4C$6W8LOPnMk~(<={Jjt}_wdE*1Z zWQ_QLea^V}$#>xV^XciZ;Ak%czP6>jf5Yt#gaceT?!3_gpG^FC{VSd4jut5TOY9pX zw^z!|IeE$uXppRndo+IyuIElH%l)I$1{=`e58>i{45VbLQ;IXl1$+_i5QS2B z)fiMaRNV1zP4aKfDWFQ~JF1jcm#qD0706-XL3N+vs<}uSQa`Ao*iFTf^m_C+dNDa$@vMWRR?e1DhH6)4Ks=Ay{-+g#}s5unJ?ouqpx_`B?f^M+>| z#mW>_@uSvctXG@~p~924XrqNJFbEqsNajd`)hKPz%Y=4;D{f_K?XO%7$}HcK_$ceYjAdJ`k(Y_a?i!@+xKEUON)Vw!f=%^*ou1o? zoi|{0bse-$AKV@9Z&7CM(Bir0=+Mi(=#btY!Xxw{Ee;LZA9`}bM~3M7n^qp1`Bd(a z1m!3nPfh`qtrwC(RTN@jlDR+OC(hBI5nS((?)gAJN9NCMqASro=-(YhiHWjJpC})R z5gdybhA?WhKkr=aw?);OlpD_Pp!Oe}Bn~Wg$qL46ac|0R9#hcfBF)y#b!m!=YH?F7 zb(j0MCHc4J6i^lQy;G-8+brvyYDQ*9zL$(op=%$uR}RsW0w5k*C1x!ws+hG$J(#s8 zCFL6rW91o;lq+%7c1BWeCQ2O9LHRbWT9N7pl&|2b4Qb6l`D>!IOG-stsgz|(8ZUoO zH_{_F{)6hukabm2U7513`>C!hS=Wz<(kUr#C(2Qh@&=;Jk(5myUb`geKNG22k`7dW zv|N&Yg{#&|Nx5HKVdUa^u2Xz2rL7t`m%>}DAimYZ38S`V6%a}N!KsXXL0LDkHbX9m zzoJ&;Zhj@3FJM%9atWP~QQM;x)XPk{!giuWTb3+eDtm(KxlSV=8toN|eHjYeRzbZf zAe#C^3Cy{>SiLW#N)AOFi3XEFm~NQ+kC|-*8JpGQ`hUxDgp7a&M5Yrl2}LKhtH<0@ z;cg}QQ%F+U>t93KKrj~d%4mggh8}O@O>$`X#`h0A{^QX_r%Ftqp-*E*c%=O8T)tPk zcP5#uN1CeAMW;39s`jBOu_SwxQ@MNdmx5$_`P+Tl=Sdi`{<06@UCP`I8vBl$$Mfu$#f}; zb#Oui{~$#c!atNzkjX+RY%HY9*1P32`ggi)n<(c|O&R84v81iVRqHHCdaM)yb9|EW zdGU-P%%Hr!9F(giIm!eBXRZA;Dv?k9l)Qd zRB?}w0QL-|-j*Mp4F?R-ui{ZOp>t{VWHXnBK4j+76za?>NqH^G8C;r$a?L%GauC(2 zbE%x_)VcJ&$98TK5yLMV4CQOKA1q z_oOc1UKba?#1!#-j~ZIisqAI@=TDc@xQYw9*Ce)~mH@zJzrJK;^T5Jzb3j?kGf zok9VwbqMxY*pC+;mqL0B=f6iHlSfEQL;s7p?~iY?y7y1o5Q0P#B|@hag9NRk+Y0U7 zw>3ozG^HD@roU(%OV(#bjIp@ibCRA|uk1xJ_n&&*{InVj=Iq!4M=cfQ;#Cr+%OWy38 zai`=;fwqak@!oSDw_KAYvPs8+a7&4W){O_;?W8rEiuAn6+wC$*-ArFii$|uSWcIxW zb1f#UASz@O^NF&av|3U0mK`-`Hr;YQLVNuO0HW0nug8+4f(>mVSIb2&Yuutp5G?hG zx1*NLzmK}w$Op*I8rD!B?^OI>i(A<5o{(N6eGn1CyPO9RlP25!O?-C=ron|Hr8vkpVSnZf`-Q3qW?xyu34JXa^@=^_$QN-odU%XxZ*pQ**$D!n|{DCZd?5{+_|QJ!m(^Gp$m zCb`NaT#HNZP$vfsO>;0LV4g9(EAV5ef6; zK0m^btZAuJ1wNFMTLO?#u58gu{zyHgV6UtVnCnzr;$K5;%hLsFustz3p$_J1Y*U_* zvKp}pnAHTQUzp?}{js(P{joKJDd^N6`Fx04Fp0k;-8M2W2Yj)|nwO^we3|K7_AJGr zW(hxwdl*ewrD*8fsC1kA)e>})h|qv|+CcBs;(HaEfxD#F73guPRk8LXo@>OHui|?0 z^m&-kA3qI5rfjXcs;i_87Hm|=(=^5=*!dUC;TZ0cK`8_V36x@HF?51T>7S9XCsA8Ad%q=ieT_SmQeFYuN3UM>S?G=k%Eg( z(OT|{oogV~1%0%HD4ude96~5CUpUo9^(`mqxgqzG>q4{2e4Q{f>zwOS8KFj>%!L#x z3g==roZ*ChE#Da-C+nt;Nb}&u)zEjT@R6$M&Lh0|I{K;S^m70yoVux;i?GO2wn%U@ zjimoStn^vY1K~c?QJ-Z-_L+q($fQ2g7f}zMa8RFFu+OerGrG^}6QguS=kxgDxlvENy)LJx(2%=W7 z(R1|hlXKFs5DoF%x#KsD5Vq)q39z0-EvKk3{0vrr$(4_2NN`ucEtcW0hMaPvSTBid z*azT$S~~o>`eax>a`h<#@RA-~Ngp`U6;R1ZeloaT`~n=AI5V&XJGRQZD?2(KO*w+4 zz^q^YG%jZCT&`eNp@48VV%8RpSkBe-|6i zQTxs&y8ZpA`Cg_eA%Xpw%RjJs;%TNLcf(h+n6S zWg*?x%yl^P-J03$0|~FHVI!wQvG><(kJu7E*3>7k-RPM{rVQ^o^@1OH*R+;&a+pDQ+XxGIP%%i<5m6Uy4k~ z1Kc@PH_bt`5aCXr+}EJxERS%i2pR{j=S$ zrX!)?vZT@9!`^#tnu;r#_pqiPkAgL>)%=o6nTSS3i=L~RBk(BYUpU?u%KqN`L~w@C z@y=JC(N3U*CC2E^GjXHQ^uvG>WF?S(a($&`3hQ4ojd)9=03$I}&mVB=z4hlagecHu zFaj2e6KNMAq{NL1cv_GmL!`vH>WI zj=mESn+K!sxCpF6k$cvMpY}>+%R*sw(&~c9ML5Vc_vd)u;hOt1Npl}T@z=#KrqABM84BQ*O1@z zXX_^t>*hCi4hNm<^}Ld~wQ<+Qx}PfxD|b!qtF~lUp4`0;$+@y_>AAcL#`#AA1JGEzS z;&S7kOdfNV{xJUIZGS&9mfSdUKBz~pKi^ zROB350H?D|+^BT3&z_kkJl*ra(LA;P83N&}gl$g>PraL$Fi8ZNHuKaXNacQ$fi$n; zF%WrRR$S$HSXQo0U~*~k>A;nohVce>YZ&NbvtmpWnnb;PhCX!<%ut4%U2id`w_6#y znsQB6C|EJp+0UMEVW4;nuB8~>V00~*v87@y_!nYhDN&nn(o@yQTBe)*;;y*V{M5b5 zlq2T$tGAM-m4$zUI|8KX*4IUb4qe5hdqIA&LQ&sQyCqja3qTZdlY?#n?QC`Z3@k52 zcm`w*^@{e%?mRx_zPo4`O$xIkVnBI!W$uij_u-48_h%C0())|MIlT|w%;|mkq%rCJ zNAVwT>-q5F=$+Tod2KM#9xo-5-s^F|Yf1il|c-zMRy2$nF&F_p}uZl`= z^A>Ld#YyZLXPCtFO^4C^k`5N%irArXYUpN9-XkI`#-p^9|9wisg_SnN-K=48+)Z)o zPr{a$lZEAF#M*Jo=(mJ<@K2&h)RlR$B)T_;6VGfr5s$GtD%d$}mGMY>bsmX5Zgskz z;OyM3KZ?6){J^Q~$0i{gAz6uRfI)mUB=W3yr`t2q5t=FRdYGgOMbMqL8*z@FQR6KhZRFS>E5vRGYO^@RAwiLRODHmy4bT(rED74{EZ&z$qX^xQ% zlaDN%d}M>y1UYD zxs|keTS@W1m5dBp$wr`+(UsKITS!H}QYe8|3@@;{1sxcymRpCYa56q4&X5Ve*;ioHD`Aw*3bYmFV@D z?uCP!i5arLCKK#GO;!rVkPX_E$)^Ya-2iRrlaJO-T zFuod@4R@P2kuB2!b_(y-JtA@*g**So$3)JD@x9H@iJZGA=N6Ikb-dg7vdFoG%6wJi zd;;#|b==sAgcV(suq~XhjS}7pC%iy~w~NB>q{4TJ!WUwtHt!LI{~9XgUw264+=zD@ zkBXchQqDn<^Dp#~q3}oM(MLwat7m56M^rlg>T$f=tkLmT1G7+pVx3Mfyl$S2w;Lyj zyw^}`jUw;Mv+y$}UFb81v8Z)P;$45}uBm-xJSC_Dtd44uEK0AOMOsf!eC!loQxU%?xVjbC@#UC@qBD)B2HsnSCrCNcdns z+XZ`ny!G(gcg5{ee*6CHiO|Kp*rAKlNOKl79O#(-QurR}-8*Qcr80hNd>)I@JyEo% zsmq=G8#{KmPAaN(E}I3Lj^IJ>E?qTDo*gFDBG&UH%&G>B_mn!l%fE<%n8I8_+X=%4 zFei#1xB;cqHPR`iJ~= zYIu=HKXpyfWg{B4ppzPh^#BDjH6P~TTXd2gDx|g^2%uwN9lgr^k5)b? z0+~wr#@#t7Y7AeB53}+$D{nfCHXXHL|DzG%OQAF#zjUBK;PqHiRZ^)X#qekc-?De%G z>99kO!_e=+Jclq(ge^@;xkSc_)=n}!=IcN>P8IsfSUE-NBx4)tZ!>$jH6M>m#LL0x zvOElrSQp70SfK?T_=X3;k%~0lPcVY+{c3MegJX}*ETz<*)+MntSd4BUNyJ$W7lrJ-`m{PmXg+&wFvHwumH{>>xgN#Eyo85-AL z28~!`l=1);*N#ZHTbjCLM}d4wc4Wx50>g%f5NNA3)^5YbX7~w^HAzkg2-(9geT5Y# zP>N*jc3&;$J}3gpj=R4KA3ROu5t%#*Ca?1D`2GN78dwoZ8h8JV{^e@xunSAlDB=y1 zZ>7bzqGF&7vh!aH0V)=0(hs8s{mSo4c}8jPd*}Sw6J^adxxN1r(uy@m)1c1+^33;m z$rGf%lTjz0JDR#y?M)2$sw_T7g?B}Tsz1_8WV z+fcPhb~LcgTG+>qcsmW0;{ET_>@`S{ZMBkCZkOz){xVfyX@k^PQiM;Zy29Oh7L5(mkb94WHREZZ!yErqPJ_V$gd!1KSuDYqZ#3fP>{zu;Qtlx)ua zGW#XOAoi8fn-}CSNZKPGLH&!4bomR9^*btMTLZ5)ud^v!=On6gvTQTSwj|k>99`!I zUeku?UvMpJkl@-bSR;G-x-P2H^eR@mJ5Htd9r>8d!{`IyU;Va9{-dwZ4nMF8wpz&E z#dVCXCo=FAmilZB16TA166F)0!+fFCEXWIV@ATQ~4I93|+_u_&hwg)=^|%_CU{nKr zg}Oc)^hk|ttBLHPoNy22QV-?Jwj9}(E8Fs;d#Hx@OHK4IxR%vOaJ@heF#~i`Y`_`G zR_`mrw-M%eS3%v!?k&t7{>7=v(2|6rlEPBz257aUcS5zhw0&tM-E$R7;cHN+pw6{q>L zK>q2#T_cbvSDbNsCAUj*q(O5eTdlQY#R92&W{*D?aKF$wpoLzR3ZZg3{|~8hg>YL7 zC3mRd(yp-jw-2!8lnHdiV^px*q*Y6Ko@+>dv2Uxa}^*Ar(;9Q9E@gQ%kB@z z3uwu+RRnAqK3g_`xM9Q0VR(q3Rso_ww1D8u z6a#13mq_?7PVTPgUvMpRNj4WC=Ow`zy*VbkRMPf4Qeg$-6W1JfvUb`^2^9XNs$Vjy zh3Y=$JjGn$U;VaJ{-duDM-QvOQBA(S)7C(<_EZSN90fiAUqGP0o71r2l!9QXA1rk$ z2sYAJnA&I4i3WTV1p8*aaX|!g@(zhaFz2Q8Ou$wE4dw`jD{)~d47xxrZ%5F&m%1e| z9#Mke>jlCnX+^2GlK{+-+3{(Dw-XSo!k_#$tqzVF$x#u3U#<(`mkXX<2){zlBJu0f z5Psp~82oZ^{92JIIkEw$thUk>bNvN~TrR{MX@v{7Oo>t<)Xty)7}X9f24Q<_E5*|8 zrlMeD?D;E{1-P}M6oAX=xt#-7MKo{~L;{zSfU80QE*%H1f_T8?#Ohrum6c+tGs~O+ zS^%qI#QLXXD-Co3W?|ii#sPx(^;h|zhJ}Dwg@{;XMnWvWD)|YrUth8hqdUC2ERrjY z{t6b^rGK!UyGe!bT48&4=+Q{aM6$5#tqR$rprhc2=>Mw;X0+-q@U=i5J%d@WD zM!ufh6TI6NqYus-FXvYN2{Nq+=Ek(BMX1mt~gPdhT`X*#K!#>;^bO0X7XPgwcHM#6iU&QSsx#Y^epUrmZd9R+uX~8J_RUrE{q5%o>`Y#XamN@6!1Gl8-OEkiQTs59d&wq48rpn3bU*k~c++ zbJ-kaoblxLHjcBfFN_}Td!t7?Mdfltjb(wwBt3-9kdjj*YcW}Wcsn!LxP9CJL>f;N zAa#$Xfs|MpAS1SSHEYfz9T+zJJV>-YTrrHpYP3f*@a`Xq1{w`b(}FnE&UXIfgEMCZj0F4egqFk#x}8!F{XOyY z`6%5GF$&A7x-#S!K8@L(~NYB|mA6`Cjk8dOCt*t}Qfoy5q^$)-=_u}<$ z08e5j^{`CnQm1Zu6N+j!3__!!GTtGzp=mRcCmECmo<@saaTum8`{g(tOBNw>V(2Sq z%UOSQt^Wrqr#Xd)_|V5%71<8J?99)J+wJ$yv`_sr=crH6LFWRG*-d1 zVbg=Qs>Qs1iEbnMBO;a;`VgkX1ULGYricSdV)S?waR(M{WCL-y9QmJy#y{h9;3h{8 z=30^(+t+&$wGEBGA_0{Dr?t-BSzNxn0I&&Fb3KZ^=z<6|{WObgk_x3(EPl-n8iK2& zf37)BY}+p{&oaFxn9v!o2`2Ox2nUDiR9x$6PiF#YJ^ft6W*>)Eqo_mq_FdxJZb^EQ zI^(dRsgMv7y$pu&%`_MSSH!bqDtiB*KPKYReoBa@8PGm3Va} zDNqC;7pZWTTGEOs)Z*`U28A1;RC_q20{Tydto|0V_I4UqK@sZ*Dg9B8upw}k+EEV3 zKfel7$rL==ckta*3K@JlGa#!o|9wio1Jk*;3Z6fem-9CdX(>$-rI>kY2d0uCKsis( zE}~Ligmh==v-JDzIdR3FA1_o$F>&{JwbMie`bjyPJ;-0N;wwr1g=*^^I%4@2;lZd{{eAf@?`TE$yHvYw|a_U9lXnV=NGj`!CV+wz( z;6jDZhQcwZAvqGEuavRdk55L)nNuaciN=rYPuXLB^D^a|Fk2UBN{a0)AHR7Ad~@RH zZ*GUY!v2$Y`58{C$M091L30UYLS#H@^zO`K36j`1M^hIJ#cY$>NYesc^NWya)n94XOPV9E8BTJj}(HrnEm)8#~RDU7#rjvn182c{7N zH^&&biyI{5m;!exCJEW(0V__i@=zyO)pGuXlC6(#L8qxz_tYdSRi%=zv?PM!~p-K+8 zjMQ1lmXdOo1ACQ$E2zWhKJB5PFnvfrm|1F>RAhK`hjgHrJ+>hj%zB+$lOptax06Tx zke6e{nx=6h0X?@+;d>UoWszDa^d_d&sUX!^lM>#;vd2Hv3e%VUtnilbd|L+d?VYfJ z*nXCFOI%HeMfu8PGy~8kuf9#?nj#Fu2F8c2F8+B`t2iA08sE{Rb;$ijyYGb-u z|D{za6?q{mZbkMl5Gw9i4bfWXmTyOI*CnjR)TSWxElPii*$;m^5|q5r)~!y)=f$n@ zKUBLwX>mS%#(9*o3k}!?4bx`DT|qBjKpPiSu}tObB`zYY0tUd_k_=o9EqIwUoCaRa}CCsIvdA^W3MV~Lb{y@A{1t- z+jUjec}ut>W~GCzjZ_c~0DTp4xLH{+TAa8v& z5-(JhnmzmL;RqWM+~QPKFTQiStoNz7-tT)I*88fE>hBe9 zTv*BXO2mTO&AxEq!ivjwF-IX)D6eA71B7!!eO(T4!2v!$rt2~}#u0tht102f^4FIp~(8WI%&4vH;fJ#NNn=@BeC^m`l4Ofe}a+tJ-mEB`jMHn!k8Nv=L*9tR7odLN1 zyiCe7%BmBOOoUIbH2F1e8*oX^gC$kk<+mHWADvF`@^%KBf+Up4H?(|b91==@S(0q? z^gE0|Gt1T19@&xOE6nw+C@}bsXfS*LvHWutIc7sxj#RMcg?Rs(Fw0m+EMvWF)5tcRY|~$gSnyIsnIWlQ9NxdAJNP^iH!T;aZSJ>BQJ0{u^(J`sv zj!C1A$&hWSvMo)vW&Ce+%vk(?e4F_HH5bMIkM5)Z%6lv0OE4>=C74b{f_Y&PKxIe- zP%Akq6%l~b=V;&}0B4K{paGsKA^>QMGg1T)dNvq-79j$_$0P#4XNm}*AyNboM}%29 zpp^ z0HU(PBPR?PFd6OwAHsbg^+AMXM*$@GHS7D!be*;agnd-_G6U7S3uK3LK#Trb3iGi{ z&4Yf;gR-MuM%|30fJ%U^hdn$m4%n`khhSSD0^7d8QV)UcHo)h&V0+Q?v!2h&`siP9 zh0e3p1Fl~Z*wUME!B)}&+#=L=`wL%2i7T!}(3#%^Y(2cDp6Fk2E%Qily;QKJSHBi) zF<1Ckzs<;h^c5aM0)P^1HG#cRP za|~WV3wH!~tkaKWRGf&M{{gqBZ)p;0LT&gCEE%W|X@l0T4ZzYindg*>jYWRjF0xe; zt?i|T9zqx0wqDqw1=ut5t{}UKC6ACC+r@)vqKaZ66t{lh_xncq$Nl?_7m!AO@e8zF z=e|VIFAgeMHk~Xf=R#$&)3|k!w4dGnKrl$WUH^(&XYUL$-ua|#nu^;@?&1k@D4~xh z?16-k_utg8$6W(``~F{$1Xb(Xf((o(Z@=#=uBZ>NM~>1d`SLBnpm%$2NW5CTI6}bM z-U=t}DireE*>uu1IlVorNraMy!SYd41}+OX$-A9?xl_6OYl-|v`dYZp!vj8&60^5{ zN3|oHEAzcjhsn}f5d)JXn2aQtRgU0Metsm&H55$>SJL$};+>Gg{P*A}f0)*<+*b-$-ONc(cj(Y{*w12J#8e6IDvLsN(P_DWbo+>l zTAwOyCI;Xpyt=2>so_NZmt!;_mCvIulpJB+vHghna-@A<(vM^9(d~=SeOI~kkIq~R z@gFhjDLMbnwdi-lSstBK*JB{EBB=+SAvaaUe*Xa0)6kfP15j$wJ1vi=8H<_j|B`SS zpG@YGPrWlg15GMdhMqK?RuNAC%VT$oa{3@`^>&iP8=78J@j{E)=d%P~*GhtX_Y}0Z zdoHq_@Am_xJrL@@b<-~}uiEaT{!3SsJH8#E+~KRWxZ)~!{Ha^38VI>NaK${oxkpPa zfo`cGW2mHUX4R`|slZYZVkCHXRbqHK(caFr)1u!v{)QQ?$||a7D+~tlhVb7??E9ev z-fL1bI!az-XR7QgU>-w%`r0ss^HaDth$Sv$YT-o0$vXbSU@)@{*~u5>gc6_w$}n`1$D1tP3UJynU`&k^V8`w9i?Zp_r^s z-8F)8+X$cPar6tgxV$>}5oy@i+06hiisFFF0lnp_@D8aLxl5(9QcxMHGdm+>@}Gp9 z6gtdtVb1&A5SN}8(8H$h)2gbfcOse(Nt?-`@LGgz{~P#QaiSW)s73H|W?*?j`eC%i z<+nTSxj}`Xr^lh)W~Iqnod>d zAN3{1xxy+EKY{s+6;E`Zi>!FbB3|{cuQ?XD^**KMRf|1;z?%R22-ZAedL}BOh4h2g z9j?iOo;T5Wucq80^#*%${ZRwUM)zyl7rG1u5hl z*HgV?GGu$=Ki>8u%LN&-I39U=@`Rf|Qtn?i;_U6v0#T&d+1}I{s&aNXg-E-)Gmpo( z(9$(eL!i4ezWuv*Fqza)aBa`#f{HS!hX*sZs>hR2YvQEgpc2!&hh42g7s=eHwTF$z zyH;-0+QZHTVZCU6MC2U6I}uT_hZR!J=S0pP%DF}4?4X=4i=6+7cdf6AoX=Cvts>{& zDCahjvx{=RC33z=Ion0frId4*$XQJ}_lTUcurAGqM9%5dqN5@weXn&;((#<4cWACt`Ak62J zzE{YM`()NDSQzHSUB${O>ro*lYC2ZXc>PPG;fX-4&Qu;~gu~xDXzJ_iNSXIOlp*`tB9CkfG58j!@t4# z?SSfsJGIXN9+Q8A=1d)TUEpiWCTdEfYX)5@v;>6{vhKu9xjGKY9lHrp?(^v!<>XV` zS;6SCWYT)|wdK&24q9z^SXtjzK));KidwMJ?kJmnXwSB>{0ZQP(4PP{CQj}0sCMO| zc5teTsaeRSz&!No7~@g^zJDst;*_uW;ji`6c@nANp)bSK(DWr?1mFTz%+T^aER$tG z!suM0)RF^<8Svq3I`NcRGNE&G6v~5Vwd^%sRtA^mcED#_(|a*}%jOJNq}@ zbpS^eqJbK<>l*4_=c{lj_d7LpX=T1l_TtSm(A@DPKjmXamG2d*5w(8>eBo^b9BDfy z%C-@|Z6t9>zTRK0M|MrM28DUmI?1N<=c}y?QRB?WHO{)(V=s}$nNcd#OC=h6Q89ae z24P{cv{17fz}=UKw$f5r3hMsv()-F&F+L7?frN5Qo(BOXD1?thZMS}EX!;}cO;#Ir zp8K-!B#>J0SK>LT^YOFiW_}6%daFYJuIqrfqG;V_ffbJGR_v@tjyo>!0jM(ea zN?7A`?Bz$V=ZH@)pq_<4JE!BnHHo~YR7F&YtJaXx9aOU3lHs?ht@Cv&FRN3b2|&q< zbmYy1>XkwD9)V(|`B1eKm2W1b*ZA&8^-}3rJ)C->!}6cD2!%(F#fiLZXa~1SPlIlB z_$f>i&drCuz#W7Vgv5(?#7x3D?8dv+0+I6#%4rulAE%t9BIiQ5TeBPS5S{kS`GVlS zG}38L=_vn~|3@B;PiqD@sYw2x9^{HYK99wW?v4b9#R8O(szN;=u8i+{%YuA!AL*R> zi0Q%p^C~!$c1E1fx$`1fkUO;#&O@V(9J0M|5bFIt_kT3H2;txzis0)3a=>=+yt(o7 z*747kJGW6bUo&?bmIYi>bRR{sXeYU~#e^ca6t#a{ZI z8FoIfjoQEZW127?< z?-jzNNfDSdUj5CMgd46?&?wpZuIQJ)1`qgv-q{BSin#h|Vu(BQ(hPBGV zpdipUSRa->KYD$5{rXE=A4abZTh%J!SpQ4w!``WM{=4QItPjbTHX@s@ja(miFKb-M z@sNGEt29$zh2d zzZy+1V6NKH9ZCo*R{hpYQA(xP5=?Wu`K-<{fH zh)iwB!|^ZB`>yX?pd&C_fR6Q&)+co~*CZ;Z1B`m-qZoI-wn}_~)Mf$mk`mY(^UzsH zO6Vk!8akIk;Wz-NAYB< z^M%7nqT>FH^Hi%xom4{oe=vUk<9UVmxZv6yww}hr2byfT%>W?(L)_cIH(8y3<4K#+ z04WJjplXpIaaO5XZJn*DTClvRMbh^oP}r;*BTn2BhgzUDO=-BB$|j;tt+27pP3G8! zih|fuY(WGnx~&m#>Tuq*h^P#zp!r|l>zq3;ZBT!|=XpMV{m|U^xv%qn&ULPHy-p3( z*krgxe-pDJ1sAWw^Kst~==*v6y@}KI(#g?IUyk*elr@}V75)kdo?tfoc&zlQ=1hz? zC%Dfgs7sFwqfD6FK{vdt|E3WhvpUp-v$4ZQi{cgpS~3&ktq1nf*7CG9+DlvL*s1tl z+G5u0(02ykuX0MuEBKVntfY}!WV6A;wmfJvsG(V4F_Z~kHH2Sq6sz^0fX2ciDxod; zGrTdU6ubMsM?hm87trd8L~ycQYhb7a(83+ZwDgK>itTjaY4fT*r_m z3HdNXS|sFa44En+Uu4KM33&%Yrc21V3~7~+vl!AQA+Ke~3<-G|LuN|I4n6o|MH2E> zhIB~CWQHu0kYDyORPCZTyi_aA@I;4=wAN53K{K+M$)kmzpBnEV1aE?^Ps=6U&Md zYxX(Xr3R&9-4^LYld#UPT$bIOeIOhUa~p@}oSb+H=zWGn@B7rUr)Xu~2rDUI_ci0B zl~e|wLGsMG)Z1aTH*!xv6>P#1%l<^f0#zE(H~UVr?*ZH#pTppQGSCpD(t@WzzEL9$ zz6NQ)8|$?&X%OC0e;$i1)iz3uz~J#6r;m*9aJ@FZir93t(f(&6eb49tBb^X`q(40C zTSl5atS*~JH)+`Tic!uSK5&>nd4g%zRHbmVL78nRCz|@Av3$_mXy`i)7wO`Sbz1Eo zC3BCDcQlQUcWbHY8-tNrGg2JyG$D!@3PK6(A0;JnJ7SyQdV;ZfhMekW$3#zc235?p z>P0)UKcRa8i1T!F^XEkE1h#REO4<)^^$vj_Q}u~cUr{R1K2rar(*C*BeJ)OE?5C$- zdRj(QOmc}d7`gG#=}qkaH zcBJ`2nEyn{{w;SIXua-d_HRhgEeK+4HOghO?8h4YzJ{5GiwHRzUeWl!8UJhkZM`TY7eg#)~86g8wTbv~C9O`}#jabSR{jNJ#L1agG%6 z+4S$nc$pN&_);zyWDiW|)08;pQvp*{0hYr$vJZqqW3ZjfK0z`_gDOEY2;xP3Y6=W) z$|43ghjUp)=I@kVDoj83sgDhZJebYd8_ZxF@{XEnz>TCv?3i@3?;BkhNi+Lz;DOwG zC<;lx`M*TcKQxC?2lH5~;$seMGQs_;64;^&V=NKdJi4iseHVV-^k_7GJ~1+S)27G6 z85Y9Ntx=e{M_^_MCH~KF^XQSlIJT5C;pQ<>xETkf{gem^&E5+CM}r$oiNebmG*+~V z8DP3dTXB)TAyr(YC29_jaBB>9u43$LIU{zSy@s)~B?>z?TfP@|rt#+Jp##S1BQwUT zf4JC=8fH#c=IbKJ`MT&9?0JM*P_7vcn=NcO3UFuktI+&%dr}TPxo0U$OumqN zI@8n9krj(B^jT`t?MV*$U7o6}K=klZhnm|cwTRqflo=?4j!MutFM?~M$*E*}@;x|}tl zv`9wM(rtpYbql$4-VQf?813MGX~j;EQCax=5>$}LYGLD|+?UfkF16Bq`Cq=V#&ci( zPgBHw`8esmd|$+U`2z!U2}a(!a4sXqi$5@IfRSE&_vQQ4;fKSJv2kXyV&lyGEZ3@w zvcUPG>ynv8dZyP5(s!ThLXBU^?~0fVAI3wN4~ zcLc+l0z<;WsALN>{-z)%!_25;_=KGq35z-h?x_m7`30i;2CpEGlVrLifjep1GcDxE3EItSzS>n@;06IHLi(8(M;~5$bEX%pz~Ka>0sv^ z>hMpYr_wd#G##Ww=A=hSG+E8m&khg#E#RjKrwVk-f0?h6W`Cs-_kCmFIXFHDOf&gs z>HOtJ|1_kv!n;G@I+K66ztrfzj*^0E6NlLjvKX_81B^Gdu-!0}Gtv!e%POlbcr<&L z-yF+PF#0(psGoyeiU^OvHMRVA3H{x?7O^#25m7tP(nNn>29oD^_Q8UFZ<{GNr{CLd zf~s}FvEZCz!Ghz#ImbQ!R`#grRAY7sO<$_(nkLvc(+r)|q}~2)aJ-;?^$-&}seU^; zbDg2^5DR6|jP^#o+%#ZKSJ>K|h{3J2%3dcOmK43qEqX<-`8+6REL&Gg%Q`!Y{K=|N1v)do3EVV;fs6Cnl&9rx&MmT$1ZsGTh*}&;(y-Ud;PcD$L{=Blzr^vCt~emx&3|L+Wsur{e_HXWsYX6kr@B{2)=EI{cL6SvfH$8J z(50(%)-Y7Qnll(mys-xjowJJI236sZRh(RUrYh*Gge@dkb?+GgB~*?q@weXvm>~gc zzY8#30+xOkphW^+^j&~P38?=rz|&I&U{CFL03MNmfBr7OgA%auy8yc-;Qa3b+#vyT z1_MF@OII3m!{^w~Th@=$;fnkrmbro#<-+Ji{#uTD>;bFxsaz)u@$zd%^x`GR&hLa= zB_W^uPRLDD1>{fTK#IO*gctg2`Ra`iggY7rdxCQ%Mpim}M|r9L25*<%yU3QRH~S_5 zPldNcj^Oren2KE?{CbEG$?(@vT27bdRM-s}3iOM*EP=u+oBEzo4M_(DpsbDB<9d$av&fG;nhs$Y>+PmA&L!&P4{tG*~;SgsUxq$*_{ zmVlu{X+$Fs4Z~XEaBiT{DK1y9xDQw8w88Zge)L|w_`#o965T^CZ^Njt3wVhw^wFsUvCTmR}3>p9+zBl)hCUow=mS z-{d&oIe46_$ZtLU!pYjj{{e$*5*kQzBoAqaf>dE zQdi>d@?nY!_AaCdV2pAL&CMQ|*g^nbMxPK+gI2LsFBpv%hBQ|+sU2^4UxL7EsJWpI zt)|6GYgQ?2Pp6yUJun4X}>x%kMS8 z$B`r*GhQC7Pm7Z&Z9t}>!N=1?rb9ije^Wl=ef`qf2YzNzZ)(8xAlJaENi+m4nb?19 zu0u<-58}Mhdy#-TDr@ZR5`#%=;F-yud6X%J}D_x!hM({9sC97PCnYC z2UhkMtmOFQ^w|dQ(2;WbM3mjy3-r3^0LDQTjyh$CmSC&sZDnzgCD?%$$OZk47;^Qe zUaRa%{dVh3SYCb>H;P5JY<8(K&t~ygr4r3!?VrxeKa!qYmib${Hb>>=>6t@`Mu}Xo{r7sC}dpiyF-=yD>YpMPBx?K9nK0#~iqA>qBWif}0 z5}Y1~GIb`r$3CA=fZk5Mug&vrb)82<%gX=**!fWPDUZ#w-|Kk2v82Ft<*+hMYI__y_Q&Rm8lFt8dq zKi3RlbpuYZ@!t&oo5_E%^W3T0T+%F-g}UjEj?LP5Fo`u727N_(cALM|iIN)^@!!S# z*Ufb;83G1RtwB)P!@zv0vFQSLA6NFMCx}|@nsJ(kYk9wazOHG4x64L2&Z74m3T!(K zX6-(;o1j<^<`o9K8_c1=HdZ3p*OaFIMf13%HuzkERqgbcFtcw3;Hm8~yEWKY#vuiy~GdEk|x;e1zFnxRpx1)gDmiEPO>DE@i1B2FqvID6F$~2w5 z@E?}tlls;uWz@i_2BOTULH4pvi}~rchBnX8-urN#5xA&d*=}!NmdgAzWnKfFeGTbw z(bB&BGk{NgEAM0QPXGUfY_3XP?I&!k-oVEh@4Kz8x zVx|J6Xm_gj%O_vmPs4J zsQwk1aYr&j%zcSVkEIN)<)Us=X{GbALgruRdtb{{{|Rq(ue@0q)%VxXgxH>g z0~U46hjAuExx_&Qttu4vn_tE$MG6lfALUD^q8U(=fskk$>>FxDcnnJ7oez;~L+8^K_>$4A>eCM3~-t7s#{VQ8$A%Y(tfidk;o!*_P1Q-{#`F1TE z8+Zh}LJ8Tgau0)0bi2XcUi-Pd-R!%S;QMU81D+ANPk%HHNVOCxqr9i|o|L**KN8p8 zYf&+gbanTQ(RrpQDI9lkq%yVE=_up~2s_pK7bof*fo4V*>T0?m3C|ir=Unw7p^#vE z@6~ZpSv>dZIsc9`^WkDL!nv#~ar>e!dM=dh^H}R%JvI>!L+f7s1S#%h zXD(INXL4rZC&_80pQvVYi}$@SQ73l7hfB2PVrl<#35ys2IoojOuFU#&ObH_o!Yf(o z?H`;OeH?H`(1&Lf;cP1rqOV}D!)T$;LMiD;*&tHVl?L9~xbE6#Vs{fR%&*jTceiq` zOJ7;eyUE*;s|N10GJ`@cy9OR5cSv(5@bd!2=)hmcgyb=4E(-O ze0R$4$pgPJ3r1-Vm1+Ne`V1 zW{u#S<*KcBSS3G-3qN-N2X47mc%oP;1Gr=hJnpe&vvD!9LrsgVp-6?e8vM{c3>HCJe z^x6wk54lcVq1-TF6VTGqyRV05gFg7lfq*j+mV|mp% zl&mz{wlaTzenc}KE{wJtb>1Tu5fa+UEvQs_Bb+#8FIY37{n;TPM{~m>Xm-BBCsD1< zW^S1WcN0(XIWU|iOkMa;UtEql*n;h3^ zIXtGri7F5jfki3oJ_Aw06@b;LZn_N>%f-|b-3@Cz@MBebh*ch5ci?~(Z4ujn%PkEcU`HzSRGE3*})t?NW zv&-9_;M)Z@njlQKB!umOvUfqYEfn|CCY2&+Cn)}LnB<8GYD<)8rfGP2l+Z)BZ3@gm#3c~2TTVBsN2M@!$3{! zI;06ycB?<%5Lt(J*2Jzu%oa$6D%ma1?7A%)sjwx6R7he{VIYn0>~j^MVC?)dM|kdwQ;a$rtPx}h%qam--KX181)=J#5nJ$Z%i z$4zmBw+lFkgD9>=}44@lqftk!toepQm-z)laKTan&!poE#hS+;q}FQkeA%amk4K+HqN5K>ksj? zu1WBW=uDb-vCpF0jK9?#Q3H1B?*k5IF<=u9%h7d@y`yd6#|Ul?cIRY{!>#P>Gl^zT zZb7PV*V2USU7O#U^<&G)-uADCD6hBoPDp%xv%jgS>11!>o9$mEDWA6=osjss@{jfp z43qzH@|}R`7n^Ut?Y7$hpP+1SS0^NH52V&5?w|a6v7^FSNR#12Th-(~G$d6v9Q}z} zb=Smie9gbL#!sgaGWvG8&(pTa^#;$-fa&_lg?m%`k^`yq@DB?e{R!D^f(PMTgi3$; zF*6r8;{|MaL5T6EpYtbU_|t&#E-o#Ha4;?Vlk7J8*GdrE)ScwN+u*-6?_!o6wSa+D z5$4b5{7H%mY6eTPilwOF`Z_96=NYTXtwt%+1>?J8qRyR!>yF+Ra!=AepN-GpSnj6J zDasnKBE5!3^#Q}9@a@9QsBHx80{P;cm_-Bq3qmml2OT0o^%7cWbY?G4zPybkdvX*# zBk0Ct(7Ai*$Eb6E;B<#E%Z9h&sbt&|jss3E~)v2Oum z-EFjQ0c+heKO$N!87xz|Rt{8HvNs`m2u9Uy(@x}V;zYhc2wlB}4JLhn{zOUk9$;)P z8@Zrbc077NMQt9$Yl^e}4!@_ZzqIJhlgFE<*051u-En!Fkh=)}_Bzzf{s-(`v}S5; z(0DO@))NYuTOMbA_MI9(J3I`F2d|g;au~|BVJM0ohHI4+jIH-ejILLXu9rvGR=3Rr z_K@cs@0W?5BxRbRFOk{CHp?>T+Y=SJHJuxYL+`aTd%uV;I6oU9JX`(}nZI!R%ni+g z^EzOv?vri2N4BxE@pGP=A4NB{EV2w@8*0}YdU@S8L2g|LAC!4zThnSMh@$jzkwV+m zGh9q`XQ3fgA2rDpLVRn9@X90d21r~=y}LuQ2}H&i%)XD%modm(&Q6i-e>XSp$3Cv~ z?hSeOg%IuD+_FuOnij>+LLnk;SLM-h?Y2gv;losi|^ z^Yt(Lfr&bou^ce!1@x?>EEES?2_j~tIVx%4aEE{rK33i}w_GminED+U0y!dJ;VUa3 zwes!?5z+P#_v|6=N>-#R=MNN}rpjqHiOl{L$Arx2$iG6N>=Sucune|T3FW=W^P83T zA{UvVn4XtKPNuddM6aB)Ws#$ZDo!BuBp}t7f2h}aJN0%hvi8r2E!q3SZ!`&o(ylZ? zhUfh<*}UR$xD^c90?qDZAy=K-yJ6PXA*Bumqz?7sLm(2m{kdTx!4&Ei{Ds%{oDL}| zWjumT)2msQl|9N-E2QHMbdxH;MMi$5WT$}uT-5ft9a_=*52ENN;}z}t)}mP*D1Ol? zIxbFBLDT98?U$YUXOY$^k+nj(%;B5|`sPoSR;{=SE9e&p@vBi-&Y22ZiNaFeUYTq0 z-e-eyPJz+9dV7i1{Ab@dbMxI7ovG=O)FNt@NL}^iPm?W%pCwxs(7%`IUy%MK(^UO{ z{(0!1q^EbjIGCQci7=Exe}S5qpS|CEjlL2Mb+!q5QwXA|&I}=-?#}*1>9>E)n`n_O z6}RbBA#+M2^2@4oUmbAhSFJSCCT!T{IXQW|(i6B&@9j@rW^(C4_ob_s;;I18Bl}kB zo8UIwyItSV=J_0gNTfSAQ`IF@dZB77wMqgsEbgxLBx}rL+z;6*b!>@8cViaYa}}~zv^VmdtD}I;4Zp%B;Ha3B1+l!1 zK_}2$Sq=~y?@3woz!erPrBmHJqJZ^~0;nF)%c7UjnO3M8P=Z>ts9HqA*qwntL3G zj}C)<8_UX{pFej&b{hxwZe|&{#t{5RDwu&L`po>mYSx$1n0oW)E{LJp>yKCypkJVD zvVrmCJ3-{vJZK86O@<-q22QPeh3UsiPfFb@Oh9_TDim=hl(7!5hj+}7oRD&z@=Giy zI%KU5Wlv~7LR#W>SHPZ`w+c9<+9Ii&BB>dX)D1EA6Bn`XO7zI*LZFDl6;e4^}iIO}Hc+D!z{djxpiL%sb; zZlYrQ5-x9(-CrWJidIJF7AloF&v2o;UvTt;`TIG@Z7hr?xZj5GR^;|Mg^a`o)H2S2cl$>lX%cvhACG_12HMt5!IUK=L*T_4uXb*12 zxakJU67(g4wP29l1$SZHufSb^m`xqx5E25DgFD}67Knuh0?k0>QPTkg;idh`Tri($3y6tqo7WZy<;;lH<9G*UK(NHaS ze>$+1gR&Oy7FM)!&x+=|)M|{P6^|3D_A;uzMp1#DU+X6H>=o$wC3UblagrgN^oi7B2uE)S!WHGsg(G9M2=4h|LT4R~^ zu!*Goz5~j@%<0jYOP!|Z*K+4+F`eD$I8=#yhv}h1&k8%{mapkAcc{KYo;iPaM^NI> zj&BnIKs{qckyX=Pj}{iCDVInU7>A}npRdjx0DFltT&RppT^$h`{IEz98VGmhVYv`W zIBP%&4YnNWQeI*_>5zq8=CX7MG?%6ZLRZ>x%&IY(1BkKCT4h~&=x<#zz=+`48wa#^ zfa!p872d)3n)afFXB#Q}coHmG(IszGq*(n{KJEj`rd$-BM1`oTO=LtSE<(yg>l z-tOB~J5z971BSNA&5f%-oEaMHzbu%b(=ESPsdIxeTZ6)ts~n}yZgIA#FI^9^HG_3f zA(swy_gyI7l{~3UpnqEbfXy((5%i|4(u(>M!}LoS)72kQy9f$D%9zz!Nh=7-{K5=G zR;IW67Nf;!-aaB|W`(psGvV=ITM^|mmr>cL9`zt&k21vGV{T|f)uyMQx~-)8@Kapk z?3tm(nizEtVog}QrxN|=dgk+D^D`SUa+6J?Qy`*#-5sF_x>(bf`9SAvi+gBKd4pM$ ze%8Bn9iz6Dplz>qH$1t9mWi zxGP*nml3x%^~Kw%t2Y@~>pisJ;YyT-`*&A;9immYBI(MR@ zFUfg$#$X}z4^~aoiLn;6g8{*i@2Lng>Tc~z*7&<&{@nC|EWaxxf9_(%z%ZJc{&;Fc zz;u0G?Y@b)_-_^r+~m%>lr(T_!>qgY)eAZ_`v*dkp0n0*BaKRNm9tLXm2pEZYbM?#4RtmW1qa5n&U6Lu!emOj1BM74wW;*0JtuvXPkx58l(lM2j zg?$TXjcQs41_LFs4NdQ`Vr$eQ-E5;>q25b%1xz-z^~WOqx3be^q}60}LH`e;TO(6! zR$fy)uB!J|n*G(b3`%nuO8}-WMpA<<6G^m&g{<+}pGEU>GXe%1^_mulm5)J|-v9-t zD#3u^UX~D52J$=1z87%snK4nX8JH9)d+*$p$uyBrKSYZfs8?$5OL6EUUX88<=MsW& z5iQ9J8jNT4s%c+`29~A%_N91Qm(Il~|HA8}=u|0$iNyT_s}l%nZI?h$Gtv;OZHG~! zg8Cy+6Q||D1L?LI)T@3pQ71d)nGm%gdLj#=eg`tq@X%;(1nVR>!5s~{H6Cb6GZkrX zftiH4`76Q3a2hLP%yN#_pAISgl(casd!icAPUq*b>;r@ivZTCbx(C^~5|Y$JRvpOo*Q0$b4j2oTB%v!f z+lKRMTb+)VS1vCv+bx7>tfkDhOrz)QUAfJ+Hrx;N=11C#IbC;4?9#&ZIkZ2f1#*3_ z>9k=udMkKIZeGBvJWJV`eSp__s%soD8tw_Kq-15UR#{LqZ<=Sj0uJ5PhiPjN9Vym& zs@i~8-w{CuaLP}S5)j8zF{dn~6hcYD&U0ZU+b7uR0HETPd`3C$YwV}oR}E5NuDw8_ zrU|ZN70g{7^J5&u4?%?F(id}50wqO|(eD?yj=8K{ZSRkwhGmm2$I1+H4&Y9xpHTS- z?re1DUZ5#Uc;MT z><=xz9g`dXx&^%y2lx>6O)LSN?o36f+wt+$o&J_E1+bA*j(NYay7M*pG%H6xiiIH& zKOfT60Yr^+sIyMVyzX#b&l>~iU5MaaR<-07&<^NQGy~2RawZ-e%+EJ5KR^3`@Zb3o zGv?Vnt-mFMP5dCHfXj~5rnCV2jq|q_@aLKQ8O#6-B)%Higz7YZHQ>1shdF)O2K@61 zyS{;i=iNsC0@x%iFbKZqYzzN(7TPisAr(+{8?3N$Qtmd{`zY0BFgJiDpqK+w%u09t z2WFkF?!KfOyssadI(fT&hQaN$cPzJ*tHw5)PAj^v*xv4+VOW~rPtM-;GF+oXITY*% z;0zO?z3jHU3Cq$!|CYU`Q)Zj|sqOS+@Rzl-`hRlmNA^B=_WRSTj3q@U?zS?iN%Qrr zRo9hV$7#lhdo%qANqrY4~?9lwJwX|T=j-BlH zBz=G*oU{QBVS1m~852N^r8Fqax2w{f3<@cgXo$V48^s9=HcFcb_gyZ%bKdpfBDwT_ zVQ3!p9badLzuMw_%?*Fei1YOswkh-rj(_-@L*vo=*=A3%&`xt^Q9;GZPKXiWz@aY! z&K8CXbN<0nOuIf-8c++nc~PyT<8zg%Sxy7Mx*hy0fh+T)>(s_%wQM<4DHg<*1apXM z>863)vXyQiU5&U=;=eB5arjT%)3C3CQLMw$e~HUFJg<${m9kqorp|!}8rAoSXGrz@ z?6$cbz7xxnw&C8ij@1eJ4W7Hgx*W^OA_@xE3>N-RmUuX>s!G7H+da(wU-uC25{qv6 zsIa)ccEf^d<+b^9I~Iugz5R*GG=pb!AhjUR{Z^{E`BrfG8G7l2JB_~EN-(_`dJJ(90js9^mGGmjF!)ZmInA;ucg@|lhb?~w4rY_ z_#c99LEo@0)-iyfEbugNy0Tkc{|AJf5Gq$-b(&jPh!_g%TbUc~)y30KxkWC~xzyo^ zX%6cScRv@QRIjJK@)9$prUN4vqh~q)6a?-}qDU%On@mwvdoJicivwKtPY)5j2Zdox z3yWLU>v zP;2zBKpI91zhIA+6ZVK0>q|rZhR{aRpx~c9^Krn*$DgSbqU$_2lh=e4~i%64i`5s2M9q zWVbc!a;L2t)D}|~l1-Zpl|3Z?JNu-6oGa}Rc>m;lH=N3v4v zVlg`p4Um=dsF5^IED!t{zwS)sp>Fuu z$q$4r-kqLt^W*1*KmInFrhV#4e}AZ<%{^P0XLBkKS6x6QOf3rhjUR++KP@ix?e{pA z_m;L%{Jp+|ANG2?jPQx-fI}a|dxB35a3A_EP#WR%#{nnck0I})oPiUS5@X;YyyA+x zC08)RSzFL6=pw8jqGuGkk7b}eFQwcF~ zxq6S4-gh3L`juGK3-GRcPhtub!R`^OrUcxS-hWYg-BLDDp{FPUI)Sc)h&rc!yv0UW zQB6hAZChyeT?ccTMYhzsSETQN%M&7ffw;`7sgZB46-qwo>fUQ;IR{Q-Oxh=Rnj30C z38;E=gI+wimlsEuaO8Sf!o4U#eeW`{*Nr&MfLaIa_q3?}Zq@d?RlVtSrjXi|i`f#L#Bp_%{Nn zI=8Vf79!9lY-IaPgBxVfSyd6)<*eVuQ|E{_+}Nr$yX1bXk*$|9+o}CONr9rAw7!YH z6P`(N7v-e;Ip%qqL(QZ1D~e5^?{ta2H;Bz8lFMxQQX%b0u1yNn#aDN+a+ws?Zz*I+ zL5@21FA;J5u)jo1G0@`5h@AiF`_SZ}mxk*fE9LlubgRVYnBC0Vly@kikcQ19+?#AV z=7;W9>kfxP74)nAI~00pm2Ra)y~chH$91328sG6E#+`xwvdUO-si3{UIhEvu2|;0a zfME7rTtXlg#5l@Ve>RmhxYCN988>Jmo=az(4pG}2bZySthKr z!sA$Anm&-aKajr0?p=aIaSN{TnwRk3=UK(N5kAV*zfqTke{%n&LU81TuuxpQS#U%I z0!>^0<+Ul1#8jFCu9>4PJig^}RDVi-&r@a?OEiGTBgrL7rB%D#`gYBq>o#Gstj_cs zFi&0v*MnCJ#{)bezslu-$cS>7ni)A|;V-Nr`^4o3a>wJI0}Vg;&4qJTaC^+Yb}Xqp zLXn#jd4bw!^JRF3<<7HZnj6-GD@0q#CY?Ij_nI3XN{k`X;Q!3re19UyH26R>KlGEt zu*yWalF9jqF1LE-)YaN@bncRf$m7}p<N=ozTD`sP$o;?FmPxg;ad3)n)X^6wa(mp2h9?rqCBEJ%`G=U zql8F49nr=S)3BWZNCV`Zb< z!_Wbj_&+?jKONZjAIZMYvH#ayuGucYI$?&PFF{GBc~A9>mHF#l5+iak)iJzp0^rUg zfGkhCk}3dR762wR0!ZL*gapv(F41W>tQlC+khN{4@~I6y_A_o$71$D z|10}MpHW^9Rz>1dz+_qbayPYW%AQj{hv#-f6t{u8rH`3kQt< zSy(B0;5nCWnZaf1OQ6wKXM7Od{N_mWHGYiJ!0Z@re+rxiVbhye_@MgxLesq8C0jza zKq37o8*I4U+b;%t6l#a=5QO6`}0dKrPY5`~d z3_6C?m5NG7?+u{XL?x^^ZC5?XTh@Z^9G%x-n@P!AR&lLc8c=I9Ck?3j+13b?;6AGQ zRjztq1AVRW8rWbfG6)2g%KQ%+Hb+HirTX(PY5aDi_96&xZ2P!%?h&$e-`cQI->YFq zp8EEYt8^~WhVHX_#L%TxlseS6`$ZGYgPQn1A42C-->0Qhzq6%_w3g0@Zs|p$rG3ut zznuR0WTZj&45*5!sD(F33g!?bJ-9JdR2HjkCsC6n%(3g#ICd)v*^DQb<#xjc%I#t} zc8g|Efz39!igOu-m~PGt&9Wz5oi&4{#xo6?)Hq$eijvknCUmW^oE*h2gG)NiIC|yR zA#w9aoiUjCN9zyuIE2Sl$&E_YIh>hcie>3dP)aeyd|$QU*U6TVze%?Ih5nsdpKK|k zf0Jmx>huYTN!ovg82t^fUANl6UJ5|LXSnj2c|}5EcwQZfr{I77Dz2J*6xCgL zqg((7f5HX7_Fuxcf>i`w44v+e#p4!vqj<+Z*N3@Fo%PUd*7w%LYs?M5imdyov5j$v zG0GKVGzKbLQA3w0k~t32?l5apL+h8aO1^sJaV~wN7(<8Cib2`-O(-M>li#1CcfpH# zMFZ!NaX3@)`Jo6nzb~hE-zQO1K(~1)meOrLtU|i}u8D)Xs`RV#AODt-ujHXOYD52{ zyLjktI8NQ(%1fU%U*9C>=^NtAzkogWIOg9N-l4w~JJh0nSirZT9OX?Z&>o7r5q4=8 zY=guO&EXyT`gPcjl`KQvcAciqlcxUlI&GV#slU5!pa9rXLfaPWA3AhGh>P81(N#LT z!@JFVW7KYw-|5U=PkU<%{rklq#J^#`Pqw7ezjgHQU-a+9??t-vnPkfc&j`CI?Z5H} zo2qx&+=^8Z{?ydL_N13eUibx?0qwccfYvj{6{>U%b%m5Y>P4qRq0UBLun|_)Gt~=( ze|SyZWV9S(&#;%;pfv+}D@p^yeWRv12Uyx{(i= ze9-Iz@l#&OGdSs5mWt#^sYqIO$Tf=RP&gpAvQh#O`0B8!j6$JBAW&$UdU6UY-k?<1 z5MhpW7)cv!1xPZ8Nqv)&e4i{!Q%u{So+FU6$pK|Rz5VN2H5R>E9Ig!NR&fd0-muKm_18wN@QjzpPcTD|yR{EC~ zZPY5+n!YwjdIol-^Nu(b^-iTV;CzN=UQ{)nt1|LSK!G?zeQq+{6*t+kFuem+jHW*F zT}b&!4KvKR1AK+QoNLuNO4W;w0WC_&+HoOSqaLzwO-SC%tJ|8qundq(%$*rjySyeT zM-4t9oDK({U>*l6Dgq!<;8fX4Qt!n5s5`gC(p1Xmt8wxVW+C+UK?51 zMxkai{Mx~KC0MCj*nF>57~ml^USLKDQq2|5iA9_G=h%aJ=Z%usdgpdVtsO_h!>m`v zoE9pVG`+46VZ6Ti(bUscZJwSMS(}bV%}~EKLq?vVzRP3Cc&*Y~Mb4X!M&on$g(AqY z_Imb#1|ALJHu``V2jMw-L3q|ox>Z*{muy)=|Nct<{{CDn{;WT;UdQ@pm8_!o%2g9F zM^Gx0YNyGa97j*DRXPtrdF9MmTqA*-Aj5=vraB$ZTqe()_iE_sthQ~^Y13s<@4hDU|hHYF(uTuZiJmKpI(p^~TOf3qlnMKMB z7?yi$JJSgFdS zwwfF08NMs_s=ualDY|BD{t#10ke`UVqx7<-)brP1^p6R}pF&#|u3tX_Dt|@TCEY++ z2*HK0Wr|&TR)ef?C35GX7MwGiXs4`nZFnse&SN)W1g7!`-~_MVfr>F_-z1w?^d(~x zJ-_26+C-vcW^cM^W$jVZeiP!@L+?=$vO@78)X#$Aa8WT?lL2w)*COlN$XaGgEzz>> z7?72&9n?)1jo5uJ@QI=HM=ZVIrC0snp}2aP##TOt?ERkbn234?jmb`GgPML$^pdM^ zK6z7Q$wgWNBVJ*7ubDLlr=<3Xm~U=pa7vmC-S$AWR%fBD%;D0bXCJQ%g(?Hhwkn!B z7P^D?Yh8uPck_{H;>(RI!KDwZwk>3>@jXPzWvClZ#*WFMp6{6D1{8Y3+2L7MN){4m zS#gE4Nb8m*{vfA5uMKio^@ay|X-nKe{>_u$caRIvq!-VM9puz+8RRG95Ax!uLB1_! zkP|S-zP0fN*+})^#(H`dHKO`AQG=c6N{AWkO!c}G|HHvPhC&ae#To3)v%Y<>fB!db z37u9yUrZ;}VQjLL4U|b4vyuMZwr(BahpH$iTkD$RT+|$lUDPq7nXo}z zGFA%qTz95nSc_r!$G9lW;+}N|%)-OtO2mBqy)_ilmVF|vorRQ$nR1H$W~iHv55fmU z7`WD-KQBd56nTNiCj4Ev^VO zL{^vsE6j$%4eM<1WQoi4KYga9bw{{~s3Yn$Oo+2h{ILW1J zf-a0O>JNXJCxBP&04cU#>B$cHI3kN~Sw-X{toImIa?+cZUX10$}`l> zpAA?BJHyN1lcu=pkfR^9VY|-b6>IRdi4%8-vZum5O1em<6-b%mV&IX>8bKU}-Aswl|ZD)k?iPSjdT{cI#DXn844J?6hmqy%v)oMp3_p2}B_spaFXy1K2lRiHCHW7loCBiE9*#OApb2J4CLB&a{mfmoNFlay+Z z?rG7~a9M$*hSi%V>hfM;x!bYLj@jTNZ!V%AT(I)hkJGI>%}lw^$N>-#m!Zg3E1664 zTn2arry+xXekZge2%=WK=dy`9`$_k?G)}99r__8$MX|F0q~?cap;g#tq#GW-3f}Ls zbQT1sYvw{VYAvpJn{41dR=PqJoaUsoJZghyCX~Xc$c^n;mQr=(`&g*qn;;d>XB94k z)PyH_s>}_kdhEp%qQQ5ockc{^dO@pej@(Y_n1u`2!T!K@>*DIq;=-d%2G4a|5_?zE9l4%Uvl@C;GB06q1)MCpGLbF*fNAtsmFF$87h$YUqT`5jwXl_^jt> zUo)bLdM;Abcp6A9iiWSmA&dIvU!thdu{Y!@c+ZM*CkESV8rfXAkag+{XR))!v@Vgr za_?VLn$m28icgH=h+gIMX3JHcP=z}mbK*XFm#^6!EUz~TmSPdiSbJ&Vn#>J1 z0(%5}h&fZS4IkKXsf}v;c&MvOh+!{#UxqG(67^CTnqg*|G2%93@ zW?Bqdv$sDOR~1J#yCe;&8AHIaNL5xmIrTEj9c|FWPvkcG^*BehXbypVAL~K9ilNd4 zUX&x2YWD5@CKQ6QV}vgXekheJIHI4@{)@veb?N2zeS^OfzAqi5zgT|+J}FD#>P~Y@ zOIROnX&oScLGU)sv&|zMpXPtkhM|UrqozE7xWe?DOB*Af&AbY2Ih2;Kh!_h>4(lV@ zzo6ZLtWcNBbWa+rd2Ny0W@BzE>oZ;jD_ogLuii42x0I<#G(asC?~X13z+3EEO6$!h zL|a)JWu$lg4KUIx)r{_3ekQ#Up~n8jvNU^#JI$eeyNSMekLvATELZIv9!E4q+caCF zXdxG%WqgcIYl?@6+wx;yUX{4;wRA9(>L~lV|9522obDruMD>Q<(XVF2|uDz3fgmn0*{1tcW8}*^B;8l-!S#K_F%C4;b!M3SUdT{6Ov)R`GJP2TP2R z4V(g8PtCq7l7;7abHikMvhOrEOiB)G3)x>MIwU!cT32XDM6K(KA=ouaDq|liG%Y(6 z@L0mp{>%+2dR8Hf*3mIH_v<1h9Kkb9vAF@ZHj3euDBT>w&Gc8(zsd-^+yO3Q48EDsMBs`GuIQf*QtEO;z_i?3xYrU|5TL*8n%uPALd2KU0keg5z%p3} zjK_WDJ0hYXVum#T9U@%T0iz3(x#jmstOJ&*Gq*gM6s-dW-ZoriK@V@Ixn+yUL1Xu4 z9os%wg}T)qwQyS~L=!MSdIEl`8?5jaQTqBOR_W_HC>}rYus&*+uZX9Gh|0(;pWi@D)8AcJE# zkpiZBm7<*%gshb+o^D2>8FbyV(Z5WyFAu7cxwQ4Opr!eyxdA4>+ScD=Za6ERMyDZ4 zqq9FDYU^W%WQTwNyq%U+QOl3*zXpuSt#VA}^O#f)==}`d?};Brv@HkhbhdM&!TbIj zZ4ulyeXTdQ6efhXSYDKxCG=)9^@7~Z(6(7ec$>W_&Son*Dby?Xs==3et5u8mUf*}S zef_`PZW(9VZhO_)FMr2&dn&g1CN%%-m&03*l`GMcX#L9XvH8CKpKZRq>hE6qmd*D^ zZL~Q|BsQOHE^WZQ>YXof__a)$#OMv^(aEO&U*CXVV*~DuY`~8M0+7>&Nv+kO#cRsh#>sE7|L6fA)9YW zw{)Sbe)Ty)G}sOTF$pneLG}Sx3iRK>5Tf-vQ#yc{r?y(*@(21c1?n%Y0|n2yg4Ylh z8XprvW0n{TT;VBWne~kE6TuK>8xg@nNs~seWf2uRsxVi**vba42~7T4tpl}iQcI}D zZXv_>2RNSVQ;3&J7g}M<)&hM-?`x?M`#-_Yuzz80U}n|8Ta3B6D~g|i^RTHEUdc6w z=06Z|sM$>RY#EPwluJ(w>GJ~w#qp^1eG8&2v~PAx$~RU-RQI4wmrd6ZgY?*Aeq_7% zWBsJ<{%NQE({#D5FhcU=mQulwABa>x#NUb<?&XeWH74XX6O?|Js>^dVm9mMFeKX?*un{-@x6N?rRiZh59u4paBk+rR-iY-29!g1OaxAAC>v zXX)nG@4!0UZq&)%{rY==W*_j+H)u1pY4a_a!GK%c(awqQQ6gFs-40iX{x>oWeC|J^G#5;FM69;67j@yojCSc@YogMLc)>V8u<|Ht`$BRgF7)Vw8v>%eje}9MKF3 zs^7;xWe|&RC%5IS=(b2z1)-DLlTO7wBAk;5zGG*4{GGL6E;S2CWW8 zvgJDZ-Lf~rlP+cldD2Jc&p@F!BIGhhMsR0|2nyyFn5I=$x(xnp_^7@Js&}ty`8B&f z&_Xs>&*>=7{zUpExS=#q&HgmP=y>8l|5aiBgV(NeC&JR0v8r4doxV?Ctv%P1CA1DA z^{BbE5&eVATIe4nQftmT$QA{2It~=tOu>2dk3oWXYkT*rLrGS;!S?L`*L#QFIk6+@ z{;Ll49O{t@4Ie!aDe%ApD3BJi`sRU9h^=|%Cw0^1XMvwbhAI&F%gzq6myhh(hBAaK zSMRy3&k$8`OH{!Ra>2knOQ_&Tceu^b{GwnSe$gfq^Y3@iPbsbKG&)@TPqJo5j1Q)? z5>2gdvxy#iA4SlOSA=DzNeN^MiJqlznDPj_7NO&_qOufmI3c}KXRl;+r8YiG?H%S8 z2M1NIO|kbc9ja{F3i+4h6F6ZqO%qlDD`oYC|4z_Yg(E17(?FG1sB6QizQ5>stq-yW z?}UptS*5ab@;>jEdg|93)XmyS;ffn%#bJJnRZU8=0*gdYqvp^9YAW#?d#8UBuNlvkFvN1G zZ{IJ}IZi3ZYDd|(a(m;iqgN8-Iud6H+s~I+D`1p7pKb-tXNs|q|M_`%c~?XK=b4Qd zm-a|1go)0DJX+62Qrm_>w9X|w5=kur>?%%O9!Z60(b>SMcSKUnG`7wqoLU-59f8zE zoO*R6H3g|PoO(edbrRYzk5flRQqz#S`F6DT)Mw$@h3F(^b`#9a*CwB7cIDj8i=A8E z(C$K>T-=Agb$%D<;?FM%%|1Z$($&E83Q^vYsEHid4kCFeOFq07NW@am2{PC{joa}p zZ75U^geIU31*`c3(GeOt8FNtO*m*?w8$HFu7Ze-t0?t)j4W^jn0 zAcTQXPo8}`q#UDxC>k&jMcP1IN5dfc_ibab?;C9_;tm7i3XkCqUhZ&aQX8rDZ&T~x zP(TjCE1yRXf-&wOMECjYuc^-mpn_uMKHv3exX)kEtP9ylGqc3~f|KlT5P|KRIhwuX zr$Zc`Ach# zDKdfVGcqq_9yFHeB9cihbLj#%8J(%ctk{&M`WDkl?m?7Kw(!s9B`I!W93DuU5uOd@ zn!t?Mfv=!p0JD+Ce!$4r+VZ&;T;XJMlhrG+)L`$de(WK-y}#0_BM`OVGfqy&>Qf)S zeSo;^J;!Ad>Fq>qSkI5}%v|oMbiUzWFAwV1wC3c5@%)AMS2{P-H}&u_E;|j0>Xie_ zjv&-UTG<0t(zETt34>L={_6>1*Nb{CE2(roBdcE)uAbe}4B=zTQpzKsPP|)$;8l9$ zHX5RCJ2gQ^gYHwnoQ*muQ$^esS%K2S`XkDl%2lOWPrY4*swK9z7F_WdG-%nx2<1^g}8D^6CBIZy5-}!3;jl!zBbuMINozF zjv|y0?mK3dfLw&@gYPr+-r+cB*>jIPW|}faB2bF zM4d&P>SmI{Xzw&PT#eL1PTdxPwNh$?60dMt9Gd)6B5uTR+ju+_V#<6$HC@!UQoEuQ z6WUDdj7p+Uw(2)Jm>k=m-PX4!I(_xVEG_+cx&~hfk8yj{Ri03YQhoz-cO_YIWFi(1 zd)cM^=e;Fvpw2(sW8)z-_}Y}Il)-&*sGFSxL7!b@8KD`Q_0u)B?{aBr#xLhbfG6ED zfNdhcj?9J{s5mrmKs< ze-Vj^Kj)ujk@zGM16rJ)OlszPv$>hIKaR`Y`SoI7}LALb?hy ze}N--U~C`y(C`jDY4Lr|6{U}tBL<1a+~0G%j{l6?WeA(3YSXxS0FrAiwP`m7oEQ-|V7pvfCiiM#(xI?ZB=r?mSiP;gw%GcI~K%MFZ%c{Ezajzz`yy= z*Pw`ifqy+adN0F?!YZ4))p}9Ha8U%!mS?YC2F4(w&U!a|AH$0o+=8+nfyCGQz7-Di5Q(R;2_8aWuG zf%QDSyP#_@RLVEpL;NlLam{fCc|`aej`_}ChH&Vju(Ia$!t17*;4ajn*X~spH$^1t zqu~NEO@yr-$zf~zjFe=4s*UD(xJyUqZ?C%VKcP@Hbgi=60-_lqnw3=1i2d%MCg6hG zC~vy~MgFTg)@HR-33}~%_t|i0sV=ATx~?qH+pWfbaw%be0p1#vW9kKSzr{4PBGzhrG8N${Ra0Mx_SN9I zi|z0JmGHf-z;`4ukA4JO2EUjTG5vlvDPkV}bkblmaG>jOoP~;2_W!!Ckj~6~A@@at zU73ev7}6_59&3!aQzEX*xAvI>srM>HZ>3^1{Y4$A;)BN&D7%YV{1)(|&TCKfxV*v# zL&&{Q`bAY54Qb%E)W08^pwqSYr{oy|rlhMZo(!*WE2wjy>k<+V#27TvrnzD5SNuYc zpH{Ml@2X?Ckm|u(W`BJBr!@bj;V>4F{UYO|1OtZUD~mcN26AZg`KO!w(=Gn#ss8C{ z{^{xd>DJsN%u5p-!}y-ZHrO}78aQW8JYyT-k9G406QTnS44o6_=qh)pm)vgCQH9b4 zlwhhO0zp8R7Lmt~R35>2)3iZ<_*|kvx_pqR@sLU(n;By}Or+!RTnSTv=@aM23usbY znteb>u*#KQb4ytQHb-Eog>}Rszt4F})1OA9qd5B0L!%VozeZrZZU13R8IBg7qD2dT zZKBRI)$X>Xx%1^WbHi@VAyt}i)MM7Vl6g%pe3dHPqxLrPNpU$H7`@Q!W-UO2zy+SG zy2%rq?HqY|>2x|@@*k0VeGIgK#@`u&GtQxPuX5S$;Y5fC z;M%`qpNQwa6ol0qh9i(pzeLYa-hF?0g&NoaFH6D^tDqOQd@VNL9I^C7e1J;jGxf)} zhfhZsi4t}&ELVy;Q;U_oea1lQa;5Me7DuIe=U+piylwEWOiQ{^^RrCV{df`ooYNl@ z;419j1>XDf&aLchtQi7n&uBhVO5u*3-N)Dbi*u(O4uv42{_kmeoer}7Y)zVl^KzMq z2>-o&pY&H7gttF?g>hM4SnPXV;3uXqN_ChjL;L;HV&vAf@3YK|BWC!g8w1ZlFHv0L z_oz3~)r3fiwh+5x_)`*Fw(tBr*^*aHOXsPiXwp2(*;9fuLx@egrW>#lg!FWQh=~+N zaUGn!nz9QOi5K%vV^$|bOEz|t2+2kSF?N*l9=Rc`N15PBruX|B=(_WXrjIZbZOUg{ zZ9nCnJotP_ljnma_i2{#8=>E_hA?p0>2&R@QFYSCp>@3Gt|(Bz8lVL+p8uvBqQ^;Ti3H1x(%OH$y>~*O zc9`B9B1#BhvU0f$vk&${Vfnf)O1>`DLyGy>&NyX_as9z!^@!sSAy&^(M}A|)kv)Wu zW&^#%bJ)?@h?@1iqkwio31YM7a%arGuXW+(=#?>sI|iw%i1s}8l{J=(NFGQ=aC_>% z=JxPM)%~?KE~(JjIG)?{Ah1zL?^&A5L+bH_hsAiNVHx?=_ZpV3PIEJi+>B37pJ`Z7 zt3JlvmSKi8o#;6~9LV(Q1LE#ai{S&hg2yhr#ZS|w@26#aC2vuCBao}PvxcXJb161;=vovgSSznoi?CHgRC?RePJ=6 zcWsIpAyJRH;Wx~#P(OV*cCb!sZq80j;=%gYAEO4#b0-hfvnRjTK&}3Yhr`UnvEr+! zvAI9`<*u*d50sx98r5>$(PYaq`gh;4WXt29C0oXQl5DAOR*+l%c3imjKIrN=yo0Y@OflTqmYD<^n0q14ce*!y=1B{*9HgzJ-tmnI^zKlax0`kwAfBPAZFrzR zCN5>veLcpNxn|lxzb5>eTV4aj6w${F`;WS4u>YvYU4@PCyoC)WeSrQ%>swK|kg3?) zmm0E9^d&F5L%1IO8{cZ?`A)pFp74k#>7@eZBBX9#yL z%S$yr$VEyEQstWd;lO-J;e4Jw0vXhAKMupvHK=w$BxWaFw(8eXl3(_-Tsh)B++TkL z)NY015s^HVRr!>+f|2_cT0-9bkmsBV=jKvPOZ2v*r-Y+2<-_?x=`6QchAJ(yWtAx7 ziXF;>D1&L0o$8ReNjhL{=u;~%vWf3lgN3%NAZ!c9(%&4HE?8hv_V(-! z>Uws++a4@P>v^4i=GoGN1%{s2g9VnJH;|kzldZu5!ja8`dvi7GwF9R{T9)$n|>YiyM%tr=(m=B ztCVR*mmZvrf?{~HEODqK4qzRmYU_aUNd~QU^Q(<7!2SQ4jkL(@+dXFUt1X*H!O&$B zCAgOEKa-ra_ z6fl{>T8q5VTsnu0VkcW5>m`C6E%9^pbE~~OGBivWvBcHzVHHAFH9iBj$SSO&KntEZ zMsN|0pPrg#?XmKDnPTW!Xb565r+|FQ?OA9fq*w=i5xE_(I!lx_YpI0d61mb-50%*v z`ML&65E5wu%B1u(N;k>1h&7l_@2T{jA=hGNFpc*~l>wV1SY@O=^h?yms?^;7Vx?t0 zRplrKUM48(!9uReSxseR$ONOT&vsC(bJUf=WoL@AsS2ZaM-?+}yhqX{kBo-JeF5%l zNf6Mu(}RsYG?+oG@5Y1T$rNmSkDmI@qh=&9hnx4I3p+>%m4wAJPXMc!b|| z(fA$byS_eSycUN?yl2L|)A>(*H|I4T8AFRVx$cD{VD$Lf{32nt=JPO9**Ur8*cdP( zhAo?*jAfP?UA6sDY`iwG(Q&ew)#I|sc-wmbM)gk*eK!8^mM>r^QYQqfnm%I#RsSk ze<8n^4v1fiy7WgWMEIme#-p>oc6{^zq5uARW12P|^+!OeAc&Uo;$sBqJK;$cXVT@L zp9+aU4Y1#LHZynEW`cvBf0nZIz+d7-5-x*8FmWtGMWC}_+5)^)iy5?GbvM|+b&0n) z#rikVPj0XZo{jQ@Rlwc_!74x&2CLHPXJ)XoG@M1yaHS5gPF^hhiY^_ViQdo<_YiRPOgiNn*AQj5de`SfuRe-Eh1?-Q!bLMY zY30;y+bq~(tVPwr5mCxmWwIc*#FEnq~D+Dz@cCI0se?YlAl_jK)h zYW(jO@m(az_z(FGr=d(lv6HQ}qP6)Y&i>LUXy$7i;grrs^if~EJ$=A_I@k0Ve!K|9@+tQlxk&_<-4>0XnDa-c zLfDDQQ%E)rjcBWGjO z7iG;VS;AsU%|4LViuYybN%>eKZ)($H$aZ%ovgz#%QF~Zfvq|RsA##3__uMAF(SXC0 zeL&0KhQCE=HWaXoTg)kiXLC%tMM!Dkl(Mr5P}axi42k3_M6RB5v|Pc$vu9FDFx{pm zrfXp47mCt~0B1q3UekL!9CmI=?Ssmi^|FPl&k-%e`?CD(17Z1J<3>@F0VTZ4HEk5_ zv!Q)&oFmIDuyfwpz2+BM*6Kij>{FknkLDNFtdn)DqE}p@qvua)hy4<*qM0WDOvB48 zN_QAL^^}m=1@E_b&+yN*Q4;omE9878>5>8V0Wjh~Z05jCR_*?%{^sooMB3w(<1se>%Ta;TQ~^jZg5nx%&R zr61VA0sVVwNWA{t{=VqnPT9ZYZ|&clA@TY*>HYZq+j4c>{=I+W!2bR6>Vf^s`qzN| zO&Hui5&Z@Z7t*}ngb>Ig@})i;ES$~r*~1V{<=ddT)2mE z92QG$M_&?trj|QD4}MK+hrrW-4>UC-He#%4SIu9j`H$d)a$8MJJpINRpfY_Ei~ zJtMN6{;$Yp)UqA;FS0!oo2@aN?J<$l z+0w$P#<-s2)$nwD)R zXImVb?PuX^H6q(zWwvxJ+b=jDOOaJDj$?E#rBQ_BE)K!v|{GiNJ{&6X6-Rw%L+ z%4}I$wn?0=FgDxXgIczHku61LvuW9me1UBFvDqFCXUh@U{_%y#mZ4>Pp0nk|W?LA} zmLak|Ewkll*_Ls(jM!`!hO?!MY)+XiSIahov!%yo`}}~G%_Op2CbQ*h*({vR6q{{J zINRwm&i2*kB3pr$E%-UI;cHa3hH$ncB3p;dR;XqBHD@~#n{AfJ7GVf7{l>&wpm8lO zmP98Q#|_a{23ioYQ8ld}MI=Pa<}rezm^GRyikHR+isBl24K!{YBl2aX4uKTm!f^;N zXDCCXdZ_5B;+*C*m78ct`Wt4C}CHKV*ew7mk8jGB=8-m zWsNFfPgxlDh=eT?u%AfSt3|;_CGaJp;7=v+1Oa?Z0$aG?;}Z7Q7BskDg099}%V`PO zh_^LRO#NyO%ssw7&1*l-ekf0N|%s23{XpkgnW=|e^Nr)xXiT@ax9m* zUP2mC%9>{+SM6G_;^3C>8u))bzTMdP(Od6pp-M<9Nc!fmdVZGKi{s0(Mf zKL!15kbTJH?5iT#=i+lqlkC(Xab=ffg~s#Jmsvl z0@rU7Ztcect!wG>D6aISr}_SG8p2Sn6hK)3^&#EfC(8YALgA21q3{ZB#xoP;{>QgQ zuuDbw%bh%D8zt<`__}73ge@}hlx>#9KE=gum7tfJu(8`D==)sk8xnFfLv~BZBrayn z+Y+<_8JXV=&}Z=0a#(_1CC2}l1f47z@R@{7=hv3cC2$Hi_PB(7dN}U|3HpoS3_9jK z0r~;It{HotfPITz*Dd~SuBA9wkRkU!C3&~4XZheP$z{unmf!0tF0**MMC{~j!$ghw zIL_hhFOOpH>2XB?3;Gxc7l$?UL4-@doZg=s?mUrM-`Tu850pmUMOK}%0w*k8>;ezr z0iwbc;AA=2mACrCm&EnVnLeAnHUFm?S%45Ez?d*Vg`nRFz-g+$&9O$Ux-_n>!LqAu zTK0cz7HTG(g|atRTcwaw^5UUp`k8yAauYAapt$3)cP355Owpfx>LZs7P*2Ug5t+7V znSc!u=<$<}%!ox2%fFP3h6I~!)YKW5#2t{kL}_)IGiqV>*#AwcCgUwTHE zp^AxN`Vz8tfuRi7(`X4U%hZh_g59Mwo*wXy>uvPAVZgg>z`Jq4yJ^6?Wx#vtfcLcU zJA3jY zLDFjKp?i!sSLe3$yvdk>L zGLyOG0(#+$y%V7S{C|i)PBcPlo8Z1IcvMNjDc&~N}QuX0*(?-K| zh|8;OUr?Rh21-wPl#4O^b)d}(WZ^~(Rp7oF)F+3h{LhmAPWR*UaWhHX=R&1BU)5vTJJ49Pa!Vd$la_tzPg*iXU-OwIQ? zVCvh!Sy#`0+XU0OtnYm&IcVuthd!tts6QBqQW@N4;pW15XWbS)7|jhY@cHyWjrXWt zj^yy?!XvrR#v^%K*g)1j_hq>}Kkz-w%ku{1VNj?61%DmNR7I)O?MDN7HRA+BP>2OThJz&Oq-evv~zV?}p>A9%cRhEZ3g86fy zyh{Xhn$9yujP-6B>opi_a|0Y-D2?KxV-Y;Xa@OUYSuQz@E&ni6HJ;p3=MJ>}+703H zUwvNOZoHf&yJ2oP#%mgk5E>)cDCM?G1Iy0gpL<&TyXOM(l7eKI4I6lDo3kWBMwTy{q>p^b@-y={!lhZJ@l`d!($K z@#-gZtfV7`1zi0hM58=YQt3=dhhu0ruzazym%cAj9ZRfCFf7osPE0EMpes@T@Vl@R zj%j*UqDgh?D&d7-!tcXQRKCRIxNVE=|1~!tF1Ytq?2a09;3dpPKB3xflxYS1tr!w9SLnyWvUHk2|eReX`)#{#$ya7S?Hy2NHEDnzsgu5>}#St zk~XLNEVcReq*e60JSUj+*uz<%`_l!fPx%k^;OfzL)}bI2bR(4c5Kc}w)HAUCTH~P( zsVbGucX1V~s}&|Jsm0|b^qKOyS)mR(y_z;9QZiVba!C)79jrnvbbf78=a+Rwp(h&Z zW?Mr~$ycGeEsW}wy`D?qYL&V*GhG?8c}||ioBs=)yV85asO+FWT{<^*(Gr8Q!&ygP z`!3crveK0l>lHo%Z_>Fl;(@CWe#A%WVQrb8E79GlAsl}jY2 zzJHO8`Cwnxy($AMt)#!})~=^}MnMK&N@;H&Oo3lgeoF#thOeUSJ?*S0p5#DEThDe@ z7f&h^Zyl^OzPslGC=d6bV>#UMTvpE%in%$gt`7xGvAFW(;wq4ftFX_tZprVtB}$6> zcQQt0q*=lDgu~Q>yLiR2u5a)?UbE@Jqg?2e3~`MYdH9E;Tr!Z27Pd*&m8y(+dp*~X z>Al}3R8xgB2KC+xq~9IC+m`A((bZscjuqD@_YYjq(nyDAmw9wmSAYCpcAQl<9PHV> z>U9?TaUQ6?A;V#Rw)MnX}meB9=44h6@y*!=h8Cfd$Wv9CRM1QEdeg}&0 z09vdPn(zjpS2V{y+e8be#jOU*Ccg9Vh^GZUp0Ii&qWZckwtf9Cgy<>#b~6*Dl-p9 z6nKgr* zsJ5{W$= zm(Tw@*F({JKdTJ({-gI)NSvI-d?6GzYG`i4%kKhI3gJ-g2NqWwsUVZXM^vXR{cH1)oi=B>eY6dU;sBY2$XzUTK z4sQzXA@d(2J-L4W^$ttICubpjeCwmCTIf*!P z64G-bG#E_W4hWODc}!v~R7ccL&@t9ds@cud`b>>1*B?r%doIi39!{(19kGH6g1cyl z4wcy!zWW)P2y?naG(>(PK(%+e`iP7NcO5FURlWNGQ#Ui&S_PC&8C9R;q`G?KLAt^s;Av zVdj!x#*5qAKJ{O}XUx#Bl2HnegW2~)WLlN>_WlV zaHy9p|E?i0hljxI3u4gY55qGTMus733>Na9mXo6FZ2xkC@@97TVoJ`XVb>;0CJonzXUUx!JQ_QNK;S|FNv8Mw4$4Qe)Wx`C z_xujpM&}b1y4){j=YQB^M{JMBXPjw8&itRQNN#vf4bO@gg*8Z_r>cYFXL_?NBGa&7 zD>`-g0luN{Q|-Uyb0=9F3fR=PvP#uJS!!cJ%zH2?zY^lIHcec1|JlI7ZRt^#Q%sF8VeoL?!K1%YTr>Gg*vu_-opy{!)Cgi!V<8lE7b< zh%Xbxmzl+)ETYWSYVjpGwlsVx6kk3$Kc+N%xk!9@>wK^_<|-e~k6}R2>ZcBPkK~u~ zot{ELn`uT$C56jMN#A#WRu zXkEB~rvlfH}gLbB+5X@hr2Ywg1PUn1=X=iIhO$5JTV2 zSboe}P@oMk81`6{l+(}UTDjYre}IhfK$S6TOvgXcHh4wb85D}r0ApIE;NDV&)~E)d~# zmgO=_Z~Ni?P#1r~f8Z4 zq8wEobkL=BtUc*H`dvOk*u;Z-^4sk+yReEW#)PJpSMX|AK6CD%);n!C!eOl6ZA()R z&7Y5cAh42>%Sxow;#(;9;~x+0)A)p5^={t$Sz;IO3+cARCF&PtLuOF?@Z9l;3o zWoc&~Fv1m<*)pr=`}JLjm!X?G2gbnOBerP%{AXt0b4j??Z6}BVbHlo%@a53#Tb&do zPh61{B~QFx#3Rkst%IzP2@D$3;rFU=z20Jg)wO5VdIX z>+SE4ZCB0pbT6+9ogbo_hMRmDMJp}Jn6!s-5tM>6(wUOnvrF06^Lo$GcX!dXdKVSG z=iT?;eIvM=PYs5VZ&qrC5K!3O-Ng-^njv(z-k}kvt&@82iVPx#f43h2};00Q~*1B$>dW@M=gW@0oG2G=Ns61IZPnVv*pr7m1ZTe&D($Z z(&WvT14)>!hS%(6Nt^MbjB`ltByB%2`ZVeki5>LmmnN|W&xPEAx@}ON z)_GDjDbcyKA%FG3Nto1+mW^CKtcz84h1MmWd~M4YU47Q+P-vVt3Ab8OmpasXugV;e1`jcN?DXcsDJU|zOIP#Stz#R0UL|OHW zo}~(1jhgmWXutV|ondv(1ix_|(-KQ;I!}&_U&+==eLo0nYceugnb^%89Rqux`}#v* zsUD`1TtWniAYIpM@x6lrV;A`wp&C#3hKX|WTX#&9)7-j~9p*qiK7ul%{Gs?8VF}<) zS6>c>LPUa?be>6?Ca|z#HEPJ*Gc2MaIzasYMzNf#bs2vWtfzFiDsW#EOtSg1EE=tE zI24@Iaj56un6`+xm9A(>Tj_O94E;>E!n&8(g^7Er`ZUpy2b0?Vo@Eg}K@moeZi#d@ zzypfuU`jXTSO1V~1NlTy2Msn&{zR8Yrigle0ZR(O3LHUI*5g%t6a<)PaIhYBA3gOj z2N5aaQaQ!E66l4;2e(T1&rh)b7K8gIXViQs3v1Hdp!gZ4{d@-bG9}OCV~}&CGj+O0 z+&OuiPU}!F!UJcw;eHdH3IU<|@He4Q@1p|M2?<+U&pK`{-rIup^XU1VUeQx$@Nv8X zEG&n<--7PLnkD%7Li*b`Lv~s^h~Upvk#v0c;!(44C>TuIiEFQHQqlweVrGQ=EbdQ7*DVBDd}OA#zkr3SWx-0L@>#Ib5S(Cc z{vC6b^fo3j9#06fjNRkwuJ$nRI8|X;gZImjd#L>;gW2~p=rX-`E!=E0H^SaUS>3pn zegX~RjoobI2*oiv_A&>nsStY3GW#Yce2dyzw0aqfR@n0U-Q0pW_QCB+3OXO$?rjgz%$Efmp`cpI91~b(~2(yW~-x$xjYu`ar2#2z-lrZ~_TAvP1f7BU0#xw5t!jGnbZ5 zkf)#hw7vW9Ac`#PS;X0A(r>yf+{8^s1-V=Sp7MK&_{z#Myx;^DP%kjBe0qT~n9R~< zaRG++vohK%Z%Hx1p##BjeItXrHBQtJI!4FQzZnJrl5~GEwW`w4XVD;%=_{>10x<&Q z&;bCAgljgcOX*rr?cFgGtGg$%uK#nWC-NcYz7;&`Jz_0adKI5oN$`}A8LTjdS5&aV zAQx5eT(fT~u(9uZEV|N44&utL#MH_Q#!)VqW(BH_`3@{i$UYEQ60)B%w;a=Hih>w; zWuNzbbNl;-$*V7X(uzQ7`~&@n1H!eKkjL|W))PTXncw}4j!~r;dnQepVuGJgzvhXL zU=OzSq&Ob9BAiz>CGh=;U%a2{?WcsyF08d6YQWW9JPSnuP0X=eLlwtQ%V(xZlCFxmS^27dDL87%EK~3-h^Xfn1j7NmvpFU(ZRS|O zGnn$;Bg{CJrA9g~ z1$o6=uJJvvJXu_xP81=(fJwAi(%5OdQ_{swv0|sNe5Qv@L`4a2@cY~8k_K+<=Jy`) zZVetKY-OMW(xI0AbQJZTroa+t<$KX?#u7}lr}9KGt)3RedCHZXr+mqIDo`)EG8sA< zxFSI5AzwMBKKlTT>U2|ZNMNQRI6Vy-5Qk9nbc^Pmak_yLmISA#Yn~gY!%JgEaJumj zn2gh{0k^G?9xao*H?xmNy|>*IoN~48RSHdrwcbAi=135 zp|Kg-=!=9(;Z1LbF_L0j$F)Ffr)@)SB=7VzS;h2pS;bAlM<0uN15DPBS08q%Z9q)(+%5b9(G3~48BAodw1+kyWj_2r?&}dQPI=TC3n-~agx(T(rBEcS(n-W3Fnubc zBWO!v^Hj07SVC9w;>r7@=XDkuyun}G@(XVrHr!eyucNegg^O9BM?xh_!aF03k!*rv zUWPU45cgVO(XjVefjtGPN|WeQAgpw5YHPZXo2La^t!eUl-D;yJ_2j)1#GC=s7g)r^ zV74qY>sy7KgeOJJRjQayqnJmUIhLh}$^AY~WpQHi+{j6rsrizn`Qu3|2v1s;a3qUK z+lRAI-Fg`>FBT2yqzl{uZ5AsmZacUBzi;OPDWP-(TQkHcmjzoh#Zy(VHA_6r3%2Hn zr-i}RT=7&BY|R%>&R}bScv=)}Efi0SgRL{+2ylSJ#(TuVS`I+CI?rgn%Rda!edt_Xwf!aqFkan#Uj2JD?14ScBAX6eyIiD zS{rU+kUfM1tJ1w~M4|9rtTF@}ZTw)=V6b|;ZSXzXn5lte36NkTFPu#wkJm4%QP zKLB;{15lQzB-l8UAFN7B`X8k@$agOWqQW5R@^07rExos}EsO@@p0-Z?gJ}A?x>AeLT`mM?n!ljzG`VNCz!zdm983h5C;W78-eq^Q@uG{d2tMN8WE2j#NZA zhW4f7CbuWC*YH9c6q*eOQ?ja{(S9h&2)64{gtyeY!y`4{w{^uNTz>9BC!qwp0gtbUHzrr|cKr)uGmO#qViNi3T0*sY+w6 z$o*r&L4VxN)GFzU-ncGvwU{S&fhz%KeZ{i2_6+r#j0uqSuK4K$xgi{`1aEt~dM_n7 zoW?569%<|^*ez4F<`pp-znFscAhH7n8|da>{TpMXMpu2ecrvLk4o%Sg9)zSK78zE!8b%xa^tkG5soT+ZMFFhVMpb>SK5N!RHJS^ z!mS-CTI*03eQH88t2?Ai!+F{}TZF2!wAVKx;V#E*`|9f%cQ$$@Y~*fvD^ z&Jiu!>xs1NJds_xdT^-wk7_N+tr7xeoz`~gk8$JC*tV}dI-s#{MB8V&$2x1m3--4y zY(NKraNV{ySo?GNfbRUVFK%}>f2nonCZz?2eEt@;6Fb9 z(>?JKI9yv9z+dH&4MbL-c=NoS%2ZPmml_BlDb!te^(y9zUO=^>n(P1yKGCjsk1=oa zy$p@EaypHL=j%P8{U^A5izlV-l@FQckHwnAqIG3YlMjx_)b6)JA!V9QS>?tFM49Sd zX@@DBCxugM!>PhuavI_X?}SW40@O(WerX6p3mJn=E9@NptR_gz?5n9u9peHIRWzPy)!ZSvhMVC2vtPf=Ap@!|B znbkMO*Vg6B(hG5uJGrYMi$?^IHGs?kWTJm&jy51EnDGZs>$ykWT8Bkyk?3bPzoWJL$>yO}G%&O-+ z>6PyB$~2QQ%jhT$v_KrDx)=3_HtMy&6q>9p25CV_ciH zNFzeJ^__0B1!c0N-=f5h$q4CLe4Km+rjNULPpNo>^G*CvD%raY5Nw86!I6%Mhi`$4sTxtS8XXf)KL%|tVRt6AZGG?UG zOwyW?^Ekuk5`2;w={%@2EX>T;5x?sSXa|~%45(i zSi`RDN?Zn4NL+qR%T9*?bNFGa&NVd92uZ$cs6$=<3#(?hzl%O8EXa2qV}FeF9!z>n zh#?SF+|vG)RO|76ysSiF`+a0w#Th|Fxh%+7hxau&6kS&K7s&-dF|OwD*rY zpG#Ah@uVvsLC$ z8y~ac5)4d7slirEHp7&`wjB+u82};XTEdjs%1YF!(GbVzTPr48J@6>??A12mH%`nd z%G*K74wpepYS*o&L$0B&p~^m?prQT_U)h*b-LlFGG*c9Kj+Hj%!w(({4(1yePtG@@ zGS&TP)FeBoLTb39{Y$AzUTQvGtoVj`S1*24o8llVP*K+$ zv8$y=9f~Y^WZ_U`U#)@03=+(b&|s<`^Sk*GPHN&wSN|c?+MDE@!@q<~+up>}Zg0gx za~CTe%5F{l!7kMw@{t&(j{FR%(MV$@jpW+suDgjb$W}>f0su zv$^FGJv*IxXc_&koM#)|aKJs3!@zccIM%#7NtaD7gR`GzkmI!ejmTGeKZ19_|AyX0 zSlhko1)EQXWbnU5JJ~Ss{1N>jmt6U3&yr!fh6A3-l>SN^)gl5D=^ENZNFrsgOYcs0 z_JGxs1ZfAnSN(_%NFg;uEaCh1e;HCnD-W_&>+VoD56|Hj;ZRWOl0K|w zb@;F;aqollU~e}E?iaJs@>4zQ=#19UYs`qB*$+^Mx;f;vkIE5~3ROq&w8(!(W)x==sGKbDZ}<>XcHmVCTg6EQaj-bfKqQGXkHrf8;;BHMdN|F6Mjpt@ zX@=l59kdGh&}HGZID)e@`b-ftT8>{F(Yf+_zWiRM2(5+!f0cnx>cRmhbrk_r`KzS9 z9U}>_9igKJRkpOryq3!yA`1LqO*IkHlC{&)#fcRTV)`W5X3CVBP&dMt3XQyF*^-K< zCAC)_zA1WcpX!b?x5gLxLr|&9aEE4>P|nO=7Cl;fqnC? zQpTWuR6g|=R>|16x4aGKUmb@u8LhT*O7zbXa z@=NsjOh>cYvx9ElXFwL!9!msaC-r_ka$Jo0y3{v>wMQWwVqIGi_&GZ*%?UCG=WM~j zafKWe!V|5!<%v+JdI2@o6%uSM?ZdCN4>9z^8T`%J*gudsjjw-LnAN~UAu2p-tgj&V zwMu8ep8mHMUxx{-Ug>l@lU zpTql*^mvnt($yc-vL^?Nx@SJ+SaAfC<_!5hsl7+}Z~K3SM9FWI75(yzN$dnevRM6N zwdP^xD)A8?Y%;!7U_T@*ZY!Lxyy35*HKOm*!{3VkK7+I1X4WYDa#@m&X5D+V!1u}0 zr?qymr-;3a*b`6I0xgWseD98|LG#^D`>LT$gvs)ZRC{lU1j;&e3rm9_sz!X@k_sRg zeq93qgq&tbM@49Vb;p_KC#SSL`v8naGAor{I46LsKRF>8O@wI6MRZiyc|H!aD7S5% z{R^eveT_0snL4i&D%(L_@LVX&RI|cNH7nj!v*JxPE8bMI;!QOx-c&>JCRlBCHZ99y zAH|#gF+peVFgIL{6QatXWCy0}?bHUEY#k;$Q%%5F-Dsfs&ZsCZQLp>YBq#&tbQ-cS zggJ!&ik*fzmstX{YW!}3-=0L9A3kwtT54Q6XB}o^pYVp`cN)|mFHMRcJGCI~P|`CM zLOPFzx1?! z>3aXP4gT_HI;U;a2~Wy_X{v**C8C@Kbxu32SL;Sw*_?O?=5Ur;>};axQ12J}Cs0jz zX1Dsr+ceKzZaeTyZeX_FKI;wtv}4OgxOByVSv%D6=+?Aj;HXdASxMKsd53A5sX>N3 z5a!gtzI>+A0NhQ$&Q;6UmdVZKdY%b?{2M+loTD+A*Z-Uu?8?%e{-wu!A$L;UwK~rz zr!@@V6#!1l9M)5}z0LS=aipCa{L>E0cCJ@Fcc7hz@paZSm4qcW{hQiJqhz>BM?pQ@U;`qn7q3!IZ^W!ZM z4{fEfX&7NxAkJN$&%IrG0qYGJYTu?d2`ac!{q0{3u-a+_8SP0yA%DB_t57f8FM5|A zL8#7{s_N`I_i&d19!u04_MHsHl=}g^U4`9vvEPC$>9>F#)F8$=J`*O_LU8}*K@;SD z`(_((U4Wrh%MQq;18ds!n!PNYxm}Gm&{SCo7PY3-AWz>wBb?{*#d*SynV8S${W3&D zOlwxB7Tr!K!>k=$(@rx&z8bH$KBmk%E_anwh%!XSn~L@)j^Nnp$v5MOKb#1q?NINm z;@Ladb52`u`Z>x-z)%nrfd*x~!C)&wkznJu_V9x-*m@9`y$h+S zS`5Q0G*9VP5h7e^K{yb5yW2t?zR6H5Je|DgpkSG}n_%oj=%M5phGV8mz zN1L&__NkKw_DP7FuK!oG7!jc)GzPhG!R2ktP;grR3}q`i#?3nQVE2SrJ2;jMIi0gH zHnS_`%69I2K1-O`nOAqH&veJFdhffMq{~4RYO2nCiT7%B&)_+qZetVcwjpYY-jl|K z%SNbMi9A9}^nK-5wF1CBoPde8MX(lq6DRduCCW>u%VU}|lQL?#mxXg%G^Z?D*3L#` z)j4wnYOAaP#$^pNppJJU?kT5EX8qoGy_Y z=I|wQ?Hs;D?o?m-jaBEgJ%=h4Rb3vhQmuq?2SOIC)^d6&r^Ct1!Ff>#!nXOmc$Ixk z+$8n#xIGYy_;=+KaJx>I3mGKY0kN3fmugG#2g-CqAhn<_|L^ZVfb}qT-TJ>uLH=;s zbpNgkg^V!d^y`jy0W%ycuG5k_z z(Af^BjhzglQ-_HS=Lnbo^#omAk!`qsWua~OAeI*$j)6gkWBn0$0AZ4dsd^$^iKI*? z>c>j|Qt4ls6IhD>fH?6#X*-U(&wXtI3|X!AFFG4+kRj=j&)fh5Wzdv?DP|a`m}%GC zmvn>o^|P49%+%~%FGKiv@&og# z_LITkJB!uicP4>CgC%TsTOKuS*#yPZiTJX9({`n()4-2K+YK~hR8MN{`}Qx~Lo3u5 zdU$X_Nl*iG)Zgux01})<7L<@F_jH%&1k6v}#nS7Y8|Ip+@(c8o)cd-axVFhIC#qFc z8e&S@J0uuTt9z(Jy;f9vd0X#YY%0PQBJLlsb0+%?gFC%*1)@6XI#;|Ve$=u2(~BQv z1CKGgVpR61iC27$OKWQO33~@^(b2kPg}DFOzo<>IA6q&U9S;~jXT=0sSl1gA9ni_R zVu$kJk_2I6NNXtxS@kFfmhH!C+e!nT8s|Zvz`bf6Xo?Q^1$j>+tcQC79Bkqa%+xMX zed7cV#_H_mO~%(|PquT1$o;`?&OH!|8Q;;?b;Be&m$-iCysgZ4`g2qkrxqKL9Z}QA zUq*ebqK{id&`GVapf4CJ7nhgH8`|2Z&DtQF8{j}lVXs^EN`txKw_+#@RQu1Yy1G|+ z*tb|Qze!slZK`{fr@)=)-EF9QmFEG^#=2K~wPy=m7iy%JvzZ_}XVOxfNz2GlCvJ2# zeE0g;W7VDLu)5DxR`Ue9tR>>^blH+qA&~Yn)h+^2W*EQ-3bB2uqu4bZ@nuGS70Nyk zwcZdUK^^*9%z8^w)%G*2w|#%-v8IhbT4}Z&)(O+QuxGvZbXpYd7<{m85I!g_%|4Jj z9$Nx>8{>0Zu@l^5sxNPwOS{?-3BgLGl~=kl)w};ONqCp_*V-)hQ`pLkNZfh;yN&(@ zCjSKg0)xOFvn~8f>%gX0-)-ZbfHO=9)&%t~T7oE}&p>5Z@>=0loJKYsJ@hP78sDRL zpbkHO@wfKy--GnGZ=t;rzwRn~Bd7A)(jk6+BUECDMI~Elto)5AMIaL{YS}qTs*f+m zEnI$TJrD_(Q2U{>4ZjMPYwtJvU@Vu{iWYf>_@BeC!hZv%45j_?lV4AMU1_JbUZ8L- zw3KOvE`D2Rb`Ea-BH8>JdzbRu;qTFbQ*X!XK$=qKi`RP9-sc`#uKwvQu3zCq0R{mM zfzrg-02|HE^YI}o3UiIQBG$b8k5DnaT0*X)}|)z)qKRJ_c_?Z+FR zxo7rWMqf3F0UaO)X0tERd9FNDmO+m8VVfart3FITb4`P?E!494p8LMpD%FQR8%t>L zOu$$@F&Qe<5vFlE<=7cB(0Yx@;LN(2cHT?F$LSpETT3}IghT!628~~5Qo1I0LLBiW zOit-YxOnn6_gxrPI$h{A-mEE|Rxbc1uL;lNVyL05W_qY$zkB)_`+JsbO^MTbQ#cw^ zFHRbflD(@~B)zF)6SiCjpKnFQ0R>8^>MK`IV3mzSNylIx&($2=_TyeM8+Gt ztw_-5l>~YA=LGpf8kB{!yUKKebePAy$0A!5ZNh~*<(j~ZXwL6E^}lp;RPM#zt6Djv zNsoXS?acaBx(LyfZNo;ke?tY;qQIpI%E`cNqn&atv$*udT*O~1d1IH^J8RSXjFJJo zic3t@HA@6lNtZlAjEB;FH3rD=#N`nUv5x&y?{Q(BD;lna(BwY=_#(@_ju{YQ){b&85Owa5FBpP=Z9ryPNAc;*{zs6>-0(Ox*M7*{@QXwoWAaR6H9#*S&c+$=5#b}uHxF^zyrISuetuE1;5kO z*HVKGHn{bLTSrE3ZEQ4eq~r}E*%@qS%xR*ejUs9B!PnUi-omO|5mS0o6gMBp)zaru zQl9yp=8E_KSzQ**Y+o4zzw?KZme5*o=??B@H#8=$#qzFZLlQS9+9Pf3Y+^ixXIvIZ zgzdo7!rtrCq`&$M>Aha~vd@&>>$me;mh@h4V(<0v2pl<6YWYl&w!#<-;p7mfkLKMF zhm&+~h_gxfH`LYx%`!qpWC8W1pws>`TaNG&k*ke16$?L{;@mp~6->5KKUl=3*6edn zbAKObW`ARRSPGdxPlJrxppAW&JOp!kL??2hjB0b$-}@%8=f)B$3>;^5`m>bKzy&*A z>VD}Si~9DSA-YoMP`zHqPSBl5WP@?aS6D)@Fr;;`8+7%tXNTx$8Q_Kv|AYjVUM$Yn zZCMjQHKaM{2srN~)a4YW8x${WRg{NeyMo&{d(gtc$La`sXoMW<=dGbo-%ngW5FgF-F?|Jpf)np}&O|Md_aSfGy_EuipWD6FG7uX9@Rchr$X@66rj=fF-Gy*KHPT zCWF1xbG~%Tylgmg4GT_%Le(9@g|*&25>X33xhoW69R?eJ1Ac=J1Uw0VgjeVP*s5Fk zqz&$+#I4=ju#C;M)VCgI?>gasK=C+Y5^BcZ==0pu6>8^>8qCN2BHsbj+~Y)q;v(E?e> zEk$%$Y%Jrl%Ko=yoh!;3hqCsn3;%9`Gi+#YB+;WdF9X}oaBAK~2h!lw%CJ7uEIJ4a z?(%^J`(grD)4d=`*Od$^q)!VB7N>d2bc4fLce5s!>zm7N=9+l+G?w9YSn%c-I+QnE zdRqS(EFp$l7=(b6+^3n$eOh#j70Ov5$ymxMb`7KM{o|W2HEkwkueo8Sv>qJdUxO8? ztm+m)PWP&NR-Ft9Q*el~7W|2}1RcZ7OWP;nvuggC==0*&L!odeF0(%DJk$BNQz50t zcfj26m2Ti^=hE<-3HVKsQE@B+zezhl2%{sS<@6X~@sZEY)di2P;~fo0hGQAU7i2Kj z!6BkN=;RiLF}$|J(`fyUQ>HF)2@J(N%?aG#`VM%IRo5+~W{-BDqlgv=Es@sGzr#vi{mK zOG7Knaoom+R_uJY(ay?j%Z4s(%+Af!*_YNr>hWv3bC_Rf zVdI+?7~eGPcBh)RHL~r^Sk3f?W>O#+wq1yr-Xtg;O^vX+F~87=B9y)UMiipnxRyz` z2iXQ^Z}r^Jdjs@o(^#FhF+M3&=^3tM3U!@NJXTtRVa=|$Z)VM|g{;}N0RB?aI-Krd ztlywEK0iQavhW@7mIED=SStZeOyy1)tu21~9~=ANsqo70Vyj@jXP9CFg9J1y4Y@$O zL4+N}ydP{YmiiDxR2dH@IRvqg+M_uU&>UE>eZPA5G1@PpG`;ZexT;B8^E=hcK0O!Q z0WDAuds301dR#HVdPJ-qtsrgjqfu0m*qy;Pm+sxyadHNC6>~h=beKkPsGA-m_N;V$ zU=0eA$QBd?Rt7a?pL%cUFrAW;c@7Qzm@NA1TH-xoQFi!^eh?iwoSsX6{WGO|7r(=# zwwD3-!n{jev-dPs_rBMuze5JZd1wQ*&cU0Fu*B^Txu*tI%?fX`u(1+c8g4Pwp9(FAd=_8h@t=AQMf0B=yzk(sf67vx#L>GriR!AQ5{JM)IF%`bwktj62E>s((P1?=ti~Kz z3E*r3$lm2^BOL2?rz_n`zdUn7DfWDE<_uAG;;Ssi+)#zi40IA__D#_>v+I%U?y zMA_Mz;v*VhBD7$W+VK3)7y)Zi`|b$=WxB)ikUvspZ(EHJ>+Dpw{3RmnUP4>>RleCL zIjeq=WvNsTUCDvjeQlMVF;SvnmgxS)XJyqWi`q3~f@s)HvTT+o?Nk^1C5~PKvSg{N zDGM_f;G07oiw^Ddw%4ecoP`ED3j+QkTa{#KSV~|D#*BvReP*3xU=tWrcyk=iXXYZB ziA>fj4$0TvFXOV-@b%bCV{xKGPefM~+>;__?6ygEHqRB?=0rzAm|xEDwG4lv!w_4{ z;`qfZ3>RaJEv8HsVd%eRAxIXpS{|6~q53wi$bpBylMJAUi8)E3&4cGBA|(G_im-ag&9416?o(o_g*49ZOgsMFoLbrLA@PQy5z{~Be!HL%PY zxK2-)&_2VsY?zA$YM)I(4x2LDP)a$nk)zT;Af1NMI{yTOM4mr)kbP5F|9b*ee_f>d z^(brxsz+h}*&?f_lFn7`wg#%L&^@N=uanj9R)5MlY|4#>a_IjdM=9mFNavrd%qZ|r zFHmL|`fn->&X@^{qUxC|D~lF1RTK?q7M;wA3kS;ciiL0Q>;tZXz;sio!(VOiR~yy$ zUYew%l$(l(Xi(pJ3H-#g>`(Gq!7W*Kx0I)?0qw5j|IvO5(m!{hvJc!zP}OSpZzn+B z+=PH_n|_Vfo9rDL?-CNNr(a+mWu+#eR$E?>;!zapPk+Yyn%@2#s;hleqzNhR?Jr0v zE`xD<{H<^zr!;O9K;xB}rvpn)I|~wwmSR_dLmmI}BweL?iI#CAXJNlMt{6a@_)dE2zDu*0ub19n(co^3-Vh$a}=Z_HNk+ zD!uzc&L`(xW@&1L6P>13xQO@j)x+NkQs2J~HyYuIz|TG>{H>?c!+t*0i4ANF710fZ zCY`?#JYDtge~7CLa_YrNu!CFk3>O9q4;l)7R{8Idblm`LJI$|el$Sxl-IU&Qr3c60 z8g@myE4aonUb5ZRl#SQiWwBRT^ z;0T(-lqx}rm86wA=yp+HaD?_(YS1$!v}Zrv%;Cd>W`1g5X3e1psI45h~(g`ohJxJe2PkTas6*J%qW z*otcbWp>WPL|ps3h|b1+8tol;j#Zv%9xXpG;J)VPI69tjO2y2QN@wOgjWbnzJ^L(i zTIY%OeD>SOw1z2pl(X}ES0X`Mm?LNo-I+&)~M&y zsOL2CEKMwx#=@v?=~2&_QO{XX&$g)NjHu_FsOQ|M=lrPWg2*!=j7=H?qosKyExr0} zCfWoaGA{Up@d8-VU5)+p_p_c*sHyc0{Uk8($uS1I~&nS`N#G zjp7PbS)qZOc|=s$$t*HmS0Z0{dwpp0G1=w=cxyQ>3)w?&7~8icuCJ7L%v;ON1O*l` zpni`$Yj7_i42LI1IYh+0SWdkAV0!eOPYiX;%!a3gtT*+84@zUWfnn* zyZye?p+L)%GShm_v{q(X!^N+cnVRq>OE2#*Wx|XbAUUe#R-$oG1bBHmOeZbz= z)%aw(2w34?1hu!98E+w~mA&yX1j}l8hKbMRaC_}L!?4NSzHy#s-nmS3W@pfoyZQ;OC7 ze`Eu>G^Vcyb>I)WNN=CwXM7Z6gi-mft_7Zr*R^Spu4xvPcN`BGh9~l8_+`|3NPU}f zWMKF9M|W>jq`#<6C@%5uSPDvgl1>a_peVzD$?l~TZwJRdBf7YD zKVgn^v1}I9Gv<))IDS#jm?K>*FmF9$4heY$LvE6gw=?8s33(PnZk3QXGGv>CyqY0* zNXSbWa;Jp6j3M8UkfRy0TSESpArDH(3m8(BkbkHH89NXYRFnJFR7TxOPpEaozE zB;*&%0hucyKg3&OzJ$DhA+MH@XK|SY5^^%$5UhcoKV8nz{@9i`*jgmvgIa1CQXMj1 zhn9-lPivX1=}$2wRB8Q;(pf2yqDsw zr9tL-e;N9@NTrD9ld=!w6Nk}^&TM=jCl>(i>c+TZm@FrN6 z9}7IU)+){6+1@WO>oiKdmFh0Xc?(%#(y~Et^Ch_2g1OZkCXFT>4fCc>v#9+9c%FT;>}R^3|n$ zVoJ!rFyz}3@(V7rM?!LajfW-V?OgjY3Au|QKa-GKxXjNbA;k9<@{~ zp+Pr2I=iO-;-qw*U>U%{iqfB0=G^?4-=cn%pF~9Mk}+bIx*AuF5i%ndp?cfs+R;1) z>8mA#lu)~`t=snZA3$*WLlL3Hev`p-o?w3acDYSLIei1JW8E(488|cp>TT4%HUIuf zs9gl!g;-ab#HrUVIw`()Hhsz37%cyU3HygfPimrZ_Xtp(()-B%z6)L0?nZ|uR-W6= z^3-4A5=I{k4snJPsBi}fIz^{?Izypqh`Pe^ty59*t@`>Yg%w^@Uh0$!#E{kO6NN`{CMUh*EbjK^h&=2U`gn+G;ZXU6iYm#VNOT zk6*mQ#oV&9w>3iVE)$o>!i#{ZAKe*urGy8g!TnPd_M7?@Ei;gVx=vuRWuejt_bN07AgD{h>)%y zX^(fGk}kZST>5D+OzbHm;Dd%qzVXSupFH!wLGIo6(=QLrp=+C3S<)&h&=Lu)i zRi@_$08F%AYZ!()DM@lP^# zr`lDh4zXrIdi+MuPrjUce*P+Z*BHw`jWS}(^tHFz$lHfrw# zSQF<@i-JLkw^2sbfR&+nAT<&+@VTIoP+A(?&mnO=_qqcDIf-%xE%;bWL+!+8E(ymN z+Gf(q+b;XDH9AwtKV23jQrP(n zBm)P2!po|%Z-Ir+!$kgFz#jGKNIFw_mrHw?%Re=Kc{*vXJ~+lckZSEqJWasK?0OY@ z$memGe67$)_6gBR6U#)Jfa_r*BKtjIO*ZG+x8ci(GXku4QdwZ!= zoVNGxtm|!7dMjd!GtSUIXnT}Vr!1nE@V3JiQfp8jw??}XX0%DhQKA}x%RL(b7w-m% zXSi<>(iUCD(@a}ul(>c_ZJVJ_u9_do33pVJo9otxmQl#t=Bd-pMp}PNk zF!KCI3zEx?`02zXqHEc`MAhA521!?h$2JIxK>X$5t-``qnxLt(?gdS45;S#1 z!5g}NDSS(c{03K*;C~?vUIZ`_vHRX45U;CF*KXSoHU>@3>~fCyMBolDl=J$U|N zZTpV}8t>BE7%p^n)1TFGz6oQE^?v&l?bd~s%q}W2XTq8bl#;g$$|8%;-c3&?Ur9Gx z=S`^n2NA!7#_trB^DvZMzPnSkTdAjg@pTy1on~pgnkPJ25{-e;xFk8m#K#|+6A0zc zd0_+Nw6(k_uydSmJ!toFs*gpEz85h6!*@7&7kR$rjM!5+lkqmzD)jF^zC`XpVSLC{ zt#O3nn}_UMxglvG$`a1ME4=KKRy5#Lt&P2?q!V2;)J*E*KJuVnz*GDlwsFl2vXyHe ztXz3z_hCgZd{Qj`PHv==``yW-sga}kuCqrGWBr2hoIh$jT-ln7!{cFd*D-_9od3jR zbv_~Gx$8n>*3DhPDdJ?K^c4WV1J;(D&j>?^S>vUyU9@t{LeGe&kw|;a%mc!oP=RBm ze?9B*(}7k+b*=CfINXFIR{DhgzfVLX@`=Dp%~I!IZdTzRnf8@jC}1QDerkcE3Y+&T zJg;|blDoI*?gSLtEDC8qz-tSeS|Ic0yE*d~nfaIWI)~AiwQ(mJ<>Qj*s_A2qo8Xgm zb{Il@vQGZUDIlF*Hb~OR6**6|dAd#3$GScaOzw7R-x5#N@h@L#h0@9ecBc827}(WjNU$%^9AIp zJ*@Cwg*)f`r?n9-$1@MiX@6ZV+1K?Y%g0RoDW&hpk5A41gob`?gus0i`oH(bz~BvS zXF&f)_C-Jlf)D3sipca|!RVSFg<#a!CC6)y4Wk!+G73g7M8W9d%%I;gvrxc^z%=RL zC@{U(cz{M(EQaWR;B-;7t61|tiBY!yP#7BLIjUR=pTB-fUH5yYc&Kht-{-)W}L}dcvUiuk=utUGX)bia!&S_fUd?txv$8uctlm!WP~G z*Fmn;`8-b@?Ey~py})Pm3)oRd;FZ(axtVA9Tz=+{oPzn?q|a~endf)e55s%lEjd?H zqUX2VRGnK)^Sk#cp5Fs=xDPV#WJx=$z%9W~Pj3J*(*2+l$;z_R{`sflN1)LYRuJl+ zLBPKmT0zByMcTQv3zdbYUQ@imH&a=b?Cl$?Qq;~RQe}yuAB9T5b`@^2wpn~Ly>I0@ z1M*-Y6FFlBj;RJsO?0eZs^ z0rVd}Dh^5>Bwf)Q-Tz38=Wa*$_q%AF_93-dT;$STpu)-`YJM_0Oqae`J)R@VG8dEBLaMhmcb}iHO9rj(uKKpu`FifK!L1RKM3rLMh z8;5Y5XAvd>bO+kU%+(9rgnlck+<{H(nD6R_nb>{dOYH7faEYD!7++%T%5-Q?Df8gG zg09BOKEb1%&c2u3U@396^qxWQF!;ke`?tUE6fW8~eh3X1IIZ)wn!^6lJXa{CCMP@O zu{K@>bLnd~v-!#e%9NF0B^wo2ezkFNwb9*?3!RwZ|8W;AV_tHz7d^H%Tm>Gg+oY^E zK#`K5q&v&Z(*B$zv?&V+qOL-18trddayhIWd9^{dKxYZqg+!uf!FwpRRs_8nR)5y{ zz*c4>gRfogsx}7qyYoaJOkQ?3)BsF(hCUQV_u;X}L?6%r>nrT^R`i29Wql=33|Gk6 z+!tz$`r-=i_cgJ05bFqKe(2oeu}NkD^|ri-y3-%l9q_#Xiz)F6H%g!eJlyZzu^foaFn#^@LUoKDkW8fgaaP2x{FF^%?9^-JCCxz zu_j2aXyU64?#)?^Fo&6T8DBksmRyY6&Oe{sujYr{uX?xTOaHpn@5bc65W+O~C2qp@ z8SRLGXdSVL;Mf2Lk~MbelEAskGZC1;guN$R`3m>^gcR+G+O-y#Ai*y%-V?O1@jFDf zcsFTx*=HJbW-(IYaT|U0VL{}wGyAc8W8{m|?A?h{5d-f=E$>cfV?Z~D?r?VP@29YT z&|L;c+s==H>Ma#xv~1~$TF=amcHzew{-;Y_%17G0Oivryb6m#kF4PJTXnb0wUGdew2TY)9(?uN^^GN(c17NKl4XU0fB|LFCwri z9@Yu0`}Z1{)U}Ixjym3OX-@p>ru+`{=$Tu72#Md!P2mzEL0=d-cmL)*e$Z#%I?Gimw$J zyWdNU=So;ut1U!5P55Axo}$Wm*|oJ=u`?%WxjUkR4V&??a<@YrW^?d`z9n)fBb8QaO+~C>}imFz844ZyEZ_-%}D|) zg)oN0C^Ray=f4BIv00|Z5sE?TE5Dj#5F$C|evZLG{!ZX9|3a7{fHi7rPJ;TeOCUB! zf>dvBTgvL?ti0aDzajN^R&i%F5M802mD^c~-PgW~ORSP51O^r5SmB-3JhbmkhRQU% zQQw1B6SYc8>6>pTF9xIE$I6>TF7fYX{(YW*xA5=F{M*65ub<0`rXjO}=5}aQstH@* zLZcn(oM>Qzn(#6wbn(Xye%=<5k=-uHJi8ghgnMJ*Q$%<; z79K^|$uadXq_-=a^ftF2<#anAS`ZD1dc1prA`&=LFrq$$Y2a|UvPO=Gn*b|!8N@na z13z;-+=Pw%%pq|THt{n@#ZB1E&m0&x;dy@M*tiK>_?g4wCcMng93eNMgP-%ce^}+d z7ofZ-jXPf0$j{I7^Co_NnU_0s({#o9Ey=otg05r_O)}kDp&hDY`ZTk;y`1K^lf%;- zNDc#dAUO_y(ILr-%Elhkbuz|!2Y}oR`7{eCd3(&RgZ8GvpJMJ*r;wdwMRCqI` zhYSx0sqk<1Xd?vKq1+ka4&}`VcPKx?utl1*xWV(qv%d+tPQtpS*d@)(lO+N;7P4U& z`!fGRJxT`?l%u@cl5|LIyEqJ~?VNcx|Ms&D&D#+LIe4BdH4ss1=;H9gIec+=;b<3! z7tT?77MkmIv=Vs3z0AM=hj#7B57#u05UE-mx@&+*IyZWA@(TXtGYrJ~Aq5Gpqxb?!pQ1CK+yXoM)? zC$VTuGj&%u#4Y>#(0ws%F*loKI>@it>&7LLsd+3QDSuMp=lH_6+T6xhK5$V=Jd);`4a5S|tD;ZnjD;at%!8+1hY?MlpHI-19UY zUdlW(UF-`S>6Jc)*)rcPrY%MZ-zfbxvuZ3=%k%sz&FWkeJ>{!&%`hcZ%L`PaTJEA$ zwY-E9)$($6u0<_hs?N2k<;&H%l%-0Yo1~Vz)w#)P`3iMzs#?A>)Pp*-s}xyiI2Bth z27P}(>1WjbC|RWM54T*P|Ns5L`=h(h04&s37s5KZywD}mDFz(jam1#m+^tXy5sQo= zLa2Q>mow;}H~ot~U*SyHOn)EzveI-03^`F^NcfCz?5ddn_ugA=24OC4dBb#Pwyn^g zU!_gFYZNJmSDlBr9!7HQvG&?$?@=@wrv>qIbl_S!5%gpmcBrI#gXIM7` z;Ush#d{uA=L;g>&Ul5KxONm!(Wy-G1VC;eS49`LiWmn|DFOUO2hkAAJ6f<2Cl@54< z(A`Gd3pApP+_$pjl9aju{Sg;3oX^#}}A~vfDs|W;e z0D+axn({vcZYWfL0_0GNB3=Mw3H;MIXrkfI?o?Dle5QXX$#hWA6GwXqZ; zI&XuKeR65ND55kQ*&12vD5bZ@Vaba+ensE>-X~Jo!JtR@ z8w-DkhF+L{W5%e{0W)! z6zAM5bKX=C3E$NPk?_5{AQHac;+zt`+IgIFNalR&CgeOUbN+~P9+x?9xGfU6!v$gB zN`A7h9$7^~p>RR@t@}O?`b>@kCWmk{qdtlL3h7b(R{r`ZMjny7etjs1uP{vn-JDx4 z6CMQ#B+}|x=}{(Y;pC)n@kjHGq2yKc*jJBn;@_>K%z<|+h2_q|H2+-F>UjU$EQQ-~ zx*T|`QXcJ)Z;AeTcBjGZ$%Jj3fL3L~2~NllC%`qS^608?LW$8}7}Wu+PIGB}H$bnw zc_hW4UyStg>Q(MK-Ly4zM@_2wC}vZAw4Z<9HVU>`-~;tZ$jF8>l=|nn(Jc^n)+4V{ z53_6^m!JW0`rDj>5_Dur3qPE1cHS$#N3niz>?ZkMf<(yoDx{3QC%4n@gDJj8(ZrzY zdx;x)71d{U`JP18!-<1|cRY^NO7+PBo(_a2ZQNh6q~;tt(Wi)cz<7uvjPLh7iq{C@ z5c9!VyhpQeX#9yDXD6Tz^;lyUt)~ZFO8x6R5bl>4Y(4jmhparhozumT(W3zWzB_bs zTb}jdgNDepz?xHPJv#c1#X$&1H9@IdJNTad9PptrCY|chpzvp>a)bPw9Pn)yIZ{=I zHm91@`hNY{tTMEvlUrZF7-Eu|Bw6NwGi#7^VmOjOOM_MeJot)%Nm4Cc$rC=}#So=S z;S5NU<#3-24_8^w<3qwTRXDP2rQV}9rbD^3=-n3aM15vpZdc~z2nqWXE^|fbnlO*9 z3GU|GV2`9~@@r|X%X>6cE#c0AMO;Edf>B>WgCJ`yYC@ePa4Fryjx31Q%nI!S^%!pY ziW+!ov+Q`n{a+=boNz`!*{t_csH$V7niANQg4t2lN9yCnLQQp#fCk*BefaH|!K_Yv zkCh1=?A`+;@!)&!4h^aVr3dCSo|wZ&-ReXi|5eUk@uO^8;KsCf_*I*J_az4Tg(>p- z?&k9lgEymixJ;b~6@bq^t zF}SyJE4~E|%{ICr5pFHQ@5u6Czx#EOKT&({TQS#yMw6*ln4WpP@oL*0U3;b&4c;!} zvv~6kUFX$O5pk7z?0%W*IY7?@<^>3ob30eVZfdGoP_Q0lh03n5I@(w7)Em+4aG@MCoU^aIRjM z{&OH^>@l^}j*GvhmL}7szXFpk7{QhDROd}g1g|-C&*FFnA1Q6ed9$<#MVefD{Z`>` zfX`>}6|kFIInz1HL#OH13vH5tDTZ%fVT%9^^#nI&lMbLugAX!v%mf|tU_u&u6c4!) zH%lMmM+Os`Z>PV)ojZR4zE!&p?gXD1M{Nw?N7WtJt(OcO3hW9!Q)%riN*~UV z74*!#3xMg-ew4~07C;0I0E~Vu|G5KAL&bu{L(V!ORD^`F8WKE z+sq%-W4@+MIK(>}_?MAj8{?lg;ht4(dxF0VaT=c#Z3FzvXKWkOMr}+dwK2`q#wT^# zHvaA6Uq0R2I{24Q#5O(++n(p&qx_5c7HdXJ{SU;=y;1zvj*VY$jI;m(GyAJ!{mCKv zwz5`(k7Ad_|9dT7I^1O#Yh8C-Vo(BLY}1L^ctF=e_FN!B8$$sl2~KXBC)ph$*GYt}sb=R!Z?WwN^2SDEc zvoyN@o1F>S^mok+ZzmIto{M3hyzmrw9uuqMpg;CK;qVtSaAj{r@KD8Q`o2Spo}Z4D z-xxh#oBwhq{mQodsWRKEVsR#r=fyvZD3=RPD28}JYpD)p30O?R2hw>;E@?u z9bhY38T$sNqzPBjh)`r1xRmi|7v8bzCg{G>;fov@TuKw#D^yz12duzCWlBDd?3^Yr zYAT`d!aah#BJ$Ej>w}W1TTgy`O~;+tHf1Xj_MBE@?c=|gX>3VDSpE`_N=6o_?_PaX zg5hDF67X}XjYZ7`4!h>KI29pls*SpNe>`PVn}*nd8gxdrd6b~E9u^7Oi7a;3BjOfo z@8`2qp?G1w@&xC6J6`16Nja6aK)lHLI?6Q-$eed^PIIEjxe@$|HcO(&`54MI@ou2E zhAebP3wP(YsHjzwjhz4W1e!zMPRc&(54RTa_sWVf{xb_h-m8-Q3>Q>BAt!;(Pk%dr z0t2~!IPqSrSyLG({#FSFEhPt|J}gJw@5ES+%1ZVxiG+azIYTh`IdXzN9 z^*Vd4Lx#l&V9kUo&7EB9McN{-P~eV;X{*$;%H~HG_LxQ#8 zl;G>GE1-m8>S_EKAJFmcmv0w7prQ1=`F2)co{x~someym&w1WX3qKIf;0ILMB+kmT zx}U=><(p;xP9*R#hQJRK_(bH#?b@o@i3X%B9TO9Ro|EKrACCeITB*oeS#zCx=qgq( z{l+O1Vi}8Z4-bs{zRzQ!9UGV?1$1_)~1X>65YD4rKrY#O1Cfed! zXM#q5AN;!ajSh~l{8f&%W2a|#2LicM?%@l(Z(!6W+PqxTuynK^++X*G94cbSnFBP` zMln>J+I~NtOGUdZv_sR!^(>fDUG|ao@1Oa9hh*VtT7)98(dzM)5Z1{xE7M?9qC9k5 zFs=zXAw#mrkSd~-IFv@YK|KkyIcp~J#+-<#U_I6bBaA~M{5i^ne~xp;03nPK}Gy64)939(kfIN(~#m%N)(zy zYGb;1O4h!9X{v!mplLz``d`XX8^tDqD0TcmnbhycI65Mc!Y;O#6+x3UnhEbw3$9jB zBgb|i;vtJ}xP;A%ly#?#y2T#lKC+eG#Mv(hR2ZBL7!X2#kQI8tEDX150i}L+O@Mj4n~LfD1(e$B(alTDRbzX4_)=ul%^#cJ!n0fAvSP zz85P;7>7h+5VHW{nseQO+jWDe&VPLZgD6E@6 zM$KC-gx;}^lHmn?%`#d@0u$1*VN`hF^i0Ij*h?FIrM4F@7JgZwb>rnYy)-y5kJ?N0 zZhdpRuZ3UnMQi7z{>(1#Cqd7-@}6xZnptt>!J>iir>MGCyBD=3RVQ(MSY_I`D8;2a zNYc)ueWy0?a-x;F63Q+Oyx6M2f?h!v^6*FPOW{s0)h-CXEY~dImntpzDZQA&E&lV< z(8qG^5WOf!HtO#V+mBS5vUw*-nsib54JW5k`E9 zTEv;#WahmhGlb%zLN1|<`153TNWboonV%7vVKItm3V)~-ELa{o!M{35?XFG=>{NRK zyAO7&-GQC-OK1-Uf#Fbo%J%Lf(+O!&CA0t7eP~*@X01O&D=~O!kgA((Z%iwAnCJ<@ z=1DTv-;Th}!Gt7Gqg~L8Jh+pq-gWTM!5va@gK{0*#k{Q*YPWn^$zKunVL9Vh0%*kTOt=dtv2 za$>~&_5Ls6CYY2Mx(WXBOSm$n$}7`rFm-B6msh4=UxrI$p1ed}$3^qQMepK-f^fnz zt}9#CbvKnJLd~V$zrKXmrb1a;A-xu=O(hs67f$8c%EPr;zcl6!DleZFKMxAkJp5I5 zE-RaS9dB)^vc+4zh8thH%$bX~);yWBh;!!4oDF#6ks#;yzZy9VGs=ZVQO!B^K`iB* zB{Ju=Uxk@1M!@mhzdH5`t<g33ERg5WHPB$59RO!sj})53 z1{i!fu=CXb{UG`r_>X4Y+sTXG+PEtLno)&J9W{0)i0dEw-4h27Lqkfac;!lAo&V?< z)>ITv%0xGDQxs31?%A+V5xyDC()Ix zBaQ3GN({>dS>KYEID{oi8_yq;f%*+rm@Bg+CYR%?!duy3Lf4-&L}1nF7${<^3d#=? zSIr^HS=@(MZw2fbEeSLsV`~{AHABp=O<+H>lrCqh;%p66u}?G{+RfrMix-0M0z?0)iyHad=gPWIrg+2MJ=q zh}^A05L2M;WFd@;C$mI+*Pi)Zis4Lpm4hXYjKC`eiBf*71*m-?Tux+VOPU;HD^?jf z`P+eNSeX)cgJiWSBT>riQvQ zJo=L_1OAFVCe$+R@|?!Wl%XB&Oc5b@8NCPoC^|cJP!$O%GkmFh9@4=R*#GX(;5#R` z&8;!>5>2CVsqlg)%FnHtO2gF4rw?B;ru-j;M0RdXB4-d#$p|M$me2U2@)=w{9)~0i zH?2_F#s2I$nlJmb=V13s%v$rCDTG0Q&&B)|ED;k}ASTcyPR9t*U4B3(eF~)7MFIQs zOJ(qtaNw$B`diC}DC-#wdFG0vZGbu^fRQ22wq8CvjC%Dlbn_~82QRK#T3jMl@^Iqd zJA(n*#M70>S{P%~Qmqx?IK%Wnf%fprav7Egjw6kwI>?u|y<$Ae1xZpRND{XoNmdAw zWThZUJVcUE?d9kQJRW-F;8w|XS1Z@d(g+%Fm89{+poipQbnsJt$SGn0Rw8->Fmh)I zi0F+nO(|LhZjA6V+7mwCHSPTfdyNBsH$~cOjOEAwY6ec=0m)%5mrQCfUskuET(2$FyO5YqQ9lgPhYQb3Q_E zm3>WiUC=Q$+!f**LBC2wd$g-_^LSQF>fC&OFspM5U2H!M0SNO8{4X7_>Ue;wBr1Q{7?T)PK33PO_G|Sh7nG~n4uNCtc;}ln>hvi*i zZog$*X;;58Jz$J~F5SkJCPg&T?#0NipXSk+i+VOF)j6Yr6n>M?w2odfNec(SRR z%9uPC>a6JJ519;l;O4zr%KN5h@tJ^k*;fR-i_Z$)4U=R5)QBFqv#S~QeGEtZ3(M=Y z&DZ|lIv<^Pob`OTHwu6|--`tJuMe9D$wWZ>i1K+b7yxg z-Z$}Q(wcN-VZO_~xmxE8RUHRs$WmRU$K<{qE!A&B=^o`r+~s*$rT*V@@T2OW5Z1eN zIO(tRlETjsGOAIpIGFMZMR5Dqh`^z>(r}sJUbvM+oG(ODb}4&_vITzt=M8jYb`iG8 zqaz9D4c-OOCrJ$tA2{N!Y(Ag6X9!%J&{k9YiU?ei{@+?RxQOmOQFK zNT}WJ&}{tW7-l1bW+USpJR85GZC4SV4$NVZJ3o|v0q3`ba)jqUw*G+bZ$$geHrm>eHrm>jQnLDkfD2&J0&9M#^>zXW&H zB!YpoQEhGqf1Id-QK}Jdw@sS3{d{veP$9(CP z2FGa;dD0{DWJKh_)yKz|cs*x!M9!RuoVoPe+~nflLhb|em3q~7(A;6Q9QM?Vj%#u6 zMXZvV15v9a62D?A7l+8sSYCU|tc}mnr#Z>Y60|n7Bj1??uMzShV~C}1SNMAO&NUowY-g2?-|jd%=@I4Ffxr?dp!vweX%uB)=f{MqV5`Oe#r<+2c)oYWEN6f@v-q5n#g}<@rF|UPnd3Xx zq|r(8T*k*^`fe9}KQv`>zpqX7J$3ii!!x3F~sc#csDS5LAhCJPm#daeM#CuI$ zXi_Jr3(fvD0A4_$zg22C9k5#u?oxLg+&Zv5d}b5Y!QI0$4x=Up_a4Rt@?3YRUGDTd zudoGnxC3@pCmvTa0o7r(9cV~vx|c-^Hg_r&7bu9XbR7GXQ4m%VBOom&D;xrnm9&%Z zgu2D`^P*hX4_sw)XV~1fJJAcZ9Y{`#%L<2SWSwRUmz;=_?LdG~x;AhL?Jt=zBYvKq zs~x$)=gF41M{tf?dKN3IBIc30Lzf(HV87?u+ksCI;5Xzm-L+iYM3?|l)^Ai2zNa06BbbRvc@1KTEpzt9vgYl~ULrgpB(;A$HT#cHAwrEpiJa9K zjdbf{z2CqN8hUz&4==7)=^+7ZYDfU<;IC=YgF%U4)P~ek_6gE@xgf2p1ZnLSr1c6x zTCWtOwMUx=ZS%F_Zf=qHTo#-`Yvpa)>6r)Up`RYyZ~rL6CZCl(^Z-2I+u(rLGkV)D z-cFbpy4%Ft(m4Ku8JyL_p76jw6Gwm6}qJx93aSxg)U`7 zS4|Rh?3E&Wp?@vZx>(U3A+w5v*8Zfcb+378ja_{W4$w`!e$F90)X$Qz?b^}n&Zw^6 z;5ac^T=NLiuRSAOm#oto2cW#46jDm9^_3$m?z~iZ#oa4nROb<<5v)Dd$7nG~?w+^> zgOj)P_DN#`zO$ulxWr^pCis!Svd9hy zfk}?Iyyzwe`~q*h7aNSa!wPV7#=S2R1wZVQmy&AJ`^M)V1Em(C!Ds*B~GQ z59LdK0=?#Cs`YwiOtdcJ##eIMsPGAITJ@Sq)%=a+=$`>P}dBrlM79tFhesbf>&28H*%8l zBgxNvWHdkXSP5ank=q$}oy$Y0@TBT!%`+{k@9to~Z!O3y0#5IBr=atOOG#yfe{sBz zweJZrW45HlJn{eXbx9>Fdp#4i2adp3texm@O*}-`2gxw3*jz=L@gKq9m0hDG2EyOh zMMLBH`@$6h{=PDHna2A2QjUiGeOV8DKAyj?oe1}DpE_>;w#5G2aoN(d9GW*UtO09c!I=!K!}86_;7}3EIA6ZnT1_Tp~2m{wvi3WuLB! z_JzuIHabU`gRTAS(ox9lIA1QG&lChT*GBu4ue5Yze&<&Zo6*%A!Zr7hxO9H&(ot92 z^qa7q+-Im`mR<*Ba=v!3paov8^jMU=a71H1viD!K=axij?JM1xJA7tezR8zQzZPG< z&AT(`bJ>0QNxuAKpDWdu@9^cP`CRF~{0v`ymd};#%g^!U=lWcEzWjV&eu2;B^5vKK z^2>d$rM~>-zWgem%k9fw;mcp?YX(m9H8+HL-qPTNj+hP1m&5&YB=bP#$;RNC8HleK z`p%w*g8RL_#`u8qEpp0{`YF7Zd$^IrzFrFVvo-=gb2%$T=0J zKF*7OYi=xK|I6$WP071D+oi4KLi}DbC#OhTj`t>hFUTeW+T6H~D zO!;K?-tK?SjQ>PWzurX2%D=jg&5Zw7=KjVmA5kWBO?J=?I@JdoUZOaxt$)Cw_Y{X> zTI$H~_T)e^u&(X_lfl~X5ZHd7=q87r>(P{QYmTy0*@~z$-uEV;wBb*Ox<}$0>`Cwy zQIi4(9?C_^KKjb`bAK9A_I00%f7o(CL$?PiI|x!^bN-8~%RAfZ_C{2YADu%jMxdxAWHj$h(=i4P*0o0w{M`p(FWFAQz>dZo&C)Zp-t4KMbkE8pu_&*)e zEgsCu4LJ?=FV}q2+h_E4nVg^39#yDmh2}${`G8He0tHP5=S=A9Hm%W%0K7ekXLrmPE4osjh?p{<+Jx_GHv$7nIBR`V1=x3n5?Y{M&2q2 z8l*gm8ILbM&?qh0+S)4sk~$0a)xHHmd=<{CUsTo_AX zK9s6WXJ=R*_#ClQlfN-4yY?SKwecfLw|P2MP8G#(rFws3+<(YyV^I*@Mf1IH8J%~S zcnmEFf}8mvPt9BteRbp$?412TzIr^(CCj|ql6yw&FEc#L+m-I_zbC|RW_*+9Fu|&i zsx`yIJ*Lsx`Uq1@@KIAXJayPCr*|DkwYwRv-%^#FN0oiE8CgZFn62H~%=oQz;`Mvq zr^}AMlyA!E{v^J^G>O1>Q#SprnW=R9)8lC9d&SVri{YuOSX+XHfDS@in%2jB^e?q` ztr(4Yv_b^tpT^E^@-oVlY`O}m-Tz34e@);#nxG^`1=Q`Dimfq7vgJaWqd!m&AK}V9=hVqLD<)ZU{cT3=jHtK4F9 zl@)69X$58O_inSyxfZIH*48yZIViALxvN&`L?5j$eh`0`Exwj!WG8Lq-x_IQjKuH% zTl`xi*|O7!jfW+&X1-FfN?DW)?+n?DA1se5H)PYXc3{nPjQ4_xNt6R< z5`igA&OZ@;rLsDp?X_Eh5;6}Egz}O9?qsFlCx+RrqqeC;?6m2MvI!TA#M5S^PHr*o zA0qgf=NkxwJ+u!~8NpD0jI>ZQgMbQ6{hFII56Jo17e38+QS?z=51Y&l{hkXYtzem~ z{E;@l-}IFbLc`fWSbmME_D!z;+RQGZXD&m3w}-K*vYpSrFE^;31e(BPj?~y!JAktz z707OyKz0vfWi0^|DtV?x-M5qlW?COa32yn4AU_tEx#t`fmy(~m z*9Ro-x99px9<}@8*uSYiFE`PbRj*9q>uiy!LeD1rwRu0EP^O&v!4#Ym{=DxeIxTBG z=ejqC8u|U}YzF4Hf4?v9M;xK0Qjep|_WOjNs0u|bL$GPCCRtSDrE2;^)vOfN&>=tw zYfU!%T&+_qYa4geUgDg*dL~t`lsqoz9{s%kjL!S7)J*4~)*O_~QM*TwDz|R6NkZk; zNT;Yr?u^!F!up`4q0-iR0NC3>I3(TL`dB<2KZu_zXkfyB$#a}U&)UW3vUnB*B@unj z2c4R*%A3KcrqI726)aExBA~B4ZQ#L~Krc9<=)Wv$1AJ!6W6oi1JxMo2q9d~m*y73c zo=Zecp;~S-Yd;RxU~TwCoZeWW|9X+-7PIz)2f@f-GP|Ks)R;;KeKLsyfdMpTzSlepm8fLO#{1E*J^yRu`NOfOVG$HeFz=ej5$2 zF6LZo!^60}&O060qb{%n_E5&SGi1!=j5cIU<&5*}XUI8|b2^YSOXN&DLrzfOgnIo8 zKlApwyjxw)?benBwBiN_g!=Muq8N#DpVj{ZL9N@>T!*(SRi#Dm11CS%LJ;uV*QxVN zG+6FDK_ak=+6!I11B2Y2XOg#@ww2wvulA3F2L@H=KD1%=&k!|d^U@wMS{pdR-~yAf zl3?9<(#o;8)$1LEBUGBF>(kWh(+3lB0by!%4>h_+G^!60%;D_7Uqqw1Vzdbl+%)`+ z!2#;Q;5);Oc!(Q(=g56LSZ{YcJ%3jY{74lZc-$f_AZ{ck+53D`?b^XX3;^AC?i~!Q zUe3%&gc95}`{CG!6OdO?4xdgD#BkjMbXS?BOevQ^%R?;>7YLst!}{`Th1HFDj8 z1j%6#g~^bqpYcT_4W9kPK|Q62c+6EtKJ3a{p8$shHu~~}m zW`l1rZI0hloz82m{ud?T?eKmYXKmp4&Rq3$@%?+=JnIbdY*x$yG{P zldBCy+Ly$ah{6%~4-5_t;WKy8sehns%~ae8Gc6DC8C3hPLIJ0}+B0z8d;p$T1e~nC zGLyC8X0h^<6!3b`@!#QO6i} zgLESpuPIhK>*zHcPnysi5tJbHs4xMED6im})bRK0iE~5|kYHW3z)jA-)*b=PN!8VM zVWz1gX~t#NhCeW!KoBnkOwPTSW`+gMmP{Jgno9-!ilLY~Xv*qxjE0(ta6Yc}eM}3g z&*=QLc8DkMO8#s%)XaisGG)>iPuyM*y}S_NDo(kjR=KXwZ+}q9YonQSX-%m-KSwHX z*&020cZiQlU#NV90Sdcrxt_FA?if=li_S`L5N%DpDI$=(;o!MS?>kZ*{J zGXCNWnwv}??`!^^8vXqde4pI?UINXe%jdI;5%#+@2 zNpmuBTcgw4W^KKhdaxL}9m$e@eomaCQ zJ`Cx?4s|j8->xpR`RG=z`{jQKv}CZK&*b#*ogQ?$KPMQ^|1;7y8#eI-0$1xr9}{$@ ztCVJFIrLvx8;7Y7AGl)qW}-Iph}s0HS>VVQ1u3mSIVb*Omol$}@F0;MX&jp%zj;A! z^Mbir<8L{BV4iP*$v0Ig_7rJ3$(I;>3$p1-4wz~u0AZv%I6X&CA3*voCjSCLX{jy@ zM^iV=Yf|T%^|VC1E;A!!!hB0aq8alx z*P_n1>6RmjcwJ^gK!y4Ch(wx8JK`(MPl`ySc}(Kj@aREq5e3_BZCOlL9Gpt2AmB3* zM$OvNO($99GVIav@Reh+a@2cxr2gn><`ye8W+DnQ4>)_un6?B3*cv~9S)k=OAgK{b zT=nF~h(o%-tS?}{HQ@6^e zbQauag!2SuR1se0m1k9!#tUgZ;sU5$R~{hPaOf2xC0YNaixRer;brj42wt`gv@lrJ z9(n-KX^=Yxgom2&c&#jE5X7gmz~S)R=4{~=<++t;QG#n6b#c$Z9->C6EGnkwouO_X z3O&6oXwuAq8K5%-rJ7Fjzg;W?bXeU(v$IRxA<53YwXH;TzB8OO7`XDl@VUP=4o@X& zRh5J=<#X<}Hatiex#57PirO5~9lPw}N7gJD(v=xP>K$pidIuCBgc1X5w(nqMV`CTz zx6x@l89P->!4CSngYQATd5-A@YfCBU6pv%6e~tMM@NMAjO$OJ^xyy6Cx^N`0OI;{# zLEQms7?@+gVO8$OY7c{?CIR&u{qus(9@J!^nnL%fo#WPYNz9s@fAZw13sX^zjjLH; z|AKn3pJMfh$U9V(g{mB)sc=swVvZizU!QsVTzm~P;TrY^qI3`va_(Dw7iC#W z*DZh6I$Cqdp6k>Fxc2k}rlM8zxUD{L`Pde*pn7o&dujMQdLA~@U1&I-EO2`rFycp^`lw3f`1tK|#0RJr<8`&2_iK)bqEl#{ixHWa#od2qA0RV6} zq}$=YYOixHPOi1P3qpkB_kYENn zbgO{~ZWR#<-X6QN8<#&EdIb3_Ym4y|i$|2dFB|QDuR7>{V2rT8{%DDnB}&SvOhhnQ zPDfo1Vk0NjX8<*qz=00EFUbtN-&;C=o@X6USNIEc_( zX6XJ~Lj2QMBuqsr@k~ehRyeXLZ;qfN&pl7*z0J??I^mcwOAvFPK>652L&Xg@OSwK- zS(XuhGXA7uNrcO?*^k5)%q-4{Kd2n{4o!H~D9&8)g$jrQC&H7Z6V$Ma`kmiknpQKp z4-WhcNA87E!9ovR9M0vwxN+9`;*~`nBSel=iHMY4X!18WR!o4k?tBy7C0xor>To*h z&~5{vKVK9T9w+HA@UQ3WC|PZE!Q$6R*)sRfDf19XnQm?U09bN<4u>1MA?IOJ+$_^M znI~yao@Z#zb6f-w@384%dipISNq=hyG%s~rXl?jDC|%QE{Pr$ot+=fBJL$rk+;44I z0&ioh=Nhby*Q0K_d)D2LH;zgF0G?(Wc^1KK^EfirEb`xJ_P_3^qJAnt$^2529c9WM zZOsb78*6yEDuE8P>5W}A;8bTAnH^wrQWdp(h9}{#wFS`-=+?y2ojG7CG(hW>ZYD&c z5v8|$O$v1ek-3$5@S3)ibs{Gr_%7f#d_8mD*Ibpkp9nRA$5JKP7QZ!qr|$x#+~m8} zq?DU|x0;o5i|^EI0<({R!`47Q0iVE!XPJp#;Vc7N!_1MDs{U<=>sA?^FVPQtz4@#16=A@ zN^mbR)ArWxpGr577Dttp+5BPO&Gp-sTgRw(N&hYEUya3JHR7da zYN^u=^I@41m8lr63|oZ%G8b*Ej3QcUzx@!~$57^SYoX51q|OsevomQ3wmGZ863(`^ zz6nz#%MyQyKbbw8L@Fpint6`$F>UjQEz=shtql*s&0wt~pN8wmxoD2fgFAqug`T1> zlVyCJL*an+71Ncj;xc!a-VXxTa_#X=p9FE+wKpEXuCcZ{fHQ)l@0F!xgdw1@9lBb& z&B`*9uZ8I-CEp^i8;T2#t@zg2!iSto+2a!z18w`y*fm-MlamMS=?+(f z--;HUXiJs4+)x6gTgrY$)WcKO@f8}Mh&uDHG9pO$r^?OdYD0B`-~O1I_o&HldNi=r zFPvNy(QLpML~;>SPYJkzo+73Q4S{Z*c7)z|SRFQ`3Om(waI0_{LeFOT>}BUQ`KqwmWG!7b1!iqtaTkI%`6_7zTub?7pOIfaCIw*g=7CQA z3UnO0!VbQ9Qw=UTj9N@S4`aWOJ{9o#XgTy53;JDP#)8=;mv2kKXJGr)p6C74ym~4$ zT9~g?&}fJf`E2{C6!|Ra%ljdVe3n$gzaUmwq>eygF)UfMOA)nnp@%M@E^X^%+Rz#u z@ z#KzPX|1B7dR`o&{H(#Q)brHK=b!nE#;ER}*#7KsdAP_#H>XoU!1s4C@ncXAt-l5Y< zzi&aU^~LV)58_r)CNv}cRDfyp$3r};9!U=d$N^6U>d#PgILKD@^44c_pB^^H&@366^Up=NwS$}7X zE%U(OQr4HT$iS6@PC_L-sW7cclNx2Wc$AW)?D!rfHwOUEqZxqI zc$-BEjssExK($E;0FFzj*B?2+RmOeJe`XKik20eDD*zCqFR}*%RSx?AE%QNy(>XSn zurYa#P4}>qaR2l{72)j$1KN%YQw@X27s`R}=7UEtB4NoQ>Ltu+VKon`--75GBeYz- zN6iDZ4#!}!!@c4zef56lX+qpI5z9j>Mr zWKf$1!U^A@1cjZw!+bJ{-mI^LT)p2xuZ6J8zfn)R*8}Ym#)l z{uzC%@6ApS4(%!rn9l5vPj`QF^+8jFKb~~FwGGLB9~`8&ro%F4D(93w>9sh{se8mf z!8wo1obThURl-9H;4K7;cQ~gGirt)32gO~S(;x@Bk#m~lKp*3rW;xK8Ij3Fb{5t1M zk~y;vMgrmLp-3Rm_)j+BfHr%os?FLSD=aB-DmU)%OyR&8!Tr7_1p1IcB+C5!*K-x^ zMeE;35CrloN|VM~FB0XI)vzGX9Nb^&<1ZCG>|veuP~~U^QTX@iuIm6u4N5Z%2TGMK z0C%;)a|Li`GviLb<@;b@nwr%izl>=E&1c~PC|9tBEyFh5sX?%x3ivI}!t~Q;gYwl4 z&<~?4CA;AXEddd4ryeLM;<+inM7CMH}Ga-GKZ zTwGXlJS{v4(Rzgnk~AzPLdg{JF+&vfb}jh|wq~m9#P%lW0jpTocQ?0=@bqKx9chkG z8G&iAyF)BE?e3GKx>V&tm%eWjT>_jjc$zISegQ@UnN39*J}U zu6(UJSWJnFJtEX|$c(Ew^l`sSnZJr5lCX^`mZo~2Up~no62fMG2^n-tl)?72?@|UO zMR-*xq|;V;d;{NI_J%F?j8PVQ3BqCzi1wo+BOq8NWw2GZ9fHiT^|mh3#Pkoy>0fz* zr~jgm&nSN3_9Y&r#AcEK{IGY3{wZ8&^1xp zK_Y?T5GI9;mm6lM$SCx}ca(`Pkm2LW*N#(0+c$w$AfeL3z~|W4{d3wekDdv2t!&XA zcAeB9YIeP9dEj1~_Vd34gYM;PGkE4fglU_Or;N3fh7*@mO>Igs%%L?|^A&eF7twwY zGqbEvdv)eaVbV>{H;bItD&?ld%5499h?LXTSgP5VW}4RCm|{?F0`uIlxUfijm*#@? zpSD?gO=)Z)k$GU*@KiVI=f2!beF3|-ZjPbmGGXPtPt*Q+#=g%$erfNm6s7u`fIVCs ze~&e`X0X1)_w!w)?xB_XHAQ<2xXU(tc?f(QUCkKbpktnnJVl5#JKJ``6Dn=oJjM>+ zZs4BgLS>B0W*h{50I=~;QR2o`i+`vUw@-*-w3PR$ zky^)6=cm0wDq}=BYs2F>M+zJ+|6HrmUw_DoPzVV(dUqb5H+w71K#8Zu+4G>iOiTG> z7DVyOS%$FGQ9k@lmQ<9i#%brGo4z^vra$4+SwKn4X~Sf8(VB3rnWC?kzz%iS!S}d$52bW5^GZ1Bdyl)ljh=hhMDmbiGztz`26GRE4S5nPn;7NYS9b~? z9YSD?uYI_Bkj|s+f-9HA*_bPrEbZ3qs`P!V3TLy)%)^U?>DVcS_V>h)(W6~nbyfQ?7D9o$2cu#Md#?GhIEieR`V z|2MH2c!tj2gfvRuA)RMhXgOqwX$a?m2M%38A9C%TrCaY8>vN&|_!dLWHHGIUazbo@ zDrS_XGr+2tlZvH~OwR>!Ii!XmF57j4+D((QVw1qONnqP0;B!OkKmb?=dD0ny{l5|j z<4jst%j=L3c1g&m(sPM~yn~+M&^b$6l{V8b=H%r~R$CqC7w+77t4T#2x!VK|jM#8F za>M1y4VR~Vz9NOLvS|f;8kTE|l4csX#(eECe>wV&{Bo84#i8xwFK_E#F4DhbXj}Np zkp9J(G;@@%ML~+}C`}3b<~nKyC-DiodTQs~lL9?WmwU?^S;L#tXBvv>sie?7Vrvvm zbF^R4X{wCS#Q^*$_=MS~O%IMB=%1@k99m!}YtJ4^!O^x{9%S*_L+_@57+6a85NA)H zPRwLK{L0e#nxK4BiRpqYfX7JbWyp4I?^uMeXK;#}eA|wE2eZbUIZOG>3A=$s+HxA$4%R@ciizjBFXF}_ zU-Sr7avgWc=*&BGsgq-AKv%0;&QI8rRkEu(E=t$eKf95aF5d$}q$B(xg%Q8wjJSyX z;4zgA$R6A+V9Ss{3(PNw`L1esP1+L!_wPOUcZq?bvHyyY>suL}XADjXp9Cx9*j5VH zHk887Z{=Z}97af~VcZ`XQNS&6#0rTc;Kg^TSRT6jT~&9NlPcZi2<(*?I)&iXF@u3p zt^~1LYD4IUJ3CH~mx*&hnJ?qR9)?3+0mWe9@8# z?P$)nffWOI`QSyWWjgp}w?V3)B9uXx@DRvH&>jO*%EPX5Y+OpZII{B#sDJh>8KrH; zt9Ap6x1`b;e&+AY3<5Vvb0(iNx(9F;^toMdEIem?skXTNgd# z=nr|~0XqOYVK<@u9@_Xjpe_2UBxu8Ur(bS{52n{2;?1zL`#s(a?}aymg*QVrl%VAD zkt^}P*$(-k?NGp9UCKFfX+AV&X~HqqkN|c81N*Q5wu>G$X3Ww!2MoGCNPxMs^evs; zwSZpH1duYM{}$BK;6R`s7YpLUkj@eSDWS(~@mQ_{Q^YxZ4Bp-7-iKqB?&8m|>{(DB zbcI2~>^xSFo>bq5{D=X_8K=9#MOC3P0F0O~F=9akM)ZW%?FljT8s!veH9#{uYx1=Rd9g5iu`DAcJN{i zhHgMex7J$l%xoZwy4FT7m4RHd(kst5Y6*uIHI>SzU4^59*=;i3Fw2LID2-rs3f^Nu zq2PoL^iuDW6qhM8IOiJk_gR_D4jvDTpRKK{q2xMQ>YlJZP1YMCTAa1v7W#Iw34B(Y zwQ(+s%d}4|cI==R`MLu2>r`K)$GmuuA);I4O=hPZ;e8 z30Po(+jTBj;Px?$Fs$k`?@Kk7;m3b}Lq7|{kFY~gc}WlL)wGCwjo72o@W`qFqtYU) zLafxMhUkvPv2h$68z)tdjguBJl16aM59%h-ZvcwZl*dX0LtS_~jBx0m4jS(b`I5mN zR`pc|$g)#1pi_8g4O83Me7G9qJ;W733F4F~hpK4d0?enSR^tm5*fA_^$gh0wvft%(R0}|Badg>ysI1rPlEQ|@nF!~$u4jooC@)Q z_TaIRUzE&33VpJk-vvV5LdHY)uMY2QeC@?0Kx^J&qcj+uad3R=&k9V!aelC zrPYpUjfL?)deCH$L-*VpA&WOh%i`CalVZ@}#Nv3NEFqLB#Ph*ZYA71vfLPEbp(^D; z3>DL`DNyAApB?zYQV9Fj{t=cDX$C3Jw*`kH_#?u<;DK@d3+CCR|HJ!Y_!pdgD8m1t zZ+!oUu|IdHcb?h5Ap4I4@!=f_dXn^W65{n>e;} z5spIm`}uIrFOP4VXt1`jAs>fHC{{H(BI-|5vSQc#IT{CKFEeExXx#59e+H8>d!9+T z5?TNU@)(g4o=6Jp;u(o>(O|5Z)p)>T@?{Rk`+i_j>J4vAH2C7Y^(Oio^gW8#;W*`; z#*@llYBqamVbtU{epow4sYj2T+Um=_T|uYj<5%U+t5ap4JpE?Bso$5Oth9Ky8htBG z&PS1>cF6mo5nj2(fLgAG?~y^ZVlW_m3kl7LH>{TtYW%&0l_T64KQr2@(B}F>K%O&GZX+GF$LCnK<;Tk_slO%8E4qX}4 zG8lkKit)&UFr?IUi{42hv6>R!#AFTzocmU@UlySS7PvW7(*fN6=iA>Ya3pI<2WJ_a zeNexsWmi=PU=h69EL5)w#d$th;z*-(SX$>F+{v3D>3}#IMb8RCvZ8z_rffU>X^=37 z_WIx~;i@HXHB3wLutSH<&b~FGThvmaXlG!`$`u*50S}$~M|g_04lv(R(d;C zs%YNNf~ym}dxFD>&VR0nQ~s%HJp2G{WC4zI=oEHH@PbwlAzQJsGj}xZUvr_-#9J-f zS?fqw0>br4m)W_q_Na5GwQ(ah&yG2*$YX7M80fSKyTsZ3pxwu*zE<8Pt=J{QE1XUE z>#1@!aVo#9PV_a2_C`xqe@#R1ar>EFG^9D4ZOvut6#H8Mks=zfchfttny;jrWvra7 z_8q02(FM2cRIQEM_+&%xXgl>a>6=;Mr1*VGH!Z--;Ow1Bx3llT%fz^yePFCx2AHw# z>*V%2x$B)gpnK(j?sWDj;_DX->79XCLz=1B`@*9s=`%$S<-Nur1jdaA1b?3)D(9ud zk?yl^&)n}T*>3W3Zlls9#*Od(rQIXIgex3*58fB{m(dA4)EKljus`qVgtrU4O~6V< zUmHHT{EL!(cbm1xH=YEsMHC-ZqE><*r&iiU;N%RoGRe2lB78wh7txDMN-_7F+Vf5K zJ-~ii)~qA!0i&T6!e~h1m6k1^!Y9qcv>ah2AQ2Y#-Q~93qw5{?e~;CpDa#h4%tZLH zN;$u3MYc`&k@6_2GL?weTuWIItoev;$aBLTR#I8SnkBa8l9!4`v5rM2}9VL$UY!#;g0(4OD4 zm3?$IJdAI-Cb$aoOjHcsUSnfd&Bt(KK=ja_;C}0iy+oU?QYI3alatHN3xj3QWulXL zDfJnbii8a`aOJpfO}biY&fKAvn$#3E;R#AqOYLeY-4rc-3&b4WZ^8t^tL8f8d_IcH zOv(hD&_GRaZlTUk8kmA}oH0>`s;LoiZH+aTH|`h=coM0Mad;vx0vl{LALlb_y4NcX z=hyIhcSkKFjuJ(=+~_*aIhadGhN8uGtJ(vEJ*WPnhQlVCjzFCzVCyb zF9D(P^yhphNS?F6vF2R&^3dAJTFLe(R2R*}9XjxpdCN=LaLVQ-+RfSsvm*k2ZR38T zIJ``8^b3lk-^Z`YPNF!xg5uat6vuW!adb1qp-77310#XH_6UNfH+JdTachcSh1P2a zoY*OA;KIn3=r8=tSCV}~smSo(7xdL8jnD^QLH23y0}+wg}JVKcXv5&lV$SlS1LmIyaRJEvMvKpTl4Tx!Bbekf5BHt|Ebny{H4mZ}NQ^TTpA zVGBQ0sR=LhgIi7L;D;5oXV_9lWsl2h8?*e|8EXo%+NikfV88+!`6ehfrHC#V+k^Xi zn?Obzdz-*IG^z`1g9#n9E<>g))F!>&ACEn~AUW`kIE?8v8K(U22nD-%Z2ASM5nof) z1xXQKlVG#|j&2>3pjJ3IN&s5h9NpT+|6{H7ElA7#{zO~IP|9U4W19gGzI{zVskn^v zCK@Z2=sL?C8CQR?eanwFjk0fP;)J1aLJcPz4kuJ?Vv7__nqlC(zg3#JXnmU^4K{B5 z8SG7TGmPP9qijmX+LL_nT%vNIzByV%h5t;Mr&%>ev zU4Rxq{&V3j6spIl3jvOMSte%V_Ff*AF-H7_P(0Z}$hpoOMi%dZR`fc+y$$Uix{)8B zgZuwVSnwp{7)w;zz@|{@AQtm?qLXFSM!3x4AR2ty8?AbfSn6zDr^Yw=bGRzeS%dbs zYEvjSF9N@S2i3<=$G^}GROwcxxcHLl?W&^F&{3Tm?2lX@h^Kk!si1mHZNrT8CGN(F z*oOJY>{8pn{)BMbOsXSZF+$|q>KxODEz32q{qx`5$#19q9Ezm9A zeGTi#b|%c!XHDq6Rs%H+N_KZr@dgJbgsQvq{CQ&(%UGV?mOp+30mNFU3dRn!2m(%cx6rgz@;!!V?Oq>2;Q%9j zBjZ$ZpWitmV%*#z16DF0M^7w~kt^8>MNhm=s5Xd*PkQ24glU)v{G=zIFSsxw_LEF} z;9MnB2>bLvVV|ASf;c2M(Uf?1z`06iS&Cu=#Aro59BCu09Y7wWnIUMrVv)UYTVBtM7?uO+d{_kwr zq7{1I>aOuHSHSJWt)3(s<7QQMx2pdBI_X0z%qEImD` zAx@>(?EPcfY2tQTxs_?kV^Qt6stp{XotXy438EQcUsDEio&TbX|7W2-P#qm08~nIL z5gRDDzuK(q)ou)o1j7tA@6mid;z|(baAyBV5R^N-wiYjD7IX4%MuPsks^Glhqd!A8 zyKY%FAEg$h{U{F;zh#TrM5S(}k@cOkAZ$vfQTt{B_5y)e8-8UF!aU_1sAEZacUQH6 zb(!E$G(95BwJW6gBm<~{Gr`fAa%42766uu(iU|c=N3IA-5-4z#3w=_l#wP-&hV%tf zg}^}Pr6jBE$Ay?NRc${Zo*cuey1Yk5FBW+a=ktUVrn41;+CE|oVN=Gkd$jyu3c-bU za0Hg!qh(M!VcD4i(cGZMs}po2o1;z;P!jl6xhtPh>_y>F#)M(F);;~B9(cbt0Y%>40=bf~A>Y`UpbNV$GVt4cZEc+&HY%&s|2 zhtG$BVb^A(C+*>;=g&15=qbhVIU$FA&HS#N`V%!UU3o~DL+%UqS5)>?2em_#T-no+ zn-kr?=27qcqu!IQiKf5D(7V5S5(r+WVs}45Zy_qS^NG<0*kj9ovN4hhUa=9BF3N+_ zt=Krae8=+W{&Z{}^?rQRd-k=_?{%Zzw~u zBP}<&{K`@99i!fJ=SIED_+Rghw|_W#EY^P|!Tvb*Pt8&1zj^;`un24x5n;}-ruXw(BFTa zRewP)WCrJSLM3R;^-54_g5r7R<;!Bi=Y}K!pA)MDeCB+2T=-lV`*Vl2`E1|=r)eL#QwN0&b?2*L zgXMD5aKdZl8?Ug1z*hu+$gKi?uf~MmrD6D8sA@P&H{d7?!sNHujY~vnX|O*G&6lXN z(Et?id*H0#m%212{3cEp@H=|HfZwS*$A#ZJu|Ib#@|_L*+-`!9x4TO7b};Dd!pIJ1 z!K)h|xYu^%wOdjQY;YG&+Du8&<9+Jv_WzRD`+u4k@8Mdx|HmKi2i0fT{|}%2{(tT4 z_Ww6y@Bi~qKaC_d2HQvrnWL=!W_2%#(9| z&xq+(X8z@uW3GfnTXnC7fyt^iDETt^TJ@Wh?>OA^rPF;PU5r8%7H|-} zWNYqgQVgsBn?Pj0J9#{0yR`TkVY^gH?N9t$3X6hSJ93J(COblIgPg33!DjQ^!Gtr%Po3F?VjAz98`c? zuk}?_E&*OWbon?h%M{B|D;7~lja(jSo{tY2tt7?9(&2*!<>@7M}6Sw&6G?X|O6@>W1Asl)-Q zfj!Q{Wh@v(z7m!{r)mFwYa}@ARN62M)_`FLMYAD*HWj|(all2XMGRW+FQTg!vCZRm zE=w_#g3s zWPS)Gju%nX+S)|F#igK^<;nERn4n!8#vV70k0*2wH%n)q4i2X%mH7l1SJz0;RqTFZ zqt2gt(Z&2JXnAlyYU?YOEUUaV5=%?^`mE-o%N27zemPmp$IB0k`FL{qxbty;?9Ux9 zuK(Ygk8^(=dp?q0`~Px2c1O%dSLdktn7QaI=i{lfnvaY{vFZOwVm_X4k@SD%`1F6p z8R-AEv${{yZSiG^G}k>BlQ4PZRh=-IT79KE_nUKYS+1hJ4KBF6Kb+Ngb8m?`-Y-uS zq=;Bi-M4f9sK0r zj(6yKl4NLB9zJIrFY1y{5fi@JSG2@>PWpZz}<<<`*$hL+$Tog-hYS*q=N8`PA9$7tu?g+?=o3!kx9S+hzC5&x*6O_h;ehauZPB01C-G zFz0C?4aBywwmMnb5LDI*{PaXD{3PR_>u0F7;a|wx3eOSRvn7-HuKX^N+L5%F=KPKO zCJS|ETg^;mVZL%pzN>Jtd*!qG3A#F;!;?DTfG|HzyJfu{{`@@`h4b1%c?+GCTA<}Ux;OS25h zH5rXp+u*sb%lgXpLidK-Gw5norJYWfPEoW{){uFAe~Lk`F8A4(Tl+gN>GZ@mGEdIA z3ie;t)}I@sy`klI@@_K8tzFq@(}sy?0277t)ebR^CnAI!c=xB_{;r}1-sEL2>s(VB zU0;rSGwOSa>T3*ozE*dg!SiJ`VI9VC)U0g7y}i$HFJ^g_NtSYN&ZH8jZj9`~3(=>H zyw8t2-vc`RGk?r{XMp}Oi1}`4qxt3&3DbJs;&EsFL`m#fziQ+7vtH`ncwHkMh1w<5 z8TaeY3X-GknV1v*`ipX=Y9{pZ`D`5R9vJp^#e{i;NiCJ#yqR!cS(?&aFZ_VPJcd zi6;Wr+y4s}OoSexNX<2mYkLic>Duaz08GUw$PCh<;tH8T!9G~;58YF=+h5?L3x^h~ zmf!>y$G7JaWlDc-w#|E=1DZ%$Jb{3xW1nUxTu`|LwuqI=F8n=m57AYj`}SGklc9Mr z@yU+QMtFVIJ|XbQt3~6ktRKhz-0{dy&+2{$YqPJiO=p3_w8jH@RL-w?3kE&qh}&Fk z@b*-R9lT3hO{YK0%F}dy&;k?Dy&%bc^Va9VQa#^dnXl$uWxBF<^K{DiSTc;8jV>%N z+{n1m+Q$%OKH^3u@5g5azm0Qa!q0bFz|Zqz0l)hS$A#ZFV}I`0_w4@y{JsPDCI4F9 z5pGmGw5)oo+}g7O|NSHe{&NqAk6V8d;~G(h^K&F67z{&fV-&>X{q?NiwK6v*yl(we zz-z?@0k0ceX{*}U69-176WLggGfKG;YzWYcVI@enN`*}kLB zi`UKyzP9UP!uQlE0pF^20pBCHj0@kLu|IcI{`_p<3(~SwQOo@IR=LwR@xr>}shFT# z`Ab2u!v?@1a$fgTOh_*HWe7pK?IP#-PsJn^l10uWsXEC9pu5y~wr z*L2qS?F3pTRmM?7cbdR+xCW4nuh7m|ZdS5p8!oJ0SN0oa6THXVi7+JJ? zpN!epCx4-Dp!{%vkmPDY%Xr3c5G0rD6;6Wj%qdpNz(1j`ZK)tZ|v(7;$6E@TPnkjTNiDCcpt0{)et~mtbHM7?7ZRY$*nzC2_T6Xj&36^Pf#ct6%HSAu46k51``AsdtXLKWv55Uj^W9u9+F!i)Xx3t0 zcU`=i7$(pDhwJqtJotqtcvZBw}(RW6opAKc5~5z+aXwc#MqVV!Jd?Qx{~RyZ8=UlRS79Odpe zgHlSgD_c`I8=ZL$=YMEf78tD!Pl80CtJfJE{U&=JaK81Ry;QsEr?X(foQAsLR(W_i zJNivtEpCBhY&TP$W!va9;pmq-wdPV8FWS-zZ_VuPx0f9^hu!@q)xPKa4}6~YJ;Na5 zk-^V1bvs-8jh}t~rqzkzW|T>H$u8DBKm5xS!!r#)7uyS|Huq+NLCn*Is z+S}}nA6i?#gKBRvEk-!AA}xPDu1QL!z-W$M;TAPc$7~Mm2VbVLw1sHkrLID)gBB;O zfqP9?;;s#^D+p*V%NaE-jVH*4v%xs~Ew5&DiG~*alPCpEsWFI_@??p4y`?;D3R;?v zmU7Y3Euy7@=$3LqEv*qPVYQ{Y3c+c5eQS!L6W+LV`hB|c5VWYF$Lh_=FnBQWsnsU; zQ3X($aUrt{=%l?qGAG(sOc}7T%i=hdY-k6UGcw11d=;Wu%2NVm=Iv0fFx7t!>yFxH zx>+P)DoS~#K=I9Dl7j7b8_QrtO3MdVQoFp7?$hdiN#LP2G}#7dVqQ5|Y1WoMvP~t_ z@v)K&TK#3bE9|rjY8f4`yo}MYCxQ*MYBUF0(0h{)pN+gf^{O%SUJ>-)j*qasfA_{K zqD+C59>L5cgw9M)!G#uVY7l;Z%o~?3{7vl79Y5GI9$iR-$sHLx7+3LP;~19NMRp->q>Y+S8(HhX61=T?na2q41)Hb4DKEw>L~P%t2Jq25dIB zsf0^^>yen#v-TtS)P18vYTseL1M&Sf&^&?YGhus#ySwvNmEkhXN5`)$$Nk6K4j zpez>y2Z%adTm%fsxMOjtt4PbbkybYyxM3!X_7ZI`8dWZV)osvzdkOF6RNBpM-pxOy zgi6GDjf?}cbc}v@(!A07_0qwSk;Bcn;dp|ui=MU)%dsMO=uG*mqcXo7omuQRaQer_ zYw>pSs`KOu9{1r4oyTno+o*eNN~@4?ZP%WCbutI;A?&xEzHG)|lYm6ciF0XArK~tKCUQNmQ(q`|lmc>c?{S=4dSD(>>|1%c}!$ zuqqBcx?RNYl=?{EC$Je$HJX#!>q4rh?YYtv67BK7BeR3yU{;tP?ThB;ox39EXZKx^ z^V4}(Sn|mR~U{_!;2aF$TU0 zgO;HTs7W`Z?S!zL=nzD5v= zS_B=2ywUKnz7h?f8@|o)L0LzDL6Y=slhhW#hid~T)dt=M@=qK~(RvJThNN@Z{KSt!=;025p5yPE6qH0FHHkA_Fb zy6AU>ONnF5aK~8tuljDGb^_*5#$98-pJdZOBRFSlik)!e>J4;@Fbb8|;355~+8ErA zW(EB}dtCZomk*|XIi`GY;$gw>P5;FeHeX0S`0wm-sr$EMf9_cP`uJR5*4H1YKME(% ztg_$0PO~K;xc4hW)^D%x-hMtIjgKaz=G-i1-&W{=stHw=iC+0xiN5@luCd zdl+>zM|y;n7Q#y%*<8b@;79n7C(`moT7kCU%yp!StPYViP1PNuvYRAM%Tg0;FA|Dc z@8~jc$P4K+oxP+dJn!aDyTvEzPMSSo=Q*+m6UT z`ohb9CYy!koRBYreiy({dN9Edt|*m#y}c*Rsnhrg{@>EMTspgU!c&Hf3z21Q_*|2a46X z95_dpSYEHrOLsjZye*UfgSF28lk$QP&(-j2Q$bDh}a$?paCVg5mMsd4jI-! zIY&)c<)vftF~$RsPb|oHMO=Mc=-Ym7vsyM+Z5Y@=J=)=K5pLO+I#y911Gy%~DYZ0? zK(3kn=pPJ)eW7bJ!i!Vet0?JZk+hPMmLeLF?omF^;i06efD95Nyvi%tqKkz*#zMbD z56KSjVE+859z2<*+9d7*k^F-^qN=Sez?9B}d^|sb$yWG|21epeLyUQ_=#JMT#%oY% z%+&UA(J`R5A{sUKiH2UtAm)fYJbb^1GUpm=?a@RV`7Tzqw+Y04aRhEf98jCUJx5P4c|-Y9NPFx^<|f9f>Zre zvDb4gg@gvyGl*hA9T9Zgkx*ZWQz`=La%t7cjbEgc<-;}qTSli?LoKhv?e_vYCOtOi z0*hy|ILjKdPKC_UqT?aQlxG%mI9d4q_R+Nh3=k>Mv@|bu*oA3Mfg^`LW@+U$CIt92 zdtA6z7roj=$Eembf3g8_RYg-rD`TMF2R zbk~sl{D+OraqtVf)!5Kg^Dm_j5Upem6N`mCMG+vZ%VXuwMh4;a3_{)We~k`8xqY^4 zmjq`3h#Nvcj7t$foYcn^;9nFV<7^U;5wcGae^?8EL7v$qCa*8?ZK~79KCSxLPZo1w zTj3nb*9wkAvr%WGv+l5PiPa{U=in$cbBI{MTyJhVpHrssFZe>juUN!-%pMz`I}}fu zJE6x@&b6jQ##4T2d8~NKA$|A=_4nHsk8+P!x_HzrcRnX*;e;&M4hpAd;J=jM#&<&A z;qvjV%_jL{J4@KJo?SA?E?SJ&*7GGV&BCv3(`4q)MCR!-^RbfA_dlU8LU+Ilx)=YJ zb+_i+7gKYfa&3r>|2YsvEup6AbfoT+M{3DcI8y({M`}5qANgzKAsTWd4=H(}{}EhC zl;fJ^#0-PFz!blf8LM%@DZ24J(O6{9}F$&!dwS-pj0y0Ff%Km_ArG6iS|(P~g{c@*xiv zYT0E(xUBeGL@$70W?d(y(cqcV%gb4M7gLWhGfcQlBitbNgCyTUOpNEr4Tc6Gjt?tE zMR0tCdL$cW3U6B>KAdiD2^O5q$Sg0RgxhEM9|LRPiq#F)(DHfA`nY$d?hXyZTKoYH zhg-+;!vz19hgW88{DDybNrC%L2>tYhRqGIo>cQl?PON;oA>9P&&71$2j+e%3A={l& z*LlA9b~);am<8)2GeVOvY64zcASU3noPc2&J_3SFxAt41V)mu)$J`PH4Z7;Z1TjTM z^_XA)%h~BR3LfzPSF=MWfwApuX6Mf{#O#=P%)i&f?3fLVpY#93_<4t~Rp4jS%Y>g9 z8;Q^pjGx(0p^x!(vpEVsw}$a^Gb2wH17=?eYSaP=b_aBQ!;orY3 z8ijw~S~Lp(zPu0jrN=`f|$n*2K;>X%ORisGlOnD4cWa^41e=IF*IZS z?I(D$c4?|P#o%=sYOaiqX)F7}#LIU?>$XtqKNGFHP255sPdu-=oY8HGoqk)Lu32x7 z3c4<+y#8?Hh#2_)gg7nvHpQw7tyoD!Ru>j&duT4`jz))S;~x5y&5-a|n)&blRrua} zK_tF^>+j>@dyEeF9`;GmczP%TSZ|L4*2a?|V7-dF%l{pKsZNSSwa*_8p<4b2d>hy! z6h=}k-|}^xS53pk?0rrw{#41 zJB)u(D-bS67s@rINck2knQ9rPD17|P;D*v@-XJ4jrZ4V`G$7*C_;s)8z4{OY3OuZ8=}~;*GL@#XuJLwSnBJVh(v*81 zM2I*cve$n1Z@KkqzA0<#Z4#nL#U?~t-cH7$vhY<@SWKX+P+Ju$JY!6!L^Vdue!i$i z$Y1UpSN_6YQegjK9V36qgZ;-&Y$rs^;wxgiAY!t9326+AZrXK?tIC<<%4Bk5f9^Q) z=P!r{g;pB7YFBxA%^T|kImCOls|JEWfw5ws{_~|Wf-U}!LjD38apa_;!>OE?s1&zB)1}$q!%JR9)zI=;c>4LXa z#YpSE#F579zSLa@cNY8PY3nj+&s(MGhBW9E@ZU8q{D&m`mrou8|F6UURc|Ex3trUm z2EhE)i^iRWsj)wIT=dSFW&!$QG44ZUZVqhmk#?15mVqr~e64@kfu*6u3H4a}5SLDD5SxkG;vr-3%hb`=-#vY>3DGd@Y~i{`}-ZzspwS z?aBp{Gg(V}o0c31ZCa4&uIeBX3q|zGHwqFAnIE#+u-V#*K%2#-(eY3{&u4ho)Dpg6 z=g<`Fo8r?8v6EvYonl-DFx<%32Jqn(iG$Gc`E!as>|8Zmgfou_LvMZ=Y-s z0ikTxmJ-;WEm@|tI2@F*QUbSrTY9d+FzUS4gJmXYk3da&Z^ec#S|TOLJ<-6$^F(UBl3^7RY#*hmm&jc=C& zuj-`OKNA1XU#K7dhq%Ru8oehmbfI<|uEa3??I6IkJHE}^+llR+=D*qKya~ahY-^@M zD}R^vhmje=8i79FWLix0+Q>bGbBay9rmwI{^6X&?YmHLgyH3r}wKxSlPY5`}9^Kyx z4p?E-Eq*W3+rWJ;66xy&ZTt$QyPL)H&f8*QpW`ca>|@(v6dZoR+aG>F-rBWwx5d1x zzY;1rv6sskhUdC#MPtiui)pv$4L2r>^m1Fq;i=-?DBcZj_p75A29slV{|c&6=M-?w z%r5XuaU1G%hQX~p5!?e^jZW$xjk|yCa{q2L#n`{jqv8Ggpncr^yFB*ijvYhe@89gB z5&QQhQ`G(y{l974{-?|S55>pm|M2l}|KB`s-2QKj{kg;U-uV3=IUdn}B|fVE9h+~8 z(myL^f?ngmq|TnQ+NlvCUUNj~GJmHYjOdZ;;#npMMJn2F`?PCq?D^!f!8qCm+l;uS zd+4{w--OcyXZCJFW;k6Dmz|?@utVl>imx37eC-bY?c(2UMzLy;lgZ=Lu#T&7X!~v) z39^TkrUpv!j>LH;($Y-uju>kuu%rBnp3JUjea6f#@2kihXI#n&77q_RbgHx8q12q@YXj(6TCc@@FgzPipIAS1uF-@gxt*EHl zSX`>jzh8gVK3GMM+(hM)iXPUK$BV&JA!yQ_lcp8!&ud?FE_oqyjdYwdSRU6z(U%NtR!)YrngY9Euu;yHM+H3kSeqa>A z>pTuy-L;0A#7ctVtvwtd*=>6?!v@vL$2Y(iZOwV5Zd<6_bw)9K%1GBing?6$1=Y4v zcYg>#GmPwCCy5ZZh+?92dpibr_-RTr9+XG(r_pbby;RwfOlw)GOQPTUZlspFg8MP0 zQTmdODUlR|QlC2NJ#Ex`@~C$->e8~7Pd1=Gef7LL40@irYrc=zHZ)B&E9*Mbr^p+b z#bZ+Xh9@dbsLlFfpVAuH_axC>m(qm3!*69LdWFC^T!5x;maL5HiKiOA+=PMLN4H$~8QMa`{)dyoqLfba^gCwTObWnxooX6KYrg zcYx=YKYpu1Oj*>wx@&4U7STCGMg(ELLjOzD-hXB!*mK736SUsYce;9+6<H@6pSxk+BWAKAJOM z{cfsHd{Ow>rM=9V3G?|F7zA{ja=}9gDr#;#N%scYKc7#jrTw*C$&kC}-Oo;8f2j?> z-!@qSVX5drh(S1R`KM)hP(S>R-)Wr6PK~|Mvj` zL8*W3=83u@Z(kzQ8vAZ0d~&UUZmExq)}>XZx-{Dl>Eikb;0MZXs%G0p?^BU0S!lpb zE!37S6)Uf@hh}Jzc&Ut7G~QZ=$zc6qS9ATpEMv8`>HDjUoz!E1cFjcX=n}4?NBi3n zUVZFN8?8x2a6UhPLc;q0Stf~**EoNywX@GvKg zgcI)LuC$$&%{?r7_L*$&0j}OKQB*%A6XqnQi6ZNpnQRm_QRJMP1;za=ne$oBnJaT% zif>I-GUr-+Yh5XGPQJ~|Yj|rKu!x*PT&HFcInU)fkII~; zD;Y}1W!A;~+WL{qyMXhaka+`Fpr<1;>rTA2o|ZXZzk=JJB63b;fLf+V`y6f`2o*UW zL%FtOnX`fGOqDsm&viOvPB-UFlR3Y61@|;vW_^xc!86q6ahVKRCX35t$ueK!+OuVm zB+i*5b57!%xiaVJ4CKs{IgjD3HDBf&!dqK`%(5D7$*gyAR=3Q$JOi*@A#V*2z4mhN)6N>2g&1yv+H}>#?+&r;41qgntw##*y)EV^yrJ+uapRpn8c<<{xT8Vh>y&1r6k*)nt0mDV+EW=v}5&jb8LWuAw zyoK=KAiY&j^}hfT*`4t^hi98OmDT*3swhv*OiIwEfJ(<#_seJ{+v6XtU>$w=dMie{t zAHCRNQS7U-m`5*W;bMoQi~T-StX~v6X%WR%>BatRL9zbmVs)WnuZv=D%3`&8v34%@ zdUUZHWHFO_izwDCi`D7HZs%fKqKlm?iupsQ*tc%t(6(}(f6FI;ev18!&kA{pX@BJt z;Hb>$IEsbDHE~@gvS@ljqM7 z&Y2~1KEOG1WzIdEvr6Wyag6DChP< zM>)3_I?8{+IrXFbNv=~r$`!6tKgw5gPW>pqn{(s&5#zQ=V|$(+w}PPfci!#P*ToZMdPDw*@k+|La%=S`e* zqs-~&oSS6M%ekMMWzLH^XM;Qu&*PlW%be%Ckh58y-M4ej^)lypgkF&zq$PPV7J;-jwcbyHCJuQ<2SX*g}(^&+O#zMJx^`Qr?-Rj zy##+nxYyR@cxD?#wF!7PfXGR(mu$)wZ+UDs*_0#R@}==(hB_0LUuq>3o3qtQv;P5; z=%CUkIw(2^HE7Wx(LH)k65Sev@gsL-kekaFPiblsy3`2{`5@bXdb=!Y?s4x{i}#pO z%^gv<(GYtA`wn&wLbD2k`{oIHOycA{oSZ7gY(72+E1;d~&Vd3)27k#tD88Ji`T5{G zTa1H&!2k`QM7@Z9%Xv&o)g=(mFISh~GGC=GfvDN7E`fQ@3Uvwm0IpP*Krh^*E=d)9 zV!DDWO*ma+8A0Vju}c_18LSOg;<9~)yfqW0_2Y0{<_@VEFrOAk5m>|zL>LWOXcdYT zn41(@Ub!Zkfm|Ceyxe4&46|LDfLyy=Z~3XZ7Gr32iM@Gc7XgCzB|yPjl7LYu_uEBchp9_UVl!bK^Nz$y z7F&?kGVeI7X5OAy&%7J4qIu(0(4v-&*1h^RbB}FG{VL|#dlLH~j?h`W@_TPRP8($N zftYRFLD`HE*~0w8aDDDr^-1>=%wXDo#)W-#*h2X7vs?W}z0STS%_8`}%&>>)*y2$< z`wpwnT8ZM>_Z&IJJbPM$Y%1kme>)tSnU~HoD6@$W%skon;hHPgHJ+@sI5St9m12uO z=U>!xP+4eFT)^YD!XoXfyQS)y=Nz~c|4;2cIy5q21(MpMOdx^{vOB!!@RIOAfa)>= z2=ly{vy4;f>5oDFVffXlEH9#Wx;BGwsy4EJ5lYDH%5M7V6r@;x*rn`qX?IX!1V3|Z z{rj}{Pp43o%09uw*8WPbP6o3lS=mn5LxB2|DNefpb)t{I79*;$_RG_tK=Ek(VUP9{ zzX{g4^@rV!UCpiNjps(?{8do4a+y5Wc{@G4D88=n#fxQ3*@hZ>!w!$l-9R@zI81WL11!69HoG$p{5$gUmy1iPWFtYS&Mr%Wlc<@YPNj2RB zTt$V-UajKmr-Bt*EjZ0>)v4Nb|4K2qs^i3}IJ5(DqoBvG$B2RN>3Mn8alw8@HG73D zPW#G|k>HsRf}a{94+2=`p56LAN8#F)$KaU372LnYh<18IK!ei)hkbCP8UnBy{Qg$@_fiES3}2s?L!7SDr?f_QsAGurEyJJs<>d*7e<$Vomtkz>=uw zt;>O6Z3*+rCB6`1Bf}U>gnvqXiIDl6r<@}=-qrDRROa(hc?SF=>N84|wJLD=u;>bP!`7Agp|5!{Lk$)EFFO&>^k4>=5wb$~PVSa0mkhCfJ zraO0}*LPdKKgZ^7A((4_m>W~8VQA4TO1A!RtM!$wx-1Fx-Kp0n##6jlS;1ztWkM{y zm}Y(Zo;V?*tyC%yC#$0fak3V>H@vPR%da0k6%?HIJ9dPnJV%5r6U%zM+w3gvf!dy{ z5V-0$RBK;TzntbH=|)}PhQ03XXqW++H2aN86|(L9+Jn-5j-9fBau;RwmRIGo|+9-PGPlW6-ng>FZ(1R|8ko!8c zP*%*kJO8*io>qNd({2eo?Ui{_YPD?1D2Y?7`5f65J)fFu^n8Bj(0@Olhj#tHJD=5t zh#7TYCJncp&okQm2G8i+*S>g0e;zfXPyDaV==PnM(f_w*v)V9bJ}>*$`O)+Fe(ss( z6Ok(GkHW=9V;A&pJr~qv8%CW`Rn+U_)SHuZp#PrFsHPG+qt>2@xY7aS$v9)4(ZIo} zR2uBprN4KwbM4%}6XtA6U`g~du~!Tog9RtLpl`}e>n z!QpuC_F#1?s4bgj(ozh%NSSElBv-W=@&zx1s`cA2enHUGA4Nz`*7^Wv%~11=Fn`Qe zkYUrF%ZX)g;ZA--U+eF@%ZxpVXRVF@PBchCm(mJ`CEUmO8F zZHKi+;l+Yd%5m?Z=XCak0oOyKlI2>*>;!|V1(rHivEE~n^;J>uXLmZGM*YdpN-8h*uS39A;y@$lr~&o{eG1Vrt_IWe^Z&kJ?=dt+?q zYb&IwbE)^5mDE6d&0KeR8QpaTus$}-i3#Ve3&Zw_ZA@1@6qsx{1?sDTUg^vgp7rs%k*+rka|=Tr9e!dL);tF2v<(34c#jn^s9KS3?AsYn{&Ja!7lA zM9fZ=>;(s}6b&JDd7fPC75%ryF!neMV(PouG2#FCf{-tR@6v{GXHhD=V_%yc6S`G; zcF!H*Wj}!Wr(}nhWVOCUA~q5**pY9<@`F2Fd2AzlE5|0z1tx*Ue<4P3M8yHyz#eFn z+!fv-YYktOO!BIvm>RWjW)l)T{5VzrmF^k(X540`MqSz}(VzvRxWiPp8oGhuKJ}yD zi@y=0-_n`}oM3A!Y;D58+=fD_qENMf4m~Z$*|miBE4MtwkWt0B0 zQWY+U_{Bpnp@8M9=w*xkQcEu{ix=VUO52-wGKl%`TpA8s>aI3|Oqm8>^z@%eUWG8x z)>n2FYCq1NY*5-*p@VlZEky5DbCGt>l+S|^_==F&O5|*c&IyT8KIIIdwfZwq2v$iU z6l!y_p~;iEqfom%dyGceGsmB!Q|ahR98BdS~^clNO_#U%MAtIsU-r8^* zc&VL`U_p5kf2|ESQ(EZ62D6nmaY-Q`uPzvwXdX?tVQ{bw1*=8D8c8~B=uP{Cjp680 zcR@r1g$>_mWQ@O&{;rv)qhwDi&@l_!Dzc*Xw}rPN?MQrdxidmEMblq}u#lPn)3He4 zR6cTxlvCPY4Z$E$GyO&wiGh*`G$l`rvNz`7GfYPWnojDai2`H^q}wv1P^aiRBg&Y(SA`hAn+kP4mt)ChUk!x-WCZpzb6vW)fiU+Ywr$%dwW+%`4_e=HYV#r-F2BngvH}_Ith6XWhZ$mizUWh$t66C zO8qO%?zexG0TaQa8Jv5PpxLsMYG%0Ct>G{G#g|dXqKwt-_c$!2M6RX=`zze`N7$I; z87@%inGtIFJY~vCd2K4@n^S(Ue@Xo|Ktu--n8j`_k3%R9{fQt+_ty@2f0dC@=e#?K zCipZ%&ATV)e9PAkRT*Eqn}1jE?@Ipl@b4=At>s_nbm00HG76v``(8k0qDtpmXZjkj z#QutOr786cWmCBA~0l~R-1R>!Xm^y}~B zPwma@EzsiJMMrd73l>2edw^?OXM{COfPRf?+j@TaE5Ecq!PW@4pwhyIrXif@R_7O& zX$#OQdv6j)gW3+-g{${W)}BZip*0UJ(PG$k_*p}obfhk#BMlK9X^!YfOGHQ3 zaf>VX*Ui6G{JVjFpTKWBw3qstbY8P|%hnls-|avT>j1D-va*4 z=U*4M`fu*vMy>^FBLfL&c{Aq#cJj4D4TNq0L_ZYM8lQ??B|Mg_!-uvO{%z*0oA?>p zAiYh{?H~m7JSRa3L~V!i2j5v-+Eus2v(c1YH>2{kmvDdHW~c`kA3$a?%KZhMpBn`K zNns72GJg}4pU7>Cc;t+M@kQ61J(?oT$ zCjCCV&?K2pxu{9-p7ON(3la=5sxwDaX9-ouUQVeKW=Wvs(z8X9lzH@w&L{hupd<$J z&85!tw>rd2a-mvjQugxgywa=))hD`_Y#sFKYvg z%@NC>YO*w+wJKm&csnskbf>_LPRk{HEyBvv`DW2i+`yC-aM&gvSzy}d| z!{4`9!rxz}=Xkzt(w)V}dT!!y^?Xe^hpsJH6=fnwV~7`5B;nu^hdEQBJ#-M}Ci&Zt zzle@x?ozl!k}MJ2@8dFK;xiw)>Eoz)6r(;F+h+PtvZsau&OElA%M?Wl0BE zw9S`{`B;k<-6v$i{R#9LX3tbOfj$l=9^jUhP9{C%wM2OueX8%nulAQKVBwsk_l<*z z$zEOQ?M&u}FY6X*TxXoBp(fD_Yzgf#szb=z>pDN;1$>SGgQSM%eQnx!9;n6(iPDZW zc_5)BYPGhEqd8=4tcwtoc)ovD_Z6}9Aq_Zf5Y|)G3ADP;u4v2XqNZTKw-cvD&~uA> z^PA8Cz*^IW9WhyI=Var_sf0Z_zgGN?HC0v2u^II5uASHgQupGp*zPKA+ zLNux;!lRkth{;d9{>l(P@f1^t{ct%t-`v<0xY@$fE731sn+5vKh(NztA@rNAG8m++ zAXja2i?=MH4Y)$QW%IcqwEl37&y!qIC|{*nTWcmUKDH=R{dqd#r|1n9BSVeg#7y(8@IJ2A0DN3J3Ap59XYysx%$q z8Is8&p`W2cA161@m4LoI68ezmG|72l=m$25E(rlo9=*1R*X5WCdX>VVs#kIGVgY7yrq>^W>+#t;yWgAEty$~eqm>!FG)Q$#wlt;{ zJS-^@<_}T9BDIU@k&xKRFXBK3cDe==l0-ncEVZlf;2}T39zEJobZ{qkV3%acAdE#C z^L2zTdv#|B@CS8}tD@GygtT3u#SHdnvo5q*8EDY~;ei4s0We3-kShF*=ju-J^Bf*l z9~9p3h0~QMZ7d$!;BXtIXN;Pe)kb#LL<3q0!+C!b+eVgZgZn1Z#BLp9O+E>-j zf{j73tZ8fO#tPUF*4E(wT|lD0M7f;5Xw${;KB)D4eU?ss*}^+eeEWs`7TSTf*gG(Z z>P-sOE23XV@4%#h*bXswpy5jqT@d;BIbsK<8l!d~yb^?VV45+!1OIc?nW_ryKv%>L zED7yENKIX#9k?{K150>U@$L&i>leIwgb&yxx+G&K&<>*%he&}eLEm^)l$<7#-xf<) zMplp^7dUb3p0q*TjK^@MJ;}RKY)pMmHU(H|0`ts6?h5P(>=YYN?K*f!-7#v<$y^6_ zF*nt%cFU&~{B@=0Qlb{p6`QhMc_>w=6BjtrHSgS!U>`Syf5XX=Ne)?<+#P}pdFmWy zm8XwlmGAz#V3qgN-!D2gK|6dP;sjg;b#lsubo!{RZR5Yn9>fif z*OC!KVv)tUi(VZjy#ygHYr|uh_zB)FE6h8rbb{QMaHDtU@p-eiIv1KeKXCRuXe-o` zFPRl;V0gK6f^VT|b)0WP=8iuA^M8EEx~}_Ub@KK?&ElYp3oV3eGrQ)@TyvQo-y1d| z_N~+fpS_!3O17GOCEX^>=7icI9B|Ga1c?95ED<1nPv62+bW(d}YKq}iJoU|o(<+1S zW?CqR83F-jdk#$$zTkcAMF8*t>k^RhEiy+#Ws!giU@`&m;58FfuH0>L4g)S`2^VXF zmr;U$Zfuyi0h7g=c~%5W3Z30&gvrRPD3~PsZykr#&$x#n3kaAdvm^H)Ty?M(ZSImQK>a0EY?9N$vn=$>%IgpYw_-cF|BXCi&Vo zG6-4ZmqL>!va=0lv6x1Y!giR&McOBq=qYR`U+mH}tciB8e$)hy?!_ttZQY# zCc-mUD!q~6mXmsDzuxnHOlMzWyIRCPB|*W-XC+8LctAOwf)@rlqye;_Shz2}}F0GkI1ew_UCre2v$Z za($bl>YJp!GBs4+MD5AQ`r=UETD?BT5fSwnH4Exv!A0{@N3!Y?=d+n15|8KpYjqCp{_UDcrUz;?p|1|UG zk^bLaiI4K1W+;gApGJe({FgKN8}Ccp&~t@t&Q;J0)Mex~c0srPDQlXcF9BZ|jfLHg zf92H3ShwS$f9`c}=|pPx@7Mw;n!~CXgUiih;QidV@D50Su^GRMAMG#pD|FCZuu{bP z#8?2{WP!b#llO66Sh;-JvehgzMi#zXG_Oq;vXj9LLlHtcG zeJ(9&rcn7*rqn%>ZDYmHbKrhE3x4PzrjkkP`(KD%3*{+?NBlI?kmF)zuTJF)25gT!rqEjNPL~qQz)jjv@Bv` zX-WP437%SSZxzq3vX>_J3TqUvlPA|XLvYqYlWPf2u9(@q+Nr51S6MNJ=R0G@cylX) zMf4dhy;nqRXlRD7F&R8x()Q;WpuOQLVbg;tt3-IuQtF$zDA?b(0LD9G`HKTHhb}J(Fa% zwBXQahhmxg=M6CF}2+0b2)sj^esm8M-)nr>Mx8bED%>NAULuwbk9P9fJ<3u za%tA}x}*I}`tB@u)Xb)D$`qGS5^--P&=x8;mIwc+OjG8SK*NnK@3ye<7Sd{2zjUEV z!uOnFx_6j@{f%9|1*V#d>5ldc*JUnpH&b0MWv)Be&r+c)ySR$I+T`mE2E9fxf3)fw zcX(ziH<(3Xn^II>=zbaHX(EL+K?YTVK-v{YCOe~MkrhpAj6Dg6qSB+kUkrFDQ$YME z=Y!_)(I`Rc_*^DmzFD7~I$V*u4<-5wY^jVAOg@`ZNPEy6&d2q|V|f%%mFu{~dx_TH z=S^al0B88FrPSUf(NxpnqdrF5m=v{m_I_XjhTtaVsZAOF#JW%rSpimBN*&oss% z9k0h3w2|PA&PD$>sWxULi{~U-N9+Pm>E-Inl>1Cd*-GWX<$k&p^mc1h??Zl5uHUp4 zYe_YH{1Z+a9YjCw&OWvWnbZ1&#;XqJafN4-|lmn`*KC7!+cBgIe=?l?sC2tQ2|%E&$X1MC$@WW)c>`y z^%o0*!rYh0tsIzBVS@h{4oERedm?u{%;BprDGRC?-`b6!?tXc`pzdxl`-sM0hw*=nMTNcC!BZ_3?iz-YlsMl zuAlwaMxOtl+GjcP;C{paTwrGn1?uqvAwZ)ZFVMN=<|yuq zou$D$BzN1!)-R}D$!4CYQ4kG1zdjWdp-ruCmYNcVpyj~>C}gHbvZJtwO~kc#9-Se< zf#A7%^kC?hI>c@^gB?L=*uEI}*WnGuDMxBY~nL zZ3<7OR0J?8b<&InUXAQU#T>eo8i6gjWvk8L4H}jHnirH^!@nu*1Zwe{u6^=Vs2F{G zs@kaRDWYOYMa9}(bkci!jo#1WYX>Vrr6c!?t`7hPmU6R2HI_=zOBwnmc~W!DVNMq( zbm>a*T?E&y>?i@FIYu9#8~mP-yyx9-9SjT}P&KLEKKz40wUZg{B|@4tmTUg6?)O=e z^?q2AmBn0hToqYF_eI=tk!zPa6xb!3g}6$YZ`LJNQaZI>XZ>vy^%4$ zPl;gVQ8qwEeT!P@V#AX|Voe>~c~Im`V($Eq6b{q?2wXN8}*F2nTUSbEhTgI zLBaLI77N9ihZv_zBsxUcdC2gT)pp5S?6ixI=?vmf9N>&_1mNsJ!DS8X*7biVp_QBoiE*&6pHzS0zo%gU%CD$!=Y*LlnE-iKbAL{-GKC1G{AIE2sNf=>pMjc_) zQKKDgTB1gi+R$X%0Yivw)XD!?Aka3l)0A3lCoO73a5916GLg2JYS)dp>$Ys`x^&k+ z)G8!^gz#T#+qsgkXx-gQgA^?yw&;A%`<&QS>YW={Fe0}${L)6ba0?!Ws}7_4l5}y| zW2i*!FLZ!TF6tq@_^A>Ybk!w?nsLFTUZsKI$t`kq#&=7@N(hWQnM(R7Qe?jDtfcnU zr4g(_Bzk%zBdECwXx?SBt~@)<`)d`%T z*cHrW9SDWIKlu2e10i5;3EKVx7Sx&s5XHlJJN~X%4xnPA@2BP=p$-UGMuI(|GorAz zUJi&~YUTl<^`>L26Pq<3QD|Ia=(D30joOhWt%ImOch_gwGKMe0S#n1Qef7X=4z0oI z$Q_6sXx5fEd55tak1r6Zw{cdaVCuF(cAL8g8#_0(d^`5kp5?FG>8Pk<8Zcx`0LgKSL;F68YyPQ;U+}|L{#aq5=*Ij* zZ|R~nQBBCWhR5NxtUY|n+QY9sgvoI~=a>kRc~&VwV~tJYOU0lw(jKp2lvgy8x4x+u z(@{J;6o-1okZvk(unB3hhE+C#z?sQ5Mih)H>A6CIgP)#*x>&tfz@WgXbTk~V;rhy~ zmMXZVprUG^Bgy0pwPe+zkfHQL5F$>BQZ-xXO_^$`LbViB(cKAXP5K8U3-52H-BgVF z)_Kuw6@V?1^^1IiIl+P7=jT3*BcV~77JE~>$UaHuk3+qeP%dMLbq&=jPUJpLX$=~oj8glGWn z&DD>@|x{P23K(#@Rir1T)Aud&j9YNaou^iC`NK{NeY)U&`!|DKiZr1WYl zeZHiJi~3D}Ew@NYoV(5RNbajgSV8t4G3UC;VZIAxev?zZ-`k4iT7$*n6(f)?(5f8- z=K(DQQ-4EEO_c0Qg;ou&%2YFM>aH3uylA;hV^eOOe1swE=Kr`MM@KfSG zBE1_*4zAG_5z=7j0X#5^!jRJ}kW)zCE8<}X{R!4m;jlAxpsPRfH9!x6$(-~NtF~Sa zEGA`Kvhx_uXl(!Zb}Gfll>n=JU*rls=#6f*NB`0$c+e*}(9ig9^}-ZqZUGKN?-mF& zgI}22Tuaqg@X)9ym#0|*vj=P{*BqWJECEkvg>|fqozH8}KA&MlXZ)2@I_``*|5s-8 z$*dWzed~WPqhapT|9nO_)_CC0^_(khm{p-r9)65)g%Nwx-L>5=7<5W~v-Uy&GKy?$ z$Ke9?35>p{=(7pm@lnHfo%(DiP4EJ2n_7JWG1PPdumav}2aE!$LzF?=t3wfr(AG#^ z*$;?-8L$|D8jI==S)_hN@-=mO1^KBoxrkLi%hcJ$r%lF5Q>jS6YQ0j~7sy!$xm&U% zMdy6a>ok((FM-&J(}HR#Eni#$)V#2OX05tT3su&N*kl~ewWW?N8^nLaepZ2myMPAs zSY!GPRuPoiZl`Yx>X_5T_=^&id}_8)@?w)j%Od0COy!fEPFWtG_9>yxsCyA8DAWo? z_Q?kRj@ZiWx^qV5`^zY<;l^@57oquGH8G_dyVHV z>{OuPXJSMmk45G%s{oZ&l~B#V0w7ZfpJ6yVNWA{5C%5I@3s9_-xMg|wUJ95sU(TPO zF$p8Dj_4yvZ7mN@n?f&55Yn5lL~qlB>&c- z0Jtsrw@#CE%z_WLNI@U@;2W#FC(M_cWW{M6+O6#vI5KtlYb=X+dEoW52H)$f!RKXJ z1nj)yYcmUm^nMT~$)V()aYPe)5B(?e#ukIefBF?S@3o$MEWq#^OYUHi+ur0)Ub#>y z8q;F^Bq=YIprn5?EHBl@-|c%R#R~y+U8uH2WpaT-?fdHF0&{&!6~ZvdT_$yni@$ZL zjgR)dBYD+PYxaWH?y6L~tJ=5@Z3W@`5Hu?`zBwi)?rThr)^ssjmS$Tk(#&ko7(B*( z_L|z4o(wtCF+8*QV7wLjD|DaZt1}CpDK~1j9onhw%8(spEstrt&I-=f(y)vruevKs zwm6nWxj=hhE%WEHLs^+>wMi6tLr|BRpMff}`R&K|Zz%H+#Tz~9Vb@;%rshzi_LdR( z3;R2WK`Ce)s}Wq2JOZZAG#?4X#k@XXsrzE@2402lrBfeVVFPy8jHTg!@28>($90dq z`%6|di-W4wwhSE3@0x2bikz!W<_-}XAHoGpl>=CFBeHgGmy?q3FeKOA0qrvFra&`G zANi)JesQRo$?lej80Vi5xpnE zcI~CB?5u569o_LkFyRd*Dt=UQ68b{$ENN?Mt~YZ2V98qlpuH5`>WZGq>Fnp&eHo)h z@VhhaCzc;A&JlC07XmfF=E6FSIUzU-aa}-5(v}G2$jRX0!Ja5w}f3a(8ARO+G;hooC=@| zO(yR)2Y9^OoW$bQ!>?|L1E(%hkrtV_3(@ixgLph~oFxp!sCY3*i4Z7Rnksb6#Z(67 zpjJ1`8w;?P+1)&BP&bKxM|4&dVcT%crp~SoRB%*hQw?MJHoWs*-dGKK@xN$?sheqc z*U#W`%c0oWdhO9b17(Ti*BhSG)JNGlGXk;x#^$A;)0!OR4`6rQcb#^vcB&Z@q`jnt z0=&*4ZPw!Edfr%4tTs?buSWB`sK{nkw6ri?&7~hvP3d1zC}I|R)GFjn7di)lYz;rL zc#r#|rk&<>cI8XL_}W?#>s@M+%wyyNOeyE;(5auok^!7jUZIq40;N0`i&U%UGNoJy z-h{FN){8@wfhlFCbF(OAk*^GxG_RtCAPIlXqRUW8LW@h4iSMW&F2lNSFum73GKd|Pz}9gO>-qh6q<`&`=Kz+p$U-yOf@wDEM# zl{W1}!vCTh2z0w!+Zz3^<3=A+Pe%vw`rpTm2l4t!bc;Ls`tj&}E?d0u=+YW(o>Lo~ zwlzNMgf`1H?bq?fm~i#5g3u zIz+emi8oLFUK7Exhxf1C@&4XA%CN(a9C}afWRDzmip+Mf)R!&XlWF5Whq|(Ydq%#E zO;%dAh#uX?V{CEo_*$GB?9vOqPhZv_qZg(#h|@MT;4tb0YcstEK(C1w%=1;B^~0S7LX#JY|l z#!uts8jQFzNwdLyy!YuF`lYTh{VQd;T92{*m zh60(6ADU`&Lv?iO2;t??nxGG*HM#2JA*b=VGMlZdACu_rdeJUEEoaf+^3ljrppR-N zbLkK>3X@)-&u|Kl9#Ap`QTZ4kP}-tmChRygpmwo$_|oHpvbZzcUCYD(%xiY8s1CBH zKta2$xzQ;8KzzW>!7jqN9dz>b0H!Gj=WGjTs!&J|dg>?@N&aZRH-yshR*&(=_oaf1 ztqQzc)`uw$`l*C_bI1{IVi6?GN<;b1$kzcH@wux@-J4q+K;EzAmJ>Zd%M%il4SFkc zcdeAC3U}91c?w3BP{|sUcyA)*u5qt^fcg)Y-TlHGm2UCbZpe71>~jfsw6F>YdvfKA zk9r<^DR+H?J@Pd@7d=D2Z(`86i9w^uldj0r$9-1A96;p>H{?eHUtgxwo9qx>*?ah4`%fP_di!#v@v?h6MfQ1|B9j%5`-n>)6`#^qCIxwMn1h)n_`**V(#VpXo9siqoM?Gt*QCt_R>IWY%$XX1l{@=Sxd>tP$tTtfY3!D_ZArHu0H7WN6a8dfof@a5M{|2QuHr z=tnF+iZRt|I#HT5`JdI476_O`wjI7`PWlv4=ES!dWA)gT9$L%haJSEYdR0yED0xa~)CF1eL7nRn5OO^b173?a&^g z4Yo02-!-m%^zngXQw4pRMjiZ@;J3G7Y3f zU#d{wstCOz&MjJnqMBAHs%heS?&P*g-C&|-8;BnHkg;umK@)-OP^b;>hHNl-#vcipEWCI+WbiI*vcZQza;GjU14apZZx>CW+LEX}NOl2zlq_h!{r zsA?-jZKHQ(bZ#hJRo%E1Rag}jsfvosiu9q;t4H+FD``H4M*YY&I%VY5(eo)c!O`eM zPRXIKJCnCKWJk`j9olthOY#<{z`JfL1D5KhvImhtcR0rEN#0|!CwWI=$80^co8WaT zkwU_VfRq%1=xlpOZYFpGrT30kabvW1lE-L!K$^Z&-*f0SrJkq0n^z8&Ujfi$L6us- z-SVl;LNpt;$`%lA<|bLLHc7dBPyU*c5ArslE!97AR6)Vzc?>$Nyoh{9eu#DF-8Y)F zR>!>x!ST{{T%J|O>kg|9^wB@spei`XMSl%5{&Y>-@VYDcDxU=RICKNd7_w81CTy;8 zFvy#CL_S{Le(eR;2&I+P-lk=Z6EN|G#8z{0W#)wUwG%?T*+I!OoDzJ_`YJ~K>}kP| zTdd-`re*2`)Y zNb4b;NV>CIruCs#m!M5^brxssDu~G=EMW*IMOl?qLeu_#AgBzd{r`ZEUPP~JZR)yj z*?O_X_cD&CBltz7L@>>q2gqr-8Gp&Ex znS>6U_}EeUr%7nSSkz+zS-G5Pp*p_>rlo zIDJ%XO&=B45r)}1awL6NNS*h7i-Sp;^|9^3k)gk!dsZ#G(zVo#TgQ!79e!1ZU$D-R z6;#tm^2jpz^F91I$Q`{?9;Me`!Hdqf=CX;k?-s^Rx??wgH<>Li;+xsf*PZ7hC&SbV5x% zPVs?0AyYXxIa^dQ*qNqzy@4;8VZi zc1#c^XZ1S}va#)cbX!fPOe%Ub2oV>Am?6leLQTyjsd$;5tG753iy?DJgdmq<@zhOD z=1Qp2Pa^{~JoOgncIqwu_>zL@cRSo&8$~chhsskTn5vVDM}04<_}r4XYHhw8K`mT=0!!YOAw1R+}_WIS>e3#f<`a(A78fQlH@COfy<61fLz zSGPHnpWwwJ+U#o%UTC7tzUEZ4dXW+#7mu?bOvYr1s3t%}Mfd?7TSQ$gL+C*kQPnpZ zBX7yc_d3HTIye`a*R&S1T`bM>V78pmFVKNpomCMBm#M)W>1+tBsIKp;x>jMU&h|n) zgnE2jkDuxRj~lnUH#bA3x1juGch`e9Mz=nyu_fNn6|_*T4|(Zq561b-$88C{RT3Q_ zPt`;R$Wx7qYFG);nLT=HA_UX!)olbYM|$alDX|J}@!6t-<_N5Jd#jo(=iztbPT)3Y#s zXCQuUe5u2@=g<|l=EhLGs?fOc@D;X_A#Lz^++}AtC~2lF3D*~Dk*mn?P_F2<0^XA~h}KXi9kN?6nPA$X{}@{I@WDUs4dir(pgllvU%kA^N3ck5=uEf6*Ub zO#d%{W;*4M*Vd%pv*qn~J8VtO9TQ3jj1lqqdK20BO;NZ%gx><~bAoWnw0j&{oio0~ zAqb~XYnmHM9XOes-+D>^Cy4#awxK~G9G`%DZ}Q5fP3oX!Td=R1mutzdI@L84Zdrhw zUE|;$W`96NXqH8bwm&&^hV`8Ag=|8ntmg{xZ#gsAWEpI!s)+(GJ<}78hqfNW{fLu(Xnto*!?0 zBTLVkO5SXaf1|o2HGMtbbl*Oj3;chRL(H^KRu&fZC%lD`Vr^zw$;nuM@>f<}y}jy_w)m~ZxZQhzHLg0#DoWmrr6M-~;u5!cYZFyY z0=!+uuON`Twb=b!KOtk^!not>OC8+h`-^CA$5I`80a-*6MyYmt>?C2Hf`p?TcZ}{B z>_0Js%oVzytC?QBFgA4GCEBw6ET0`pcyC?^^qYGi_rSC*3**(mhJNZ5Q2k&~G&_Ls zF%j<7Z+&3u?^{l3)J*`0f~7?zL(@0HSKHF7OAeU+r5=XZUYmQd!`_Nz(UQ?Eo^lPj zI)AS%W6y547Uk~99m*Z5H4ZgghWHbN615+^!B+#0lA-e7B^+y(UPD!4y;}?84ho;RR?WI~xsY%%7OIU`l^V;;+KV+FSi-R^w;%rt8F%2*t@No(Th7s- z8kXf@QUE76)370o#4aJw%#FsYAk5>g-31`*THv%6+3w^djTXUTE zn1fLB!Rd=8f=Y6^d;S?xU2}eB7t;FqG{+jP^x^V%Xe^QMYPWjh&W()psvHS#(B9mj zRr$+bia51f3*yxUgzuUP;>{tgsz4I`@oGO3{qg1+IcT)1!jNJ6-4&SJTI1c{USVr8 z-lAVx-hkFzsJR}{N*_r$9zn>BcRG4NWx>JT1L!Z55av1^?$ryZ2e2B>)L)a~&6W}I zMs@e}W{&5!VyYPfc_eZt_HUCn?y<(=2-;`=pN*yTw`wfYcq~`pwV9gwal-Lq8N@@> zD|G!~?$qkJ>$DpRT(n6xGqOYf4r(PUR4cG7}rzp_)RtrS=caN~(+1s}t8<|0HVfIB2mtVjlO0ID76v=?H(wQ} z^SB`ZpDluun>0Fc@=sP4m)aBb)7s&RI_^R(~xvjh=J{%}iC`hfeneDR)9tL=f}p7H5onyFLvjy$c( zIeIxA2tH_v;O3N=giviGV%hdma}$$%gW2&Wmlz)i>NYswW=9&pC4cs9L^h9O@Y106hbj1C%KM& zUit(2OhM^5WDT0aH6g36~}6!c13k9kl1TuqV0hw9PE+rRs~4>ue%s zDGt@KdXMn6PAIYk+U-g*&CD%09%ChqNIHl9I>GCC8VRFMzt^W7A#My}WkOrIcC6q{ z0nN3M=Gu4wAX86o&ug_?z;wZ(wnUNzRNhXb>HXa%%+g*o+9mT7$(ld`9Qo~Z6h6~9iHgbAK)Y&oe(|OPAPWnyG4+!%*MVar~?%bjBu|H zms0N4sSgCF&{=NkR{^%iIDWy+EVwhaw~ZdLFF8jpW>2tJpfm0d5y7q=*M86e)*78c z&pWhV@cCxxV>`6Ud>s6R+AJ4sa_u;6^XqAwL*&0ykT)OlnLtE)kp#%R_%G{b?9tjk>6Dhv?En2AE5?B~_E!Jjv!tnN2a*)X2 zvUm5_*P&^n=huXGZ!6OpT?xmt3C94mW8>8>V!#(_jl_4k77|e$C%&nrK-&Q=bWtzS zX86muYA-|ICXz?rMjddaNvYZw9ouKpq2A0stz-n4#2R}=`7U=CyvS0{g3)u#(*-t~ z{@8i$7!*t0n{Ut8zUR=oQH=5x4g5Yo;l+0M$7YtgW0N?~OyI(E%4a#h`Nw`aMj*-BU)}&POCk!ZAp777>H7FuvHS-CZ1C;)-0X)j6~u2o-v9ks~s3 z`F0EjIhHL6*g8G|ZMoQi!Jr@06b}{9JkRux^0Z<&OO; z!Si&uCXY2K%5M2KfMd44`Z)e-z|xrJ2?DL=;gcxMac{0FE;&%XW$8qEx`CnK+Oq-U z9sr{wnA@-I!X($+n$HHOL=?H!Gp*uZUjC}~rwEY*O242cP&EBkkGm_zf>3Yy+Z{*S zi;ZjdU154gWA`~c4UtRGv)>H7?#TTAkIi*?5BM2)~(TI?W%Wk8tPaS;H-!%l+c zg@ZI%G>4wSppR#>#T&_u-{jM7E7ZLI4CSu4V^xSwk>F|QXijhPcTAk#RM3$({d+YX zpFfk<*^)u)R29T$Gp(~FgVw3?$7k2Xi41D;!@#27L-S<6tpTsIiSaL>U&^0L9PD(W ziM|)ows+*lzgHuUA~U?Vc?g3?j~39v9D%9XG7z#_V+AzHiW4pZtex7e&bU)+ay9dg zy){shBATCgSqM77F^oF+tgQBz@559Q5Z#TN$^n0CbguYjkK|a%m471_^>Q`QHE-nro#%cfxUVqN3Zq z`Lj4v{);wg!wto;14}0hex+S_!wsW13j2~Mt;;=`dlGnQ+IIKmU=9t_Lm-t8rkrtH zo#432mV9ij@|zfaD%D`xE?OP!RL9JP+Wv(xxDGFJcl}5tU7m%x%-fd|E14**CK6*I z;q<(P<@>+0grRb7pm1S)3ZZpsvvidsm)NhBj_b8q74FU7%SpK63D*vGoLk(XgmR-f z@$222C&Xvaw9lTnFn7zcy92a)XE;cu*=+A;+l##MdSkB^#HLtZHJNfrtB$5N(RMEEDi#5FNMv$ z1opuDNIc}FhiJtkgb^M=jZpB?jz{ve^WrmUpB2X)k6?pcN03B3<*nFYx1O=VEXHVn z_7d%$P}T9wp0T7ePD_Fc8JT4^zdhmpllsgU9WDTIJ5RSLF zxTr^({bh`Rs&hBmP)H-(rN`44Q97mX|-CN)ha?o zc=MQFXn9ncUdQEEXE(jZYI!s-@x#))Mrk}Q&-k8<&4jJ%nX>~sreL-4{ z{=sfG+;26!Evw;Iq~WAAJnW85#a?eLEPvab;IP}b1QL$#5=oe-bQVne?U^7D?Jxd+ z+P{0Sed~!pVH3j1bHB0LqjeO(*ai2)xd-aB`ry02ZHG=pLu$V^%dgwy!f_U^`~F~> zD)U}2-(lv8U17dgt1EEatW}RA92F%i&}3`_#KO@X$)U_!NW(qw`jj)xd9^P=nU2dR z9GmriqHB{wB?p)Q3ptZJhp&NP)KsMrF{zS^7PD!rx;Woil7fTI%0L4hdlh<%muqeD z5e@=wnk@26Tz=DMoUuD``9kQv7H4cjpkZP8pCP-h350OaMkX$tzQpN{zLH0C8gf7k z8CU))wN7F(Qq)SByX)WSGu3VS&!W1^VxM)#5_vc-2}Rc{x{<1#3oh;D*temqya0Ev z?$|=999&akv8M?F$1fg9rS#d}4&^gA7m1Vrki60$6Wyi-^uUUR53g{miJLN3ufy%v^cvKSyXG$N0^=52(e)34k zkoH?v;vj&mawMVg7zQv_Ir({!8Qfj;Pez~`&}F(^3$u!4TXNT-Ev(ZqWLlNznK4`< z`oJ)b_fFO-SfYYJ;!7HlGR2k(I|X_nee|>!e2;8q>b`et#ppWd|U5Y%~^7& zW1i#|WodEGd2B9Yvl8wt8-X3h>!zv31?v0jCI@~oBIbc#5Xq_|`d)75KaM0vCsXQs zxg$yU>Sr){qXGgu&2U?7+_N?ESZ(Ulbcv=*ml+?56VKekGD{5t731&WvuhG$07UYr zrTDh%&}-^4-vj55M?n0IB&E|6SOZp+T%hzI$}LcO5YKp~VvD8i^2GnIx@|Qg8aD`S zh()`}{TvaghhEb=*`&k6ufvn!q!SUOCL=-s(y5n=0-l3Z$wF4VdG2L>5u3e~jyhPc zq%u$e3U|`N=~RI`iWoE6O+nnh5`|oy`H3~=?<=&K?%0>kpwlQi-~+jeTkqy3J&*Vcc7-eWxtph$kG| zZaffaBkp2Ad{gHyohp z&Lf60BsARxa6aYUTuZ2NJJnwim!qhK>Zk!yW{!d6nQv?egxs;C@3G&SVq);V zsC78w`Pv0qT>#2+*xl?bAlo2ggI#ZRrOCwc2}$<1EIG!|tB|k8`s+1aY-_*-c)%d~ znmg-328jC$n4AU^7kX>G#lTQs^Ek(!5nY%%_R;`r*D+Y60PZsK;zU-}hB8}Y-5g_HiM&FQyV_IK*#=59%-rOx-W9emT>L^Tn2Jz7uX5X1LgXJ1Jf~XLziF zUTrTGP0aR%nk{j2fj(cnP0sf)%{bp{M2NL(x8vqz710~h?`-S%vR1W#)?-rq&bIiS zE%7_&j#igf&5hhtUeyw>ntNYvysG8Q`M>c+%zvBA|D7#MFSOvHO|3yyOXsL|>)h$L zwzy+G^kw>;3y8UEU7)ot$lV1{_=^IBOW@_10b$3l6d;%#xg8y8psF-VI%{8k_5$}s zK(wl5=@jPA8e-XIe~FUCoBUnJIl-xOS`$+TeXA_krT&zY6Ab+}tw z2gCZLR$n;nckX9%Lhx|+=6|#OYE9hEUd`xKsYCL~5im$a3D*~LG4HU$WBf2|n;1Mx)xA7r#MA*1z-mRcYH2!I%N z+D`Z8n{%jF`yljmKeR<)fIz6BHlKBMx6mQDFj_Ig7MVfEy8BrNgv-StH2VAhN~Pi~ zvvP`>7ZQ&RgOM!)@>-bd_#&WPI0^44j57;N=@Q?yi^Ok{ub7s_Dg7|g(@N+0T@2A57YuT2_!H;fi)&u3l-{D-v( zev`QY8s_I~Tgut=%HvKf5IF;pJ|?jF#4qi}v#s@+XH!;$-EZOvh{T=6{nJ*&ff z-D8v6F>TF$wO9$){R#gn_vT5VTEI)UK?4XC39(l6#f9)S2Nwfcs8AblKkLfD9 z6lpBsRPd0}h_l+VC-+_<_VumyPHL}y!b8Z^8QsNt{96*qi- z+R3gt^XFx5<{I~Nv;bj1p1;m>;-I=J=g)&Jwfa~ae%ex%S$zw;(oC0Z46n34erxQ< z-Mg)GG4i+dB?oZ?%xWe?uDKp=#0B;>zXv3=X%#=FgZN5qc0e%ve?7z4w>kc;K>S<2 z__zGVE0O{Qg4UMK7w7X~Fb8>`e(UYb^Ra`GYqgu;-GeHhCzaO^-o)Jd;7vaH!$hm`g=fq|qWsjH`wA1)sq**Y zx3*}5_oeBdTidjT<&K04=S%!NoG?e{XK1;dBzn(IogBls(e-kqEy0X##^xBH=UwJiSDCK8>;=K>)!8p%pGog z?ZFQ?Op|nM{9sM=-=H0z(`g^ko@W=@YtwFBFt)uv^?a=Mj{ID^Ad@1)`fgfxKB%d0 zYu&@=1J_2vTF;Ys8j7?=-*ZK7kG^M*G(_LaiQE=_Z(`(&(M{{=Yp&LVa?RTGMDNq| zP?zZ0cmYUyyS4^Gk#<|hd8q@EDWuJgz)98CjD*FU5IyQkcR`(wJ{YBfjTu7cT@@4M5ql3}Kk8 ze3|P(WZiPiwR>&mcqLrXQld;aJ`laIj5FUfVaX_arGriC*+>^p_aBKJ=ak z!c**&so~s6w=;{_8$)=9vsf03jehvhsi$Oo;;U0jO49sVdqQUDh7B?@ENx{ zz^0r_302PI&3+Ymg7}qlAvU|&ixLH92~nAu!uqsL4jx~VQ(sYGg+?@;Az!Pn2y$X5 zBk>}AMI|RTq!ZoR`AWA&54w^Mav`oSi57#tT3=JN~g7&uiJR7g4QCk zQ1#sp;}ZhtSfc%D1uDO?iLutC?}?d5iA-jAwW<9O>~)ImD@n zX)?shT1iuIC$$3l)WwTKE?%4njcN8@&l|=mNwt70>ctGoQGvh$A?*P`f}rZ3EjfiTRdrp_EJlGl-n%7ehgnl z&YO1+9V4Sx>qE&$P)v*pFPAz@B<&&8ER~w=^>bqv(=4HrUv^N>zs&2JZ2-Z+zoMIP ze_(^kWe-e-PIiMkS>}#C017i-^RNdygoPgtP4{$ut32;8{k{KhvpzL^Cg$lpuRO1X z{@#DivP~N=1jTZb)}5S0jOhyeMlM>_gPKpi?0$A2de{@);hH~S z8&CI5oX2cE*7c?^!TKvS-nAVT7h%KERpxQ|7Vp z|FGq{*ZFW{r6N~LRpmYFDWTJ6x+)K8GYeAt2RR$4<4hIZjXSdAnL?kp@btSWTEJe! z1s%-y6G^J~;(XJ#>%E8RuipCx{~G*zf`5!G^u}G(Vr>Ul*r1@!RL1-|gKV zxB;Y&uo0(=D1&aaR;%r!t!_-XJJ+Un52q7OEs&hsK-gs6} z)wOuzZXss@=Tv<%&gYy*RnAh*c}nHHnscH<$myfE<=fGpCVKnB{h9q)wH)c`1zYw@ zo=GAvSMry!@B{c@7hEp1bLaPRVJ3h^zJ|vpC;HMtkBKVIFd4IUs}C7+mLNj_89vJy z3X$O`*HDTKS8xqw$S?(uRTaoEi5~T;pjw5$;kQclb{)S}t2ZuxYYnRU?RQaC2vzOE zV^ssH>i;e}(}D~g+*TVh{ERb%k>RoLV&!gKgdBhRZss(l$FJZEGZ|WnVK`8779MYP zqTuK7*yKXhk8uVcGOXnc0c5y{GZZ4jQqE9{47r@43>i+{hYS_S@FpIsg2-?sXTZ>n zsa!)fGPt;g8e|wi-djV+@GRHRfDAw98d{LS!!@)a!$X`Qj0~&jF}i&b2DI}ARCyEl!#i~mEpWlRk6p?KFM}NEH?}?M>1}4LmW_Zqx?Iv;~^<; zMp`rDtjRq1`MO-@sanlbjgeSNlW?n3YjQ}qoZ32>oU3TQy83C?w_+kPw6Z^H{gsFT zN*+~C+8QlvsybJ14M4=D&vvxi^wvTGfocW;SgjV}0jD)D6?O$DoCOa1^R?B3+1Dg zo{g0*v{q}jGqE%*0NtD%5V-qa1?#&H1x*UM=)%;`udW8w-ipus0mCZDk1=j+@RW49t_&~fkh8k(n z`}HbMI)K6!y~=C8m0{{fL>B{5Fu}t^K#_NE0j+^)=`aOc6}+FeF5iZZB7g!!3R^79 zvznC?h6if(A?j`rLLaXz#DIz~xP|}vhew&TEaRoA;HAlo15#n>n23Wf?5~houm-hY zEymy6i~ZE5H7&puHCnc|)PZ|>OLc|7noxPxv4B`5d1pW^vTp>|DR&mCwf2v|Cic!E zt0!v%%WTS5*;>;#33Ogqd8jH#)OA$_s}EIGD*Yjvs~RG3tExpIBYA5`u>r>vyQ;Xe zj4+i@ab~g2cq&I{*V7P5&qq0`P`OM$N{5CLGQ$Dffdq_m^DdRhpL2s72WI@cs}Kuj)Z@svayzSkVDHLwZ%IR^`;Iifi?%GAvajq0~cmg;@Cr<5j*} zWVW!ri<8h$>f~t@wJh}!iyonlOABf~SF^?re`nX&!vYXM#5KbLGywk4u_btYgq4!06YEJu{ zJ~gF%E_n**eI9u#WdBrtMxck)f)p#CRb19>||ta7PshHMBL)d z<#`_Zd;bir=V2Gi(!}ZySs6ah8K5p>QvBNYH`1~+u>*mklK%4F!dAIC?GHx#{rk4$9~ z=q;}D<;dS@%ug(LP#34$A{T2Rr?%J;|F#1s&|+u&+s-&qS3zgo)_UZco8C{2rJvtx zKR59l^xwiy?j;!6YdrkLi3E4?6_JaGLB5<4zDEfm{u+L|(hTKxCHLLaer|Z36#I%Q zw!|zJZ&gXL4Q8?ZRVa2iyV$)} zu~($n^QzbjX0b=O*elt^Dy7)`G3MSvuP%QlrFEmX(C(3|*JYa;r$C-Ii7Iq{?_Ky3 zbixSSwd$R0dDP0F&1Nk8@fB>1qD3J`mixxUzvH>N-DX9MspeQ>D#jc!Ciz+j2YQ40 zT#y}TObC=BAy9~&@oGeE1eq2o)omkrw)-=pA}RU1FIk<|A~Sk@j9ahaamhnUj8|ibGIRG zSV>d!Q5yQsx;VE*D_QvioWD3Tf3eCR5=XPy`3pIJX=eUX7 z>QHgrFIWHaE|2lvw;VR@g}@Uu*ClTfWWl6!QJ})za|dLKT~C7KqXVh@U3b?^N{KG% zOu4(ta>ht*SNmfg_v$O)eJgJbo*%prS*+-BVQ8_syKI?>>Yf_#kZcRo{PI6;|LlA- zh&A7kgiPK4Af)pRf2Hz2dpLdh)U+M()ux&_{&09JK#YXA%8KbGt`3QmHv?aBysJY) z%0?r@10pn3hIg|^V4uEI1o4)8Qx=djS=7S7!0hZn_O+^_Wr^#;*N6M9Mk@m?$qdJl zKJ-XtN-4Y(4=)Pt+RQ+5S9^-7dKr?^`$yQMwv5o|VeU*9FD=XahnV&^qf;+d(WxUu zB~mX4BoJST1uKi%B_ug0dhs+_qdZNT5o|gccCCox=1OI+t#&N}`^3Phwc8}P4?z}u z#4S=lw)*ehPo-k1dG4;?;NJ5=E`475Qi>s?`gw*7aCHn8HO$ckoxG9&6In@%(=;u4 z2oPu;^{u>;S)fq{ZR(fbn+BV4zJBL*29uZt4*ZEZyKD&Hbi~D05a3l1;LEtN@hJ#k z=9VGAkKpqb1aKV+0;HYFKm$)TN_ctsc{%_^8q{1Hw2-;*3+Q#OT6K5>U7%LoN2@OJ zOS=N=WYxvNV9;8Bgxg~w7HvB)8`1^>*MAn1mjDYl6lymE%~xBXQp2pGcsN|5OVmt2 zff;w+TsB(dUaUC0%4z!$nr+VVZ8$b?uL^%1xD#@f3`Mtiw3P1X(Py48jY2NdD|;-L z?RodQ)21Q6gXfYP`o|t;h({a!B~$`(+_B%H6U);l(((a33x0vH34zv!;i@617LkRg zECeKA63ovgI9`31ge{Ys>tU8a@m{j?qDgPvUCVJ%IfL*0t+a1&nv=J;nbv4mD`5t+ zbZeE8)k7rE-g#t50sk*T637u3~f9AIW# z&muW)i*-3zIjsz*vmN^jCo~--SwTKAK8b9FQB@=d6u`=P=;8B_WvIzTe4hHKFOGn= zL${mD+R9Rk8J07STXo#o&ak|3e4fF7hUJcfgS_Z7JZbkFK#(&m2mI%n^CH1r;QFM( zx=$|7)MqMz0Ja!2$ht&0h&&ufg)oT+KI}x(C^=;5DmhHr!YQog|bjBF0OYJwid7f71YY(z487mk{9Z+8^ zc&`?he9_H3M0c#t&Q^JI*;8pgpP|W5Y8Tpi%jBG?vK+1|H><-ttn2K#KqEvy%!P~@ zCnX|y!IF>D`}Hj%!{VI%hlU6V%(bi)DJ>~c)8@Ln;2PHoLAFMPAoDb-n8ye73CaA& zB_eOTK%-q`y}@Laqm~1>F-*>OCaau^kSfZ;#1hn=L#`!jB*?Hfj7+)I_`o{)<2>kmL=2^3Xld~Nj=L?v^$-X#;waJ>lBt98RtfdwUV#lXFqrd!WD zL0M@|=c1Y>;|V&ZR2pGlq!DhT6QIpl1wSQoUWY@_H{D&Y!XfBO?${Txt#kSO zN?!fAS`Yi`~|sQLh&U78LRRa`#7s22XrK4>{(^4%Ii)mh;nZ_h;sI- zXe)RZdn7uzH>9nA#vjY(v~xp^W+2B~IgsP62;@lXvP653RUX-lY8I~n$D}KYZWc8| z{;Zux`>NvtZ6;AeFmB6ebmro*N#gQ7{hoOq;b`n69HjRiN8d2iVbOSh-i==FSpHh-a+_`7)a2=oge&iJo{n=g zR|0am(VUVYIEqEw8&j*37Tz8EDmaN#c6aP{p*~s=^mrt1BXv78JVbyv#Ql^060||Z zAGAkpa)^~b6(O@kH*Xbq4w>qcDFl@&bJ)#;=QXIrz-sior}wfy$&fLbhA7iUn{AOy z8{8ca522g-5aGfGYI;#Em*aVplcL?8%;+KQPSDy2$=D(@2!BR!njD5xtZ;3U@sCf! zTV*iX8Hm_AE>tlCxPz=)x@3!k`pCX)0W0Yh)ANEk*L&LS=1f%ZgSBe4bxlS!RgE(m zE{vIZq^`qpv(PPY=nR8FT0-2Gz$)Po9IOO5pqe|j$)@DAbk2?D^V5(r0KLbfls1L! z^^Ts)EU2Zbr%M?1nvj}f05?x&yIrP|`#LNWr4MmrchI-E_mau1BT^G<(k8Qpxiaql zIcBPWXDR|F+hGx4a<4+85W-5kl?WZay;2isK*<#qsRTIebgvy)HK5*c9|!)IkI)y` zs9Mw8ZQ4F#+uChKz)3qG*CrlTn$dX3*vnl@GYK|>C&9P|@FuW75<+B#_$(Fq?6LDrRH;RxdSz}O}^ zL!yrc#!kI1mm)&SdD0OUi7#LCwA|wGp`X4VPaS9u+iiAR_~9wQ`lgTM3TXYcC~?4# zzmADa5;l-Yj=?a5L+1*1-+9%0sq(Fnd$cLq%rX`&?1xh20%u0m(B9+_6$Q1t*3?-< z7;6bh6Q89fsQ3eHN`&(1=I*@~RDqBL#(Zh&k}>m{S#-W~L$5yVe!T2Qgx-%bh9@NK z?wXAQ_*k-UjZ)_(a=et1niHFth~8(sdffyYBojYHjotfwx==5c&*(>OlRtf{%?vYxeMJ%tq4hZ^ zd?lYlM&2x7}`5@sO>OH*W@ru26Cj`-kw99x=X%XEMJOhrGy>v zkL8Oky9~bkjePm92Xkz!c23Yfw=TqEtouq1cEDWYpWpD?k}ICPKy66VWWYDpWFSM) zK4>c17qFszDf49hfN|M_SJ=$=Y(;yYQnWV%&Pe>BEJb@aPlg0^dN;>rQ?>&|NB)2# zcm-7Msm0>tPuG29R~OL{aX@OVLZlli=QFJvtj^xm^YJ=+3{fb~?5>NM$|9(Kk3J?n zCE_<*0hmZ(Pne}LE7nt|)z;x5>OVq?z5?)=sm00(`eXPwdX;)RY!))d3Aw6d$r(u| zC!>XZ&;W+d*^?}PG=je?zQxYcYo=fRb*7u%^&&Wq7 zD^%Rsq62miE7;IP7)dpr4_TJTIT_0Vla8S?VdwSL|D)w_s^!R0OTpb4RviC$U7Cz8 zv9#l!N)BqJK)NiuTVFbBw~9X+q{Vim$g&*)eeGM`#ty&k=cDh^Y8AAV07_t7e{7u~ z{;$+A61tYit{P^zifd)s6T;6xucE>zf5&eNB_2FHi7?ZHH@%Y*&Ki8OyX$eV-YYV$ zKpwP&F$e?i(~e7%2?_G;$63oN=Eg&Bg8UbDM--geiB4+wDAr1eAS#m8PNuIwB2|Yg z5L6ysTo4(&MZCCFGaJZWTp*uY!1@p17^Dl8+%%60J*c4$R5Ri0&FTPXwGZh)f$Bg_ zW(SJW9VkwBpmh8W0M0{6b9SqlnnpJgM>D&DE1r+hjYWS|H|Do$|Cu(aguNR~+%s40 zIaxKn$^au|Zx_Dr4M}^o5+S#9I2oj@+z9K&}BeV zL6=f|srD+vnPYlJ1RP5Wa7?*=3FvuGhIwZNEZh_+PhPrXuNfaCwiBoHit*`|j`K~L z#U1+xo1imr%f^L={5GjxZQQ`eIgLELbr~ID>#kYJTbJL*H(wKrhaq*RHO|%zSSD-% z{jo=7U*5d~o(7dE0j%+wwYQ8{zQcEx=|$V57L6!rq6dEfy=@wqvo_IxD?HX9w*ZCp-e9m-Dg(%VjXV4S-5XWIo%LoTnSC<}UaM6!p z;x8CE$k?)h4GWz|*tv6eUWO{dS zO8SrmkH`u5Jo*XseAw!Yb;Ra#2An6|r>T`g)b1(#-L3CFv{mnhb0KT{sJ5$B+kALh zsWoF;uaVZBXuZa4UG7`5T6eM!s!auyC8c*)(w~vMAjUaS)nQ1K|nk2>5LnrUVMw(po?>CzdjUVu%Ml8j2`pbv>r4WYR|0|Lj|L^J_pow z#nzEm5f(%rsuPaEI`UES7sr4r^*+Ei)B0~rQnbFo4=%m$C_i}gzT^Dh)%#BHgHP`} z#SebH?=(LY5YOPUNu!zcJSh1hwtp?0V$s|`&1IJfU+;g505Ug^IRYB?K`f^bM_7yQ zBCLbJ9m54CThH4@{2+w(=a@o;N+waDo0)A5JEJSh8dgdIg%Uz>W!b}mM9B+LyC5<2 zydB&Ep+Y#1LYPOU^$3%`!5XPjhBz$3uIgakX9?%5 z%&Hs$pL(VSrcYouDL1Y?m{MlmyNk!p1rdxR!+f#B-pY5$Y}h-RupLd3O=g4@0Wl-j zMU|!?JY9Y0jPM-DoFL375vXb!W{&7-m=Cj-JC_)3Ra(~4yJ5L>S_Mxg#Eu?4vgHGg z8TS<^+iq0~6S99_H8tD&?;$4OS%iESp0WjOs%-hGL0&WxW;TdOsC@4(`CeNm2Y^cj5xfv zvDKhiN5$l;YTmw zYmy3x6_h4B1jI=e5YtXBq62dzsUFmRUViiqmUm_A>NF*OZYt~Sq4RTX0Ed9~LI5t8 zh-j^HZm$enV6(zw6JT)2fE>th0QDh#rlTDfh5-@nRXIh{m*@P~Vqnk8+B)rrN+ie^ z1$D`*>vg%=u8^A#C`JU~(2?j@qx(m8Q>!Dot$?aiH-jD5i1DhrG?*J|YGxs#amO|| zk;F92jc)Uzw{|#=vS{n`LVkW?c)FA)G*aif01B zk9Y-e`&Jy%9x!-vxiWX`h@DmWd9tFQxb%E&2g6yF!y>(~ZYKG4nI$sv>QRFVB%{Oh z`)ah5KymcEgZ!7Kc{uMB9W$1n(Zb}_x~Ue@z~n@cOq!YG=gXIc&Wt2cIuSau;Owz7 zk1N74@1A*Dqiy6>ZKiJ|SwE5tpJ%r_l~|BwV!!y*X-ceR1#2Z5yuZ<5%UBTcx@fJ* z%UftoCM0b~ehvh;6S8bj;&m*#pUYCfps`!WAyz+K8Nu)Fg-)=0EP zZVKD>5ixdKn{oBm#%R(P¬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

d2!9SJzD<~KJ%bscvf8hHgZ2g- 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/t?0wUFix?^?nT1!*euz>HV!~(Ir!(Y0zaZ_QE+kj~Q@vlW* zXm=J2!^M=bk;2ppX9IJu2}dP1w?lRSZZd&RCmI`~K&JCwH zX4~S{&Nk`-44LFmY+^&Faq$U{PRg8|uTunc=~8-LyTBlnWdgUqWpSGZUCpufeBCM< zy%_MDSI{(cD~ZQw{&ycsI3xn2`QQC4*EmkQyxaVaj(T7MR(Ho5j`~di=x%0O^t!Xb z*lFVIc6w^3>^@{qDysmr9-Fy97d>@ZD?7^5L<5hpDIek>rl4cF|0NrY0vbijH)J4K zciX2tHHvRW3gyFB0!jL|AW2u@mVu2QPdj5ror{@pLsYd#*cf1c{@Vu$LF%%WMQFEK zxsOCza-BqwJqv@p{5xPS{-|_Q`~}qLP1uaTi!BJ;Mj-eJuYlk#BUQ&B_&(4n!@M{) zvb4ryZ)IV^3N42XB4GJ)>qEN7Es*?SDhezE_x;fi!A{XDamTQb`MFydE;su0MgVY@ zKw3tpKYL#c>Sx86Qd9u6OzP;rSeTN)vX$_&6e0k93DbBOuK?+zewgU9o|ulj@Dy(| zK-5mXb#%}=2BE`z7N9+qZy;P#6VnZHoIr#_WrBh1CjKZZN`aO1+T~^aHa2fMABfW` zjW~hJN5`}|Hqh3R-+%t4QYwvOiRzz)tWE1=t>C&SGk@zkjXl9g5E{pXLERVg@Kf-r zeS{I!eOpi_-z0Rs5~A)#m=jSYobIQmL;M8%sXIsHpodTd%>%>w?yFDsVo9kIUxIzy3*~fb(26c5OG?%hIWy@3rF}Y#QeC ze6W1@r@)H-RijAX_ske^u8mNn8wo|)!w2;<&(cACih(r;py*r-tf*ff$rQ~Gh&zi@ zdmkTGOoe^e`YlxH&x{*=Oe65lZ>{STTBOvuY%s2ub4i%S(Q9FDih05A%zx$yK9p@f z%p(}KGPlN!ema{6JjExnfwiL{eBlWh{UwBLI1PD?Qg9{vrO^OQdw*;GLTcdX$8rK* zQu$eEk8p9I6b-RK;1t6#%pUNH;n*-gMd)cgKSh;btH3da8K(uFA;$W^H8lR%Q>oz> zCddNyel?+JijU60FFivw@-o^}?G^y37&(}Z8*ih5$t2Q4Yr~{Tm@$Jzt}l-o)owLm z-4OL@9bl(E{WiMD3GH*Pa-t$Ai7`W0>OoxbL|>Gtxfbql8L**7pY>V}w5ZW&DuGxOfLeKtaHy~Q ztbgcd{lft39|l?fFvR+YVb(v4u>N6GbyZ$7W;X-;whNTyt#dC$n=29`&oo|3Wd~zT z`Fp;{cTpOo>j}yq1<^c~O~;zj7km@P_}Y|jrLXGX(Qe9IpWwS^kog1Y8B7(UwMl~t z4FA4_Oap4{;Cqr@?xmN-c5dN0!{kOr4z9VB5(iN6ST&u#)M?8ktR{2ByYgqXo4DuY zxzEm$D69yBq855svtpr-5r>6-Mi4l$OA@;!u~&Cmf!^GzRc^casgCiDty=B2StIXs zxcRBl9ALDgT7&P1+pn zQd>F9ShK>~6)<9p&>epTn_2mc6qWW)?m==!LDRLPvqzp(Hu0k(*eeDVntbt-wb!@`HOsfzUuV(N*S-^ ztAOV{5lbZ)U2no4K(8)7V(GV8B*I%PPYg%jPr2=UxKABGTyl{sm>hB4;MDeG0N+At%cZ6DX# z_7y4v6#wSnKmzDa-; zmvO;Jk2c`Eec8>AxnEFptnNrhx_o#Y-jiu#*A>mNVC}B-=Y*H?XskH_#z?AeF`vSD zlALOw*Q1N|zIceYg(tU&sEQ7vD!|Wa%0IJ{A$fO#@1m4GE!_feiF%?++BB-dIvaDZ z_*DA5fmqN%-DMP`b)p*@lET`c-*PbCOC)sFJqr$3t6A_U*NEYqpw z9zJ=Qv96SHC;O=zeFshA0XnZdoiqkM+a!tY37GqcbnrEmfImC-o@R2d@ zt^Lj5J#&!r)?tHh{jsm=Q@$$Jr!ITB?B%MjL4Uea z9$A6VFe; z^5AbHP&%*Op1xKc&xW%Purm1rtW4fjtW0j6NtwLE*pvU#dA}gKFPDM3cq(VG4tee` zpjb{bvIhp)-}+J5GcL-pI0JHRTvyF1^0m9lM-UjS_S7BkvvluSv7VLej{uRhtfsyx zy2oB)9Q$;8?RBh~-&f1p@>zacJx6rst0s)`>3aFm(-Duc%cYbrjS(7BFeHv>7OLb( za}@M{Ed6{4mCIMVVGL z;bY_aY-EW4Qc*`ApWDo>C|G*=CkD%M$!U7|pZ`f1difvFdifoeUjC6Ehl}*`pK|WNBg9Tz| z{>!I{EKClnyRbRI$=u;ncky=ZX;63Z?(Jz(ckyQKX;ydfjnI=)cVV+HQ$6jXH4lAx z^I1!ueo>)>K0P}pxO?vUImSnPS%TP|{%nWeXbfcO;%p3pd}<7VdTL~=&c+C+rp72J zrp6ekrN%fYrN#b^W@7_7-`IqnH#Vc&jVbiGv7P#> z8oN|amul=*J>9CYSM|VXP4&QRSM|VPSM>}SA6b!Iw8H32XFJkH?_Qe50+<*4INtn8xa6bV?LO)u&DdF zF%KNv&i<2kc)>cY$>P>&k0tW=o{D%lwd!CSn`tI#mu7on)WW zcM@cLHysRJFr^DNqu`)j@ER06uHds|a(35Z_SKv|w3W?46MWG^q|we7s>n2~)CT(3 zq(sfVfqN>~aIX>uZJ)l5`}K7^ps(W<`LBH=41sw!7*eb9DaEFw>6Eq1Zy`S-qWn!QU4-SVn4OGwgq z*$J9(V;=Y7x_$ummuvSCnGr4@`2y~~-`;TvE(hdi|FD;T(bN&3jJ4O{G5x2t*Kr2F z;hO6`!!>v9FL6V#E2e9uF0OaYyN{k{X%iIPcLUai(W~z}^zWfQzVCYN`>yu4_I=mS z_Z zcbC3`>ux}JdwGH8b0@||o?Fw|VSE7J>lwQ?ur56h z&1@Sy*>mKwyI(J`Rrj{l@5{Ud8w&n#_2ozBSu@NZ8|~|S+0Z(td-d}bAb9>dRlKqX+*;{U1S{~d^$&iccEc-BNd$By-1FzEk=Y@gdUsFF|ps1n# zmItSuH!6RMuHNxEp^se?6e*tSp+yt%9lMlh=LfnUgIUmxQY38bw2DNmBGDdN57cwy zme;kByLI1cbFgo1rCC`N0t4h1W7M&oVYv4BJ4#(<-{0=?mrlU=i_8CyU3U4o6Lxv_ zKhz%cXY1kaE0Apo!SpH2rcdn4U`dqmcI!XOV#>!Q{9Gd7x(|)|U@a9{Id{CC;l*?V zELs2ae{;6=kMa87RcifTcdh?lo_+mGXK%Un<~Le@S<3v2r(L_?i+2_60_VTXu^9W; z33|$hpT_=Qgj>5KWURGB1&M&UC_>3988$97{l*u7QcP`CiLkjS%Cd0GrMZaaJV+&x z6R~nIznl}&IZ-1?kI@w85=Nk~e2apnC)eSFkOC(5p;oGox?mDn+sag80s_OZCKMx@ zP>gCq5rkzWWv?CHh#z|61Fl+d|l+8Ywx%h%^g|z8+|@l#OkwH;NF!Ns=S4+%%NRMgPAnl zBXah;U-o;T*#3|9e#!k_WDHoe-~HBp_n&#cUv<{~9>;z!8+n8M{^EUS-S68=XK(p< z?;G!TOlkfXuetX7-wvN)zl;9X&@l)mdE@(O<9~TJJCMP~4;YCscKva}DJn9n1`FwT z=SEpVN0?w9g6Raru8^Fxv>3ZUBQN=siy?tVciH-l$rhW&*&e9+7W0y*KT^g zIY{`Q2_w}R9YrrSN3TvLg@@$lnM(>ADZuz@GG=q*<1n~{IKdp?akbOmN){+I@RtEr z0p7}5Yv}M+Jq)Zvz#MUDh54}(TcltY%gduNz@uIEy@}U4y5YPlLEC6U~5~dZ>|xA4*;pBL zgS`6NE})Y&J9Ya9u=u{CT{Zx`?JbmXoD;8~oSHl&%7l|!7utxEpp#&7 z3&Kh)N&U1p_R{RqW`vkgZ6{jAZ%(AtHhqB2y*ouF>tXT;YqAfXQKnvymC;Xe-h-P& zT8bUGNyMcfnl_2N6t~eP5t!ma+7#O)k8yf@P#*o}yyxLnU{ma2B+MS50Uw&TVxSVk z_UGH7mBg2!U*)HMZV!)6i0mOcA9(0BaiCKTt zNgve+D`Z!jQ?Kb2tzlJ8_Fji66yJ5&7dAXx%Oey$dtAqL3~g%AQOjeV?qt+#vN|`2 zKB03l*Qt)1!m}5)vE1m)I96i+$VX>IwEJmG@67u>{tK~K zR~>}2`snr^pR)pYdQhB`KeWJpfpy4Q{7iQ~+gwk9!!L z;f&M=1M3C#A&W3{g^zSK+$G4M0&UD~;_XR|%NERkTmbB+BfPnZ(6Z%=f6&*ijt zvpuo=-Nsq*s!x{A-tzRHoE5LK{-eS4&lpS#b}bf0I%%wlvF~EmwWaO!Z2sf_S}1hp zQ$OVG#DQHx?Brt639%tx*5e}Kr{kN@uMgLIJoyzriFkw&nT&Pjnj$T^RIoKOmVgp= zgjOtHXt=~9w%)@rm`kLV7j#z3lkl^V-|#J4>eq*0L**Nun+qMIQG}w%+&)++AAS@G z*Ngl8%JSjAq1Ux*q8{TB*jcBqG!}b|S;jtXnQ^i)8!R9F++0{@oTBG7?>{oYA6)K9 zzqhz(h>I@bqG3GWvp|Ne_$jyIKOsw7yZ3s-r8?(neTzB+lA=b zqFAmzl4}X3T64Dt^SQ4U3K->$k(Rbj8RM@B{X`UoAI^c0(wE5-Li-pzZ|1$}CHT3b z|B;VbOvAtV+{uC*AvuJZ9sVOBXcrJDOawDyLAJ;40!06W=wu|cJ$5f+CJoFi>^)kK z59XS^_{4(VjcgyR;ddjO2nz(a^pLa?lGZ@l`Xr-*(=i>yHDJ$x8ZSF=Xu^gPYT~Nh zgjv*VeSu-c3l6LZ2&`zQEN4?Q*VN5M(+PsaAm^TJ`0aX=oaZ+CFOv=9<^N#a2sL7X~pXu|P!Xudc(dp*&e zgtXBohbFkg6ZF}aQ8xtKS0V!+TeG9Tgo2s*L%DB}t?@=(D|cC3)-{MGN(mfNhTN`a zc+5C3wU1_2?bQ(YP0VZ&23ccckk*`v2b8cz4y^a6Yu=Afz986N!)@2F{$}*(dqH-v zt$0WT&%h2U+o-7o1Mrky@%%3go`Ru(TE*PzDb)wv0HMAsT>XmdE5t@N1&q-hr+Pe6YqVD7bNd)G&iH*|1(byCl;d5gstas#1;)b*F|~(~|J_qR<~d5!EYNP`BCDWcA<}UC0~%(di)nrVs%SP- z!A-G8=-SxrW43sT*u`wCt8es zT1%c412C3jGL~wsD^CdOab6WZU+>=F@zh1_#Co!sOpXRn<0@Xkm* zgh_xBA|;CkjNIs>?>i;RnnD}^|8M%0oB#cpV*dA7F}*PEJp_K-KmF9=$1y(VV{~=8(~QhPF$GZgirE!{?ygV2R?HKep3fy_i)|n(*(jFeHG+_U%yp+)lF4g#&1FW z7Nj=>c_f7DnXiBj2?Oo_=2u>~!Zv)G3&c_2VJ=XC0vo>q3ZN1cz@o2CrvRqW8@b1A zdgIQ=ZF=LgkK6Rdz~eRp{&Fr5MuCeSx1F}#&IRHq;Nb!lDDcb2>^^+-ad#iEKI?x3 z{%gZvR zALy2Nw!VU$KYQ4nnvdP7(YC2^`CuM8_Ly)Q>984mwbdBAR5D5!z&jb9)ondB?^<6D zRjXu-nR7|u&9SrJCY7vVQLT&=07V80T{;=5qYP&`t+mC-&P=GY3&s{~6%V=)vV|7MmwaI9j=($e!znPvBR*&Tn zPaYxsfcgmpfx3R%jmi?Xj!nKKsuR4WcJudXDLyMCdtg&)puLwcR}5L0%Dy;|RiG2aLQm`9SGrn*J>c*hR*ehwZ1o z!X(~p4^R*ujy0`>@fRWNY3nyjl6r5%)V3JfOo#|qy97hq+R- zLZDQr?V1>=+m$ z1eea323Ygdn}9Wp zejd)riu-}MRpY#DVaCeQhr)9`LpyL>2oErO{CZ*+df(K<`hTwz+pZ_J zT~BN~O>En_PAqLAXZV;~9y%Hd6%YI991&ZTp4l7U3|z7o(t=CQl;Y?+Z*Kh4s1G4b zo87<{bpxR<>IR-;5ALwV&km*&pPIRe!PUdc6HihWU#|IU;KOd@;oc=Ktw&jyxKGmGhD33kAfVrV9%Z zjEswydzX8};ATP4c$R8IG$Zh5ISwphBia1fp&f{j1joSozb$Kv=Ov_{1OlTkwhF`-D&2!OoJKQZVX7Tyl1myeHe!kiYe> zK;yyRq*32cG{X!IS_W8Vwq%8?qjrA=isQN5(10b$)~EAZ|DsTs@d4FuB+@Qu$c1X>LcddRI7%} zaeX}lQb+QCbRS~%HL!bXb}RxVhgfT8O4!T4*-TTvw1>{fwt$DqQRiq3VRL~mS1(jZ z!f!5M_fBdSoD+Il5zQgj647`^6sE{gt7+KA0yallQmxX!Uwyq$(ACTNg0|nt1Pv|y zM;KWo*A=Axr)XHM`P^S{XAv&Q>TtBPrL|3WcC&Prd+OsrVeoMS(Lt(}mPLXdNYSb- z2$mC=zF(w$zqJ3p8ST%b_Nn29<%P~vn^bh0ZqtWAwgoMwum+c#m=-dP_QOuM7An8H z{KM1!JQBapi4cSRcmvKwD$)w^2c29w7rXKw`^!RMyDFk+H$I)^hslw;01i zCj?Bho0}N|EItfeR zoF^tkK{wgJ_lzSJ`st@qJOOT&wNTFZax%W!hwbt8K1h$918DsjjJk0J z3Vf3%AdLd$5K?!pLV<_5z#s~2#N#~=>%QEGHx5#RVt=}g-tJ;<9KF5&OGRUX{Z$(! zhreWx>O{^SRO^?D2Br0BF}AKRl^z@Kzaey_1PHX<%zkv{lkO9V1Ew~nwLE|0 zDq*jyjV$5!5I!4;A>*(TMHGJEkcAu9ia!MUXjq=;tM&YOKy4i5hoIVcFF%CT#`|=Z z)EKmC47qB2z^-wVT_a+%}tcVP*}{2Zxz2MCA@MQ%Gl3SXq@;7W7)+ z^B)%-;sJm!&`f`0Or`8$szrDo)wW`v_^7NgCp4i~$4`|+%_MY<6;m%MOaH1;Ki3f@ zQ>{diuSQccb()e9yAP;eKn(-!Y@KqoW?pJ$DubE;`=2yx{mp%rboiZ1PZoH1nH~N! z44&FKB(lI7`WxZVB7&EzDMRE?8Gcq8o+T}ty0Dj;1EP6FqOX64N+?NT@U zxPv$Q&$f#&@}(S6LM`p%Wn0=0YGHxq25VU9O|D$#i4H`Dh_=mQE`@b>!>)?Ly3w*eUzv; zOZzjoH+a%-H*Rb&+6XaOOlV-r!q)TqHi$4Lv!iRfgPo{N!F_3J``@QZnwac}0CK&Idu{L#^HuKA9emCwle40Rd%X=Cu$<6m~OO8Qtr zujarudZxnD^u6d(b=oaLcHwHVq+JWyjh*n9caFPpj^2;BIXE2X1oaL)k{&-3N*cbG z#@E{D8U?E<52%+br81HIg% z^YSOmh{ryZ^fx!k(ptRk9FEli`LLB9-^-?J7|yb20(D2fctS|$a!o-!S~O-r4jH{bdo55B=`3zwSY z5ni2|<*|Zg0^)H@GXWx|bWvF@Y{W5{W}k)*%}ym*MLCrDnKvDpHuT}^UT1YhNFA&_ zQ$BJR8p|_yWKCw#`DxG^LDDGe1`(DMRp+3%WT}Lm5thzWSTg2gLELIfXN9Mh>XImd zg$bAoMjp|cRST@h-C>RLp|=*!7qt%9Ri!jY zky*7oCWQ`Nx|UE)UBPL<=rSW!v%J7w!3aFP0RQ*GLPwOU{r27q^TbOdf^A95j24^5 zUfHj8gd~|_&=MgWN(*mU zPOLT3DS8=bvGg0;V`)pru|2km_7@Pi{eqmC`)E(?)%Ge4wrzps5rspDDIGS9{tlF0 zrB7|bDwU6X8y$U{u|Ov804S&z1oDIg@`PFr&4!-8wd|M^4l&Xq(>M+CH0fq!UR%rw z3-qMy*#!_v5ZTeJfe-|qDZMQr@NDO8x$mzF1#1h*mXuv24g;~?tCkm9kaRP=u{&3p zg%61Q;P1~SKj;Ul01?8yi%N(P&i|DoLilvgSw#q`(%D-&@tEu^g^h|cdFy& z#=Jfi4Yvo?5Nkhy8O^2C&|}_WoAWLCV*WY!Z`anx(-&sz|Rr z%CA6vMX7wkg+MH){3_*B;hFmBM!gRKb5r@u$m<2D^wSHiIwiE`IAL1(%;zdRD=WE9 z=69GLwUKqS53IACk9=g?8T?UxOzEU@EZ4W@;Z#SwZD%2`1EYIi%COJ+$46$GOKANqnz4ZKb5sx+ zbyu)v)Lp@W(GvC+)cyu``aO|;D-k2X<=(<`v76j55|`^p0?J5l?*rHH}fPnkiO8!`gw}7FfCdTDjd!{8L)B!#W|Ke3XuiOP4$7b zc2fqt!eg4_S-{(0p3=O+pVOPV%O~tVFiU$WA@gl++1Q`Hc)2(KDM~T!;Vuvz7VRiJ zcXaM@Prj2226VxW6o;q`K+6fSNHpv&l+piYNpR; zot0BpYCbc^!p@OMgv~6U9ywY~0T@xn%q)%&Ju*+s%*t_#NKurC+a-isW)+D#MJilH zV3*|+tq*1vGD0fqY!TC~q#-IAT|U00yZo6QCE&-j{czoodq4ba$h{w4z#H!el>9E2 z3~J`^Gh7mu87TQLTr#dpeq{*zsM^^_;UR2~8rMF0_Yk&+Oueyyw#Nb9N9A0w&b5zz zyA}oOTm`Lt^ux6!PB+#EDtwhNX(9x{wrOHct6$>?amEwC;F*b=RvQhB16I$(O@Bd% z>DHfIBy>L8HEEM3+7Lv1^H+LfjAh0GZ-Ak4ahv}Xv-wYPZLW|IOq!LtLeh7?D0M|d z;VUTP@`=J%Sd_s6th0r><2`DO@uD#v@tEdW`eKB^fXV060|v5gsSoYf>O+zfV=`b5 zBl?nK*)I#iwNcY^F}2YzPjR)8k9>Uv3|Qc-WO7nwWb*qoBa;fF!l?+!ZkEtKJSbn$ zpdvCTzYHqGgR0cHIJzNR*Ra-SV#0=Vf;uOlb7F*`1AX!_OUW0TD(ZjG?tjScf7olq z>76_TB`jTPLYHynyWm&D3>ekmhEYZL_Yjb%k+Jk!&@J^q@ooFN!?OkpYBiLCT4z50 ztBcP6>Z0?%y6F6`E;|3K-RHk?4NiQ*Iq~majf33aI`P%3agZCP-yD~6!6sM1^SNNt z^nxb`QLxzs8eSYk!R8WI%&tL~J*K6vz3znE@#x-NR6wme=_K6B38$QdTdXQ<{c8Rm z7{hxR<1$Ok`I<+)?-7W80W^zhrfhL*Ee1T zoTT#s4e4u04sS$`a!_AWe9FTDx&m^C>6T)A_GHk|+9QCpw*)}iZJ*+40EzWS!?ylt z#C@Ro8s!6h!ebq1ER^fOy7A$2KVNQqxPnlCk+GE*8ugXNT1I#Y);;H%qOj3IH$2-p zFdD2IvathHsIh~Hlw&mEl!G1U5!%gvn&B9b{4>oh)BF;b8T+`YL1_C)Fa&*G_yxTh*=PZd60&OQcMe9UnUl(1b{*V33pBL@N@xTQzX zLvh>TV1!Oz1A?AO@$lruv(S&eEK(N|%S8YSZDf z)R>yrVv%VCb~8I-+nxKRh(4RV)lFXA^I3YkU+$M@^FyBn=*0a3&{?*sBtAEtUK?2B zu5kYvcZFkX+!em{j#3q7j#aww_X?w(`Abnjw?YEdXivoj#zo{X1?4ElWK)D?PX%zE zWT9gSNZ%_^eSwapTMXC<%I#(tClJl=f@lHG3F@4X3&jdEtc%#NPJA#4m&<~6aU0fE z2&~h{*ALw=+YqmTJ{e_DMj4S&YNRqGqm0TZ14T$>^*IldCE?iFdNdK&IK!M1ng-MH zZGgw=x5w#okMop0&eQfd8Rgkgxy=t$sYW><+y>t6GDYjpV9vr^{Wj(r5X_a~7Gr#t z#7)YF?DD|>+;ti)bRWbkhAR^EUYZNUX{vX&5ciLT&SqeBe zO7XSyMiABa#t`*P*HRI|Di@M<8y&qwU?`I1Dn^Ff!rkm?B93z;a4Ep2N#K%YJz^;( z23&@o+Hk>^PdH{(BB9_*D$17>yV1%V>jwy1R53C^OPZ`S*7K517#(qE{x+P@^S9xc zJ%1bQ)o8-J=^{a=NZeJV8S|!#gq$K3t|BSElE6EzlNkh~HX$aoKJqUC( z&VhUQGrbU4D7#a~&CMY#T9{pa_Z;+a9XsdX`edz88?8_`R*2>*$4sGfS|#`DxK{g6tYbr;s^F6hG=|YMd^-0@H~Rab0XO=4^#GU{!oO!G#)F?kg5OEFhZ6!$ z!WvEpIthQu2_Yw;n-juL!i}FS=HE}l&-Z@D-NE<$xx0f=ysZ<4(zE#&xuou0{&~FJ zAMi@aop|GZq2v}WsXLYb>SuuEhc%YJ=`+PxzG9&k)AwN|60I&x>eD4(LOyn^T2PD* z!8s1?vh~k=@Wu_8;y5>%2d9w$FGtpTV5BMhoc5ScZyRvk&d~{$%ii&7PD*&F`mmcu!hueXiFWM$UV-O$8PNJ90-gTQXLGl zaHZ&~aRZD=HCjitL9jcSChMlMh2HVW*@=brB`hA{M4aF3-2CPohU!U6mXA0jT9#kw z(31e&;#No3X4#cSHMQiV#ME)ktg=$#YL1NwayyI`YY3k7F9)7du^ezZ!U0hbE?iCE zu~Mc$RCCecIcA?$KA?*FwDLg`#)zzZ;JSOMO6jl$V03-s3eU+k8emU~F!qCHFJ2Pd z6putyA=(*3YO*}-=(IW>OZA!OO#6oC)!}tOmqpK*yWGHI;Vv^G>C7=fn(FPc|Jm=X zn8}2)$v7A}*+msZRBdLRC5vOZ*`lmA^DYp4P4G9t*v_IeS;FYNV|3l>TWptlbfYm%zT-Uo(r<7J5PVL3|wv2>=6SyWi!JI%Jo@;7M(j9 zG$51A1p`!pvjR)cHu7SpcxTjZD%KMv-B1W z19RSii>?*93VoOV!M!d8zs(wDiXNJmf=?!bVs>%H(DEl9j5z2TqcUnX3LI*V`Rqc* zz3xH)%e1%rw#N+1KQEk4@?BQJywjwMclvIxi+8HYm32kzK8MhVzA?EHdt-9NQWy$q zq|-5?dqd(b`o_dv?2U=LIMYi}KAs5+O4SLy6jzQSeM-qmXCm)tjZH5FZF(tW(@SBs z?wF*dMA*UKtP}5RUWu-{JyJZVRw~1>EKhM^{XK|C{&-5!g zXw{nL+XZ*9N?0O+5+k6K9egvRC~~><0y7pyGIObbYqsShERV}QH4}yLrqJ8Go%rBVFw9Y z0%b!wxHSi!({ZdVhqeuN4MIV14_&nyBpQFdyCH*61WrFlNdtENl%;BO|w0|@mJ65las6?9LVa#%#EV`981tzg*Z6ZvM zMblx_&?!s)@20Q*u*Xfl{Gi88zI><0O}=dJnMOKY(=&~9y1Zu^=@jIIh?7vb zd4`B$Iz2Ubi@QTpx41iW;1+j>wsA?_q5LqH3~74mRxTOVCHrpy1r^a0)JJb|cLMrN zgaHaZ7!!Q3LgRx$uNxl>S*TIiW^Cw#$PJaiikNwZb$he;U?r?Z)LB_+E_^U*bAzH5 zrWCZh2z-$LZs1Nz=QXZd3eDxRY_h~tOXJ|t!JhMeBpAZc;o7FQ!ukyK7xfpgAo}E> zg|a!Fv1G48$Te$jwjexxl;=iX`{-~>kNB3S_rROLzwP-xcP!MI<l!yM4rs$Ht|i8o5fR6@8$nXoVE5E`dg~0!IETUyN8vS=DNDrCqZw9-LPEwxZ(I z)3I(TG=IchsK#B$sp-V96fzn#<9&0To|TAoh@;jajtSx(mZ;Uve@1;^WGsETF@MvC z?+(^(`^;O0w!uMj?Y3LzWj^OGr01scOJ-f-S>p14qDuIe?yM?g2kDkd6LGo5m;GRN zjobDKK`r+!Rcg7f!n$=5@Kj@wdC&;JAJXfNVqq*)Mki*vB1U|5i?@a1y+n zQ0XL`=tM%blkfvhsBseZb3&by@GVYAI0=t-7GEjT?~{sU?g4CA<{rS|W$podhD+)J z!$uO7H{m#FIOV;R;r}@&X(#c z`_EIkMgIK1Tp0Ek3j=f;jVCLxP>y8#CT3cRU^{MWOU8w=ifD?F!3+zwE2n8odRegD zr0TU`yIIw1!8S;*X2#CNHl(zezDZevZA;`G*E#sMk_Fo}wqQGskKsM#i zrhM68HWe_wTmJR$Wz)2aK2z3dT(|ayyMvjZoX%DJKSRF-;9WDTs$l#fAboD&Tb{Dl zvnv3x!&xqvUE#~!R%ZN)KB)a(wm*;!ufCKZ?k{uI_h-hv>LTL@1a~t(Je}F;tNrfa zu}QP);I)^msWJY{SM}V;t83!Msz9#pOU9oB%D?_f*)OQ9YWOD)sxNz0y|e7qve&6( z?e_+!;E>~z!9B1#Uow+lPxs4tr%M?VEv|YwI{?>I=~xU_#-Z$|{Mk?WvhT}&DnOF~ zG}c(UXKGpsL@ixa-W%c*QCVYHmi zrW%Y3v#BPd#g|Pr8!fbTQbr5yp?0H%c2JjbLnzzS?W{}N;*QRK&n?#kji2X(fv`s# z9e1Rv4yum!Y)@2mbY*+0RZn-erzYFeobBngoc#9X)?qTle@Bi_DgxzdcfSKxu@#ml z;Ix&s3SNZ=)n+D>^Xh&kUNZCHZvtFUKBPz z$j|IZVY7txVP6WH+1^;(DQsqgWAUi4nJtdRslsM9ITpVPo42^yddk(-n5(TFuC{i$ z+S==C>wv4R=Ur`?uC`#|n_c%HL%fH*_B@339MGPT3Fz`0TuS-{xE|Ks>ax5@bXi^` zXmd$#Xn#ppXp2cdXqQPRXroCFx}s9CF@F$7`LxC6t;IuAwK0E~Q)-B&=af2Q{wN1@ zugs?CaNWy^q2kY*0PDj1Kf#Dz zZw~(~XLAH*Ih!Lm%UQ6{EN8*Ovz!Hs%yJeiI?GwG*eqwk; zt5yw1kn0BNm$29OA^!6p80*&JbvxqUdVdv$d#&Zs{P?T)}32mZp78BT6x4yX5QM*;+Rjbt&tJP6%bv?J5 z{o1f@aP$ot94&6}zTyTSup4~4-QeM0>INU+2Jh1i-m4pY;0+plpt!*YiyM5{ZtyF= zv>N=h)!@V2;DfrsO}fE{-=M*V-3?|pKY~g&Kgz$4@$VD-yM=z&Z+?n?*KWY?yKuGH z{3njzRPDrXdWGXR{jv@5n|^S+j^9-6j^9*WB=5H4O#+K~b;oR=HMf6I<<0Mki;1h% zg!#NYe#ExvPhYC`Xzvf{5MnE(@YAlAbZra3@a16USeT<#Pasw`;#B)@v78F+ek(j| zMx~&6^xvk-(0JAR9WP0H{Q*|||M6+2>18i1_ zlvz(hOtIE>#8KMz*-G1lJo@bp>p}RYj&cWyCPttAR_{d?MJT3kYPWfwX38bnxz}Ap ze0E8tV-N&)>O^lSsWvD=DRANhsIW*j85trF#TB(3s<#Nd-}aOJ=~P<|u{MhLJB zj(^=)B3^ci;|Jb^abvRb|A@zqE>!+^cuaPqz$PxxivlfNpbrK9nG38yfxqVhn4|m( zJl?-nkLZu_#|P0mC$;ghSla?A_L|F+pZFCV#S27`!Itb$o~>u4l=Syu64Q70vyvJ$~TkUc2$ ze!!;pS-0PHs3hNG=lg7a0Qd|u{mGW%AIz*TZs%9n^;MpwzB;>n!Y= zlp>eFCYEWCjqG1&BWO7w1qUT^f$n5f`2Yzd0RR`y`KNWjOOBaCcAo zytPlZ^1~eVb~(*m=-%8=pVw`Vmvi~D#ebH*38+5f^TMU6fbC^eaLJcvkH^fvSJO5Bap!k^UHZ&$rHEPI|r?472AWxNQXoo!c)evSq*Bl#U zeqQ1Df$Nrxx;U2@E3(Zx0IPNG0BIf2-^Q_Q>5;v?ZyE;+gK?~H|h z_`BzJ%-jmUa1-lKQ#-2uSY{`)1nW=I1W|Jf#2|Y?jO*<=28P!ju)p>Q$BR=>&@xan z3qso4-+~HOx9mB&@{wgF0>UmY^narSzb68ZcezFW@j3m3m{uLk)d#YdjMw|&qBvX6 ze%059vxJRgd+M0+P@U~bF#Dk?+tZNkNo9MwvOVotuqFDkJ-ykU{%p^RY|lWpC!OsX z%=WCxGJ7G*Y=!LRVg4QE-zWI@G5&p&f4A`O4*nhE->3L@7ylmM-@W|%JpVGe&IEjR zGmZ^2Pc`VSbXgNSH_EDVt1> zCs#1Jd3-!M0Mc*^evC!wX%x)ME()e2E#%AcIfVmi_I(hjB-8BtDgem{5yK#i@eb)p zkO*SyJwybt^&BRG*m{l-K@5uA4{btqIGbWcF|fO`O-PG?3E3WtW}Aq74;jfp4DS6H z+bT-o0W6^0CsI+6uyHEN#{{dzt_>6g* zn3X^&4{}PtnD;QJ1daJTCh9Na4w;DpdR<++HC<^i@*tgV`Ki_Ei>*#0bDTf&%ZJo; zzH*)3^DPUuVNvhrw{NLI@41F7^`ML1(aZVW{6M|z<%fVVzmFfNOg}$_vdr?zZn%$s zAK>3j{QDsPKFq&hXR(NXGa|mN-wa1bMUEZg+x7Wd_}jC@i^cIXn2%@7gMcpcY++;! zj=Re&*t9pT^Elm#6Yq{v>RDu)`+~fkwvY1cc6;`ON|S&Ch>a z^vy2&8-Mz$67osPNnWa~t^h-Y~D%~%o%Vw7DL+PiAOLt4@S?LH$4{hbrTR(Hb zwQIVtg*W_&H?EOPNjY!&UBdo$m%f!sH=}f~RXQQ1J!dLihtk(bX#(1i@8-XJ^5RlH z;>BNB*NiPEaT2yv;y^6~dgsaiG#w#OVC={>m1kZ!!KW#E(~0cD{P@CSa6^Bz%(&@< zh6_z)nHNsiZYAhP7n)g{j0=2N`F616vB7fp101(zc2;-f7Uv5Gj=rxpSz&lrhNopW z=vOQ zydTO$-SQ*Hl^NfRKr_K_C;A|ii`-Fm5VGmI+$YO2KR#WXUvm|}=(~9}P|da%r5fKq z@I2=4d{N&NiR`E9Xx_HFBp7R|$CEXfz}54Oj>JqMhS5E#Qwj0KiP24t%P*NpDC>;&ddee;7Q)s;_x2!Dl!_&2< z-G^@J!`KVD4}T+lSo?zR16}%EwZF-(X*b4KUyMe(H2}7^2{mc8)zK}@b)v04S1bL} z%15WQ@^-5g!YKOCO0U&Q|JhpUcUoD89Y+mSqoMa(&=GANKV00_*ADB8=6M^_pcbFz zD5K-6-%gF*|Gaa(Sna;#YM1j}O&=<5`mo*fpHFML%4+(sjnbfL)7A9hH)#5>yJ_uz zU+{PKvaw=htfRAhxM`MK-rvGXe%i5yTk0PyCG`)KlKT6#IeU}M8ELjTBh#h+r5%y{ z?o$7aw$y*4E%mp|_NTd6@yocv!q=-Taz+przFtC{jWE8ZXC#o>#!SInn&=WwZUO56 zT=ff(D7irTq}Mw#k2&ZSvo3Q|P@mh2Ce|?)TgD z{ebP$X@zaGpB9^arY*L+ycsT&wdi550hV%@EJcfL>K(vf&){5uLn3I!c-42D0jy(dtJOAl97%$#Z7SDa-%mMcMjz z{{4`{m2h(D`p@!*N#!f@h2Okk`TwNzh2tZ0RwEO2gI`5rJYP?ob-rjkpD%%5Q}+;b zh3)X5BLS=D0Bt~$zYe0p%kM(1JvGT~X7H5H!n5pbn|Y{b^yk?^dXYdo8OeJbox^dP1t;`fRz@%^gN zr;?{k;e9vyRr0hceB%aIZ)fk>sxfZvQH|ohtvkBys_Ad%`6y&D;mR~vByQq_bpi4mxy43m$+7Ps^#jE;%ioctG^SH1C z^Vi`dl&0}OAG{c7wfEZ+=HM>^5|Kll#D3L zMhB1}S*p53te zl5OGqCH4{|E^gBoIi799>`hRPuXb#kzQFNk>&`B|o@l;mSY_C803^07Vi368Bam~s z%eK)r3aqd#3T+$1>86F1wnd?BV>sQku$oN^D`bCTGAemba%?HsG*wIJSX9RD7jj7}M-SW*LO5|haHf7l;1A(-ipfkX?e0DUkq|ovt(p+Sy7q&c| zsN>5$hql64TD;}Ab7#Ydq!p3QiSbzFv<^1u#+zyXyl6Z5K}rovE9-Q#i6R?q?21a= zFl`L>?NGzkQgQfAS7Rw@gkCBSZB3~Os|(Fm<$7#&;sV|o(?jtGYl4|oF;BWO^R#TH zc>=}ZwKh;Vka^nj(##N?Q)&Oy6b|Ee`?=l^`Q!OQq2KfIYl2{X>wtXYa}Lm@F`o1V zfEJAY=wi?EGWhHKS)p+EI_B!pr4V~PFl#-(jq?V?eN(m-B)$L+kF*)i_h}m`(>`DX=?M=;!D!j)OH-7EwKD? zE^z#DE+}?k?PE_3@#_%h4uwk$NM^z{Ds!*P z$_-;VtN#_(NYqZ3sdLmkUNi!@J#9RZaKTe*pm5}NuHJR*(V4q> zJTlH_v{#+tvBmWI9X8$m*X1Z3HkLd3~ z`)#H*cv@SSo^`ueJ(}A4=6kfy?$JKAk8Nxvrjj*>@>XHGVkaJ*BPt;7i@ZozvDj^N z$mKR#c$&(^s}=WvGr=1s7*|tbn)~QFbtbMYc%2~N<>KDWc|yWl{KGn%yOreVLbvtsTtW;;+9)MA7c*>WTtz4al9YYIRTx+&J8%n$2q>3 z&Zv}YSnDNK*mg~oV%MaRj!%2f#Up>Dlt=!$5+3<$N&sorF%f87G^QHCom`kyz`bCM zW%x9f5kTcfCjS-u;7v3II z+zv$_0DEsY7=1-Nz986pyTRxy;_-#lT$_k*^w~swqu*?B`ERuh1|zn?U{nkS^If~c z;=qS@r(?Mf$2MWFaoYybFE)tz?@s7W z@QSk`u%*qsq0(FAU5EDudQ;(aqSDm~zf~mU6p7kJEQ|8EZME(fqx8(v66EG0A=sF4 z(J73J2>2RLOQf3%gle0fJx!kudkc(>jtcg5oYcOKjYV;BHl%g8#RZW#9iox$!3^vX zMhBK$yEdV*?#3wh3V%a#*orFwwXj4~?}w%e&SA?gI2_@RwHFeaI31f!dmy1{5+@{< zubANy?o_OHirk!7eV^gjtHcPj$K2`N7caRp`p&+X_;9jWwOWV6Ir3cTiU&$pyrOi) z>GI*M*LLbW(;Y;~ChAHzktp2+yBIb-%Jzz%P_|e624(Tp+^%ng?b2X%YKBjclAQ{b z?o_yRr>sWfy3q>VXrL+TSf|}}%sXbIwygIVuRMo)BtGy*~M{#nY9UoJr4|;4Lsg8H7$xZAa=#5<= z6KS&Q(5^$HTwHug5fQQK49Rx$7M9s%$MNiDJh+!Wc~ItB{G8}WKD7 z$*lF}MkVI!HWq=?SfLtwM2RU!7kZ(dai!9&Pw#u|NY!yxactr%{;<-*etv>*Kc!~} z%R`TBUb=m1x2_A@6|v-_<^+*`t8~YOKGX^P#WpsSKcbO}Cfa+4q|PpSJZ$k>Hd#Yz zrpLTIcGKfAi`BF#W~&!$ik;NMr$>EfRyx6X{j_+x+Yt0)bYlL`D#hqAbc!MBKW2?8 zagqvjnfqCmziMjhq=e|DVRRjuP@-(CcIXjJYK{~AZPNP2YP&mc59h@hm0EIgu>+i@_vmnJmsR8`r^xe8kyZ2xt(d9ZI_|GN-=iEW*9I(> z2YGD0uyBL5OV2&aJ!;Q#uVzsPTh&DFKJjt2yS0B*_{7mEi(8H$yboI7K!Wf-Y{3K& zk~S!50EZe~C>)?!#EC6xRUCd3~@zO1}n*eLrr z+a!HJ`?8%_1@uDW3*0^!^8+&*XXLFRmge&oH04xEKpB1gihI7Gb@_^izL2SJU-o${ zOnCOXnr_we*2>NRnK2(KF=h-9154v*#jMe?|3K{zt(kYR+fA8{i)uM26Tfaz$2A#L z|6`@N_7xVYG*6>S7T3PcO09EJdx}&z&d#s@;CBjz{^9BT`u2}I{Q5(CFZL{-W$e#i zVg5((>mOu(y~HOg#jkJnPUqJ*O;2(7^(VgjfBE&E|C9Lj4c<4+fw$-d@$vI>aaLp` zdCK(sF=8X!lm@0Bm_xxvmzX@5maxww0u&nRwRZ|$bk%n-%G^_uE{Jt^Y*2hADOlQA zh*XV6xOp+6d_(g>ttjH#B(P8xO@g)P!&+&e!pl{uJh^)0ta?&w&Gqs1AlhaylTMZ)prnGE&}Nk4?D)Flm%jcLL!kW~sx=>`P<@Kc>aakJ<3# ze+7Qbt}Xuo{FpbzjmbRSVDUPv5R@f&9kL$H)ATo({L=oM*A+!^`j1oH@H?s-0in06 z<+2HDZ{N(Na601l#T?N^&?CJy(Gj~yCAaUl7)n-=3Re+vE7%_M4fVx*MN%7|mfAdx zaf~q~QS5Hn^1}~@OFoe^niJ>hNcmi2A{^&Uw@tZYBE;xjq$4812`S;VTN0cwBfcC4 z>d3!ZH@qGJ`hRHG?|B5a%jD@UmC?l3X%QVl1VBZPEM~pB#sPN=Gs(R;>WG`oh8(!*#kQe%pqaTu`~!-$`K7|POf z5~#h+)^f5`(on#9;GH)t>HVva5-9LBIt8O z1ijq?k{4sl_MDKWs-l^L2`=@vE`?r^B zKl}-9e>U15u-gB|Zo1EpOAoZ;#o1f>h+5*_$Zd8GeaajQ+p3Y6QuaS*>qqW?{6DpR zWRLxf1}p{1kgXsY76r*=E=|WbSpRJ_|AW)#znK#9uY5};o)+SKEJW4BnbsfV!0)#H z(v-0wb|!k)p=D#wl+u=dqw#%eD~)f<^zp6#gtNZ`-_rDa+phnB{e66l+CM$L{k_sY zs>+W(rT6!D$4j?AtyKsAEFbCgioSo`#y>CkdRQYp4*q$3m>0TB;qO}_@_1kTIQ6A+GQ+8*Y^U9< z^uNM>UiJ6x{VWdB{U!FZ-p2OEF0r5eR*?|o+g72O8#-=pXzh?b|H@an6Zp(Ukyzb$ zGx1~mXNVtT7qJ}u#%(viesKew|CDrMS&@Hus}DBa7_%u#o5q|@TKa5?(xx#>la?`? zwA3LGrjd{q|LY0LYJ&so+*2{|yZ#j?C3l0Tzw&Rr!R2Mk1D*NDkN>QY8INU-dOvqW z0?uCO3OHLnJZF|8nm#x2Zm_f@HB?%XD!0BJa3y33(y{rp2>l#I;=fzF4>;{^a@u{6 z+dVhQ@$c5|!%n-8IPEfTXq`jlpIM2))$Y{ZsqJP{T%+e9ci<<+y^6^DFq8K|We3R( zDLY86WnNIpq*a3xV)Dp6&dTehlFgtOOLl{`Tt2VEEt9p=b1REy&2G@^u*zhr^;F1Q zTa%%~NwwSIq`K^IQr%8CDVKf&7=yyi6|yW5Sne?)IrGT;g&om;DFeHR*w(R?9j_bw zHC4b~rPmdmo(Zg?m_s0);-(O9&CEZM5Wx~KiaDA(U=)OL2r7l}GcNm$9P7|j6>?9+ z*b8@A)!0a({Jyc=D2q{ZzC_tr*_aykvA}P_lCBQ>>8rZ7wO>;+r8=Ky%lJ1=$^yiR`>h} zjO?C#`RL8?E+GQNqGs0KR z+OPJ}LhPNgRd=)!jT;^{2Vy&06y1cA?+)Ixuq^kAEn4s0SD$&|ET5*%C*LP`gkIQDKB1(n?VXu+dMikZMgz=%Mf= zSS1s*|MpOI+gXIYO|y`OW7RSQj4mNvIV?>vx5Jvo@h@}r%hW>FXTqB>F z@@a#-9gw$_R5VtqqA_+P~jipMD%>pZoW z9mP4LoU68nC_q+<0>n#f*y|SDc6v>j3`Ycs$SJ2 zBP_#4>&|_EVP}95XMj(v) zyVsnU+A(SCZR}u$p13|d>>!nqX?+MZ=b}P~Mtq}z#GHY|oq<%?H<3G%SSRiiD~ecg zc4nt{IT28q!)40>^mD_*I5(+IT^N$O*L{PJF6&up>j1og{Qq&H=hjZA=Tbt?l>?1F z^r*JiyJCyOk8-Fvbw6LY_p|ZnfVv-7a&7}ahBGu?&k%#pXZ)~#7LBD zt((fhU?liZ{@4`}htU>No1X&Oup>~~8G#F*7T#KsU zE)j38V)15;9q**;^Vc(Yw>-qK7LpJ1&!%)Im>#Ej z9hE4a=4PA;_E}m!G7Fl2>()DXC;<{!UXOY`!XvNAK_77)Zn##>G6$BpqE&)eb982n zQ~|;airB~;wO}F>=%xehBU&Mrz3AvDht);4$IZnu8#^{-P>IYa#S#N61(M@-Eitgt z0$O6=Gc8&Mt@%w$bqy?ufGO&5dwHJs%A8{E%s!4ChC%wZOrws{0&X-|M_G*OD2o+V zl*O)+p{?6lS58c|S?=qF|nfm$7OVuS{6uS6kJ`Yupf5(R}_8fgbo-S{r zrz`RFo>lbp4m|B^r>6_?lT6)4#Luwdm40AXYOmjGZwcPi{P6+|J)B>rwh?s%%|F$ew(q- zb0t(GuES6HNY(@E1K*_C(*JzF6`SCF|M*Hl<<-r&Qq*zP=~taTb+HcW@eX}QF113P zEcO51;=IiLCg$I~k|T@|a(;O`Z|H`}+40V+gLVYc+dl|~-<7FprL0g4QSbn-Y@e1p zCAllFI64cS@My@b{rB2Ku6>+_e4NJ1J)j}e$~4Ka7a}^&*wil&rIBH_S7ze-M?^*X z?tA6hlX=mpx2=Kmcb*_CWkA&jWTvc?kE;5h%$JqYr0PR5YgWpCQ}tn)J1Zro>LW6P zR?1sdeN^UAr`*2E*eGi$^?-0^FS8Nq9X~zwU?bEE8=>Cu(^C%)LcQgu zr@q)vPnY=V=@LIZU8h&xCo^}w&z|(zW~MS|(vxUA7z?~Y$LoAo4r4sm2TJ5zz&W83 zIgHU=A1;yeea?y4Io)>7HonA{sr&i%zMrr1`+0ZX&wKZN-jRBb{(l#9NR^qd>6X?p z>)|!2$NXnL6C^PvkSX&$wKCZ z6PewGk+JMez1hXx*@aEzqoZ>Pt}RR$O#x$ZlM(p5Q6EEos(kpCIjFH8HI{Gqbs37K zjHXIsalhgFypgCex~h#=%D?_fZcSNsVV|+E8~({oS?($q_KF|Jzu%z!IDQKiwd6l3 z@7aaD+11^~PSg4x z7jcIrVyBZqaBr?Olw~jf*5XdB(;@2nwLf>=_c{iRn=+wsI<`G`Z1>8sHNywR15;!K z!=9Lj0|V~?2A=$+V&H2Vhd27895 z9w9c|;%YYk4V%ORHaihAZd5q=a#^YI>kqtp)MfPy(Hv`dSCUk zwY#);A&fVzJ$Wi*@)9!LCwO$rQh=|1%s%n=-zXN_a?74;5|6`-78aiPSHRtYq}b93 zIP1D^3k4rF`mSjfTT+y%;zk4&d|{`^zqPx#R~`m7lzy)@8uM4)NwIxh$-q_xFjm$rQF zTAgAW+>#)e-O^~n%-9UKAlOd3A9~IG5F7qpl!zf2yqk9h<;LJadh_ zn-0ZthrB~!oNH_`s$m8l#iK7H8~Ym0`xP5;nA~=pdTVakFPd_j!bso$_E(nazwWy4 zja(N^KWu9X8prZe-5WTFc3_|8Dy5@h_ddh`xLLEAOaj=}4d5R9M5G z)o9Nc38B zw_r2y&G)d2Uu7SwsQqFfvxyF`Ft%)=f`+I41#DsQ&JA~j-P!8{HrHRq0vA?K*6_;- zJV(zig!7_U>J+|V@#XO7YZ^%CfOeQCFB7=X1}DCic@Up*C@#n z6zIAQ!=g6jCU6@U2D8xV0laQ1E`?pLVxz-xJ&AxElBp&d9E_CPMkP}IJtJi&{Kj~< zv@adn7wxq(+T~&wh3{VBm>WwTu<%j|sb+_wbt_>LIspAZv?S4PiUP@E^DwF;qTu|< z?=`7F5QpR9;RpbPh$|7OZYO9nHvBb zMhfusT9%O1HA+bKqxD2*Qh`u&P9P5}Y#%lj^MfL8SJT7AtgtL*TrTF*SxI1j#sv18 zS#vSt4sFN|GyD+D4i|HIxyAgmd5hVG!F*{k!+*$PhHyAbcsS_0WNs%~x7@c$BF=rA zH_}mrK11c81rUvaXp;oQ_cj0^^5Tj)Kv;l4P-IpDVgE*n(LZC19^WX*?5O}?#}}O>_sc|y3Mt0%-+=@s%aB0d z%GT;<+r%5%;0bMQL0kKrw)U7f>rupn+CAFZlTsyA?+}S#&DRG>j4))GN#lo z^o95)rT2_%toR}QtUm^50pe%NN!Q3H7O5O4M=yd$N>x#~`hqe!DvoLO&rQ`ARE~;c zuo4UR7?^mx=JJ`mYXVhbNFUprB6OP=3Xc-7X?bp;`2I!~gj-nOx#1f`8fBd=lo!^h zL+M|O2Vf{;+l@q|ezEKX!e9(xklj_G!!+N}lWslf)RPW9Y1h5Am$=5i2IH)L76IEI ztQl%_jbeT-04{>_wJ~hS4saF|F_b&)n?=j9^tF0L-l1QLI+=uBUrjj;6Apc~L$7Pk zC)|2nitnz}b>tHs_`BMP$sX}VpL~&)FWmYt-QIqvAa^_TUo>VsO#A+7#M%|cXCX!Q=)#Cfm=8wOoA z@hPVOniBe9+9f71v2X1%w`=?`?!-rlnEYIVnUNJ~Ckx9Ye z5$JfT{tzXMIWAg`=)Mw$J1gR0Xz$$;N;SF7oQ7sH1pM|F1h9PZKy|TZC@}PKmZuPZ z9~4W%rqm}tiGj>#e3G52s2vJqo(AxV34-I69}q!vYxbM6guoRbL5r3{VwnSk@7i;< zWe(Rk2Av{zcxDA27+YBHSs1cO;KF!6rSzkCQ2S%=aG?VIzTFz zOz3s)?4AnJLDo4*BU$GlHE|t#16PnT*(N>pWDFb`RZKbU;kE8ON1(x+yLpZwfpPA{ zM$CXM9T);SH$I^dFjI&1u zG_57vDu~g|^aof~Z8h*$6uuW`U$1bDY`2r88CAyBFN{v&0b1*$X-GL7K!l?ck$V-9 zxsh^r6P*#aP;Tr@pbBV>Er0fZ8&UTGt(nPNsZGFTKe^tT zZio`I;i&;>HJuqIIN8wO!kIAFLUQHvbY>_kS8kCv&_K=|ou=d)rhF;KWKX{zj@;+u zHWR_v`=v{=PZeu2fk^torowN&>b2c1NzCj*cmWMHj!w%jF&&!qOAmaONWY`0@B_AC9~T zLf#8CTs)CM{J0%QjBGFTx6Fp#b80rUWrR9lPIT2=CiDR-6B@OCAoul>PT9PY^2chBGk7u4(yGErkW`8F18k@((hL52$6eW0j1B8-> z6yU%S+-Mw?z%}6oEch8JMju{muFr~YwlRy)FlSyiLY1yB33<_rPQ=Q0$4$;dQM(a4 z&#U}{qE6-CiS9wtWmSexa8#IXpVB*WM0pjtD@=bT=_APzr@Z_^2IiA)hP}$o5MBBV z_r3MXeUI`iN@F^Ev%lvDDg*@nk@j-eNW_7)$qSo}>g)&)wPggro2|9qLA9xfjqG<& z1P7?Sn``5r!>abbo`tp1&XgPB=}`*o+&H@`lR=VA)ueExz#n6G@ROUZl;lT6HcaxN z8oP_1f^4~@03!3FX_QUe>!!rLRr3?~1}T(u*ht)qQ9~%2#xpr~+NJ0p=r%Z$=g~_4 zU&HD9fX(WiY)k-FZ-zb0Hmljq4P&B$oT&I9>)B!K4DmtSG$$jjNh3P<8%}kH=Y$C9 zr%A2nN!`QA+{IXudARrI0pvhaWXLD0w`BNa^=6*WR&RO^v(;OQ31s!o^W}E3dh-PW zwtCZZl&#+6-;Y9A{yZ{#^TiC~stSi=lXptO=A9D1x<+TV-IQuurLF~iYLW;n13@n`%(dlbP` zJ*{m+yNtFC%`(-KY}wLB>^OL_Ek!1qrzwu^xA`3x>KisKy~z!)^p0;k1$`|3!F-Wx zTIjm(8NK5>wvu-vcBFp%#yRKBR(hku8+&PcM zUl{vdCihP2W0Pv%kICfT37v#7(g{f=r1Q-mZx7GtVl0XbcxooEj~N#@{(riKKZ0AhQjEnG zD4F_-FIcwk@jfX~$7sVMTv#zMhL$#MEJ8cDdUDp0y9a5VZL`=CvF_hk>Hdu@P=#~$ zFTMDOSlRt+c{gYOmb@w1zt;9j_V4@`&&>N`&i*ZVQ?h@pjm_P^5zGFKS@&;b-u{i1 z>|fS>+K+BrOo<1^ZnkjvBd`m8zzTw(gE@Qyij|oMh3_HB3)oa(UgWE?T81=t`7x5}%Ld!!RR@Wi z^5zrbaj3$Io8bxrmE~nAMEhukfy%K81ML&$EsDkpB?qwpBweviE%e+$x6P#7a|c}t zlcLWZblFQvKX;H7=RYF=@pU#O1Mv+q5QF&snUsXasdc6~x05jR10@WxRAd!~l!*I( zx%h*m6M1^}^#FxPZO=W5tX)NeH5R&HQm11k&6*z&)^7%c4VwXBgXSeNuewC0@R^;_ zO*-wi(CI1*o%UMjbkIVl!xlR2x6tXNL8tK!ry|l(dAmmv=_->*drcx8G>LTBB+`D9 zNGBDMW-vTr0*5GcB~s}38(tdYpS6(Z6`xfu8dD7pq`x3dU9NT6fgqnH39@LjD`MQ` zG%0bL9e%Flb)LOdMivC+hv!e z6<*P|awU;q(mOnMmA+GimR=uGpFT7H(>~RY{O_BB)XRxRrm-e!A3o@B8o@uM#m2 z6|q1FS8Q9<;Gbejn?P=AaVznmeVe$#T+fmMy7R$wTI}=O`JB!CP4R#j3E)9nT7;E{ ziFPv$KmDg=oJ=mLxtT5n;{-nVX{ozGHuN9LT({YxuMvFihyGA&ZJzk4XEGK~R@646 zlwM%`^%nuIb4Lco92N7eWyEU`De*1?E17m z7Bha<4Sae=mj&O&S`_Kb$zSM3a4noJR`zmu()=t}I6QlJ@8LJ{M`r`__w!Kwc~ytr z(etbw^V{g#kV&?o$}62u(_I?(kYA7!4sHbDd=NuuRbuL1spv{)jzINCk(dsBKOLXOE@PU-=Y9^+__S_i6P7t^PT! z{+L#OQme1h>MOPSpjIE!>LXfxSgVg}^@p_jyjFiitM_U3Uaj7*)!Vgtn^y18>fKts zQ>*t>KU+Orb0dx$-N0NE?{T3C0jH?w2h zx$fJw4`L ztXAK#@LAE&E$o}M?P!f#IMAVOLeo}uXT{V8v}@u2rZ)UoJ}ul=+m^~a!KOfpgZ5<* zxSy?|u?;Qbu?zccs7nkDeRQ)(25WEnF{=*guSiyo9!$TZv=H@)J1T1XLz$=0c6ldF z^heOfS7SpLiiV_cZZ9mEEf!Hd_|;hE1?Z6KXlwh2K7+o1-=uEq`$cFYB3NiBqTshY zD3&&^Y-=qXJPf-&UI|q3og&&OT7$Jq8q?>%8}WGIr|=Fi0{b2cW%gl1%leRNkhw*x z@6X&O26}Qs3&j-X$F4u&x^E9VPD>~k(l1?R%MONY;!yg6>|j+f?G!Dc=E4`ciFg71 zwU(S6B-5j#NJz2X)TNawCuR_D-;Kk_W5JG>GofTp)(poldl8eMl zzVL473tGfQhSlW*(168BLidbWJylMK`aU4j8%BsB*X}^)`DOjCiHe1IKC%KYvN8Mh zkWCxxn5s`8@LUs*;fk#YyY5?-5N*+%{U*^Cd~G9Jy>3G4E-mWsmR7D#W##HTm6faW zR93FeGg`Si(W2gLZ?@RDzA(rZ^<@CnW!++JNHhe+W<+GM;RobZAhcyk6I-?V-n0W9 z6?ar1kbAUcDdB)`0HL-bvTJJtu8FThkF#e>db6ytc3Eohi?#aTJJGu{vh|96)sxz? z#)WGeU-`Zm?9k(Ou`HRt#xCju`eJ~4{v1G~>`vJ1n3gH-$X`>yzs~$M$MCN^f6WQ} z>&aj9D&DfW^`uzVE1bj7q*xKGCAXQ3R=YMec!7YywIr|Dmy6t1JxQI3HrPC_kxNke zXh^v3t52xKSdq-xhySlwl4owL$myobaNZ2JL>=FRGyj|!%0S(tuvjoO(X8u9cC$jb36~yyH zbVa20P1yN>-YvAHH_9#T8n;3D-Gmvy?*-CSlyFC%bxk}#8=^Azsp8KosCMI2eK!d$ z^&L46xq!ng$Ud?xBnG+{PGtvs?YY?S$z0<{?Al-=^9k|F75^)H+`i-Z0w7vB-#=<= z6IfrjE!?SJlHK8^hgGlgpV#qE_IQQ9>g2!-jHG65V(=npVIZOE+u`lN%kWA7#jbD6 zexbJ_ca6OQFnOvXx1w0P4@Tx1zZQ3YeFkQ#7l004pE~@KzP7_P@o{A5^&K-UG4vcx zbc@h~$`fgfY}e$BB(~hc==0xcx4ulo=h&@p{n{(Vnfe&w(3sV1jq5js*FV^Hy*9w6 z>$SaXxnA4NhU>K{bS4r>KZU7yt3J+;@T zT;orJG!Arg@I)^1Goh zSI%{Fs<7*M?8bAtZrs$(->oXsjR(Y+sd~4y8&BEUbWk_`({Z9RK3sI zjlaxn;2X(?UpNZgt?d)LbN$F0t0#fzWNE_>^bS=Eft+skTqzsS{sqeWO;iW)I}sUP z*cL)EL0^&l`7nJy2psc!NOxQM#73&R45W=sz&JWXu8A-42FqpURKb^NfKn0bf z)9F>BH6h|j*M0w_$2QxAhK-YtTQt8^wGW8Cl>R#NODXcTJHHg$2P|LeLHhZS?t4fq z@14_reN=~sBly^w*6p_eGU77}>QJ=>#?}sBX2Z73Cs`hUZ5VYZj`YR7KZb2Qljn_G z^%c319dKSlh;1Z5FKb^|AzFGh`$Mzmi2B|s`vbHomQ{kpl&x>K18;WS_Y}$+Ff!>H{Vk2wQ~T82(tm4F`cF&z5xm(ai!}po zbgvj`Epe}S66WKmeF|?prQcKX&4o1sc64D_Y54%Kn(8i`U)P6q-(ONUN_8vJmswpM z-cd6UK~IO1Epi)$YNz-L%aaSrUt#$vl-CSY9sO&1e={H65lV;Nhfq4S=m`8f^G^I% zKR&bQE?aVupo^-blhv3;mSrOta8rbqciWqVmSWkGx@1SSf4xt%nfx8 zu0mGrCg&!Q_^*TAbl7tKY4Pj!hr}=*@b$GP^!gLJXS-+$i#pg^|DxMB=cmy>8B?m@ z;h!k|5?~Mcs7fUbkC;nZU*g0^C(`>RZhUkj#b4sVM-RFwVd9c>i)c!SRY_|5+rV?E z)tu+sfHZ$-+xcre+%n!nCy$AWTA1DV2Z|()7(|B9_BB9TnEjVyLzjxHfoEl!;LL;9 z`fKxi?D=vm^V{mzswZo{oPsK!>IT9EEoeU^AI=}5h*f)aD)U>!*C$VnuTK?g1_@t( zv2w0E5a8>RW$|^8@b#}(mg4J^W$|^8@b#-JOY!x|a`-w(_}aMAz}G$OAz4Np1eL3h zM_j(Ltm9+$u{0}hD4uDk72x>8d+7M`eI(0(yeEJviDA0FX5X%Fpd8}Kp!n83Y`bvi z8)V`+r@p~nAyv0aLWa_33B?XE;+ZZ9>!lHYzRcq{q?2Ewf}3)k{pv}d zq-T%5lQqE72@k&Nfx^{Q*a*h$SRKM9df?USu=plV;eHXNBs)LCUzAr;-%B&qOnm`v%U|0T{s?$zD>_|LNJNh#Xt^p+7Y$9 zOymCpsW}vNW_O2nmHI2Gct(B1U>$dldv&7_rHsYioC&%Y$jdjJYsQL}VIN@L}< zmXR42>w@{0a5Y&`$5{nj6Qg(=d?kp%4;f-G3BVKMRxy}e#iKsts2~PM!@y2a434$~ zAJwBN$yGauo1z#T?WY%g@hlBitEKzSdUTSUU0-v{OWBI%J5HQUTI z_O0!MckA=}K*m<_24jK#{65_<+@$G;412nsevlFlJwh`WSO#4QIl(B&2pxf>Kyw^r z7?qZg6iOjRIJ9<@MU0GR9W1b;){xCdFcb~ra-`VLw;P0IwgE=91eSN?pGGhBRG?c+ z;AIo$N_DQ$J)|L|4N(`;>#E@BLfOW5N-Njnhs_*~ld$vlsbB2#w8(W6Od+Z?6^t^3e?$4D zVSN?uk30owI*vp}v0RuO<`5A-rOCykdOV`XV`rQk8BpcquFvSx56Ovn@m?BC>}{s$ zU_0!oCgqi>H@H?j0+Z>4(M9oZG`~EicgEDDkO#sh1rWK$Z#YRZ+&*}dFT{Qi{0}#< zYvO%p(N@=wpyLN)J0t(>0)1Ls4ZHRM#ug0bkOX193cZG9K0i)`G{bgE?PjuB9h%nI zS50ESgo2Uw7VD791Nq90t2{|EYZCdt89`nN%Vm-!vYka82I@-JKA9Pbs;hjbC@+lg zn|Aqu9*txtBSb$bmsS<~9jZ(hNU+N#r&sZ<+=5t;c>M4au!j16wTc)x@iZ&RpY!_d z!S&0Hlgr^Awg%hq6-CrMe)tKBAtgVM@u(`L=~Dd;9X29!<2yeLjf%AlG#wkO%(#Rh(^&6T#`+Pq%^LRlS8b-feuOQw(quovMp|jL zA7MMKG~AD{saD$VIhmv~-*e7LU7Fmbv%?(}|F!UZ(rc4IE4+IJMS|1O%s@-(&F*k= zWH&c?B~dguFGhpAgF?J=Ksd7ZQ$RSf???SPj(v*WPq{k#6;J2xAet{D%*niGyUOQ1 z!xP<_S1EH?R96x7&_LeL27SzXhK@|sDMpOzp9So1r@Jf(%5va1d=w{1sxD{>j`Q%FvgSph zmcPtCHl~<=LOg9u4t8(&ZB7uT^cWFf-m&5Q3nkOq$0OXq4?cKD532B z#q{?sqUyi`P{!2w7Ky|?5Lr;#;kc0OPyuBBUL~p61PhTMur{LVfv}?o1R7Qz_COVb zh{sbR5fRC#8U-_GAnZ66zMhn!QJ`Rv-4oFaiKs_9piuw4`Ega=r!R&sdh~Z=?wOuI zFDmfk*lVBO=>X;lXT{f%an9uBov#ufe+5&hf-oiq78LP`GVnlXzTgME{@S0kO;}1DFq!@JCu)eVABi*`V+K z^1wzB-mRf)s>?i$?gJI;?^=R+;&?>jvJ5;v6;^Pf%0wMr=ZY zJX&cc_jKnTVa^rxKfI8O!p6-71l4*@*4b-K))^o%ni8wFt4y8DXQf!*u7rBNg&-xP z4a_}d=7C-%6!F!AyW~-!tS_ir2~5L}OU9-TKVp;4V#Ctaw_hKYuD-qc@FYL2*N2G} z%8?Hrut~SxVd?7Itq)6AUpQW#%uOYXRMKEuh`F7Jvueu2%=P0mu~Oqt2f%R1=5>%pkApnb#xmX(CZS4U!l=1HUrI zvPP+Q@=j;=Mf7*$A?v&4T?IaEuf_rQ(|}cg>t23=4tzB7y)!>w&0(yCl*r5jx`!J%TQr^cF zIrX@_YJv&F>14|}#fie2X~*wSRW_Sd?lr3fB;BU8;|O&l?f4IDMuHR-NgK!en01R| z`uIcpEkoURcrW~)cQ!%A!%%T6sCZ4NrrAGa8ztehjne{1Y(P{fB=;^1KH{rMC|=nPHye)+0p;i{-I=I#5N{iAG?6ej&Ne zBXs(K69{D#M`1&Q0dWA`csMEIy-B<+Z=7I}JWk7BA-=N;oret&9+5q=vMWThBWx}n z6f5lL8ivUbFu7lIYw>`=<0!`rdI%w;hCWjLEMM*<*4^msTevIP)Fhrn#q0gRWk)6l zE+W5>{#yDV^K@6|M<>?+%kApiUV(qnA1FM$tb#Kqxm_cO%|BW&awn>lTqC#P)Zh8z zJHqaoB>~r{u+c5My42A7m43UaTX^mtxecp}=1_Xko&^=N3pSzAxc0hK=7?Ak+Ef9b zG(=nzx1vyPw~=##{JJRG62f`^$gPNw>Gz1H%F%-ZXWd;j`e*Lh3&ql4?NjN+wZ9y2 zh^NIhp*`09f=lncdmaHifOz7oprtk|@;wkef)L9B?3P7hsyt$zX= z6E9K6(BC<-FGu!km9~KeVrt<(p=q3*!-6JCw?xafZQHhO+qP}nwr!k3mu+>~HovlN z|F3Wd$zjeicEs8rp4*23RxQ-*X$y&+F+5i8tb|<;k6uflvzcPkPOWgGjIw+CLHDN8Unqr_=Gp?xWl6a}>?n zyL2fDrxm*okd3aTfs3b|F~^JlM5>Ly*6Pk6&F>ThrrM&e97%V^UtP4nvqIL?KilMU zPmZuXPqiUmLk(--o)_U=H{>$?a!$SnOG`|hnLMJt!{bSpMlns24**+Sz5WAb09+^h z3bo}u^Ner`Ckxk=E|1hz!QLU8k6+@<3*9#qqfp>L_Ln3jZ=|Q`QxMlXy@RcNXWi^- z1#HvH3ry=vT)|$*WlXv~3k#z|{ADQXd(P_0!kV=_N31*q-Qs2~29-)B%H4Cxb~`XN(p^#!k1=nXMDUX86^xp7YY&$(vTne-z$9W1a-<8at@ z{2N0N!qzlb@JmbiTO}s@XxN1*^)mWF+6}5KA$YC~r)v$4d88nOn%;+8nRMiIO?Q2c z8@g>Hws(G1nbg-DQ|R|G1|$d$h&Kp$yuzi$l*)E1EiB~wkO*T=IuxK5QB5;U!F(5q zG@M!j{#PsjEY&F>tDv~T{CE3~f`+s3YLaCNV~dvrf|j%VT7n9j4p9yWYfdOu%NNMd zXxRpMaw9-4=GnplFn-o#F*mfdEE4t2+%c@WCW|7A?e>{EA{zeNNPmw4l)Wt$kN-Wp z6u3E;CdYBraZ~q0i@771Y{5;jJ+9QUZB$v|X&qOB)_;$r&}blIqsqj|NlJFy-k>St z>nUXU`{az5AhMLFO5G}|tjpRbba!->mN=)VvoJ}!-dIjXR1S|d+lj=Z>e;(u`!f^I zY>ANxWWgbhpwulA1WGyM`JZX68dsJ7)Do7;OKO5f!kjgW*{`krq3?fb{Doh#bPW%U zzH$*A3GL_A1|X#_K1GvL1Q}w}DBrA3s3!GpLBRmk3dq1~xUDetBii0Qt6JGolXXlZ zv~x5$JJ|1n?6sPEt93Z>+U^{M)*77Z_jT7?#o)h?6@ZVe9F>8r6f2(Pv>)WxUxad` z(|0j^uKwRX?R-DoR-c`F3kU))|_odot51AGvO6 zEDmzxDwHihmA@B6*V%`1Hl!UwHA1~f9n^q)l0=Gjj$kcOjKS|9av{_@Iq z$M6BPDtqp*`>I^%_G7Ge4@Ja>_}^%1%@e*LPqlOlnC3nXEoO{bCTp1XO=)C64*+q} zjFegcxq|;9g{-uY;o&M~ysiwxLZ%l$Ja;w)ceSK(%)Poe&07%8xLyB*j$~Ys54QO% zo;ZimS?YI|eCllM{**WomyhQ=j3f%EU|o`u^=xgm93tO@8cKE8L>g*dAhpfpei!%- zbMSLU-OFQLbgCNWJGbnJx8d!_8%^qc^@fQKX0{PCj_?t-PmJXA$taNFR*~|I?OdLc zu}j6D{&kOHccQnagP7ebT1!~8mm-X@t=sLOrMNRje}O~fvbMT-nz{n!?+5Y9l14eK zBV6AoCaFsHUj4;2&Fr171s_u&vb-rzYK7YK@%bPzVOl56TOin6@Y&e%x$~Fb=s@%w zH?bL$K|hvk^%H}E>gT|Eu6PDb&WNg)i$i4yLTgblO!sU^Z4nXY;xKsl_(A4+$R~kl zoFHpVI%ih;d_|+=OvqaG7)9&8r&3a>n>bfNqp|C>>iQ#CFA=iuISYILs>4BVeZA9D zA!aPyka>bR;izeqJYRaiS$E8k=so-=QX6_hxK68OeJlD81nd$2`d8s3hi?Q!@tgDa zkT_L;t5`GNvh*u3a71%lx~=r=3QH*qfW2cps-a_fLB_^pnqeXMk6djiYIC1N~ zHVsrRxNkL)2?n1OxN_^h0Zmjkc&C!^5FJ)1h}N=1bW`WTJpDA;m_tdS=`{L}^9g#( zBTqwKTKbyP359l7GA-xyRN?-VU{bnZ8u_uX2!)(b)Ta}l>K)aJ7!}uS*9S%1E zBd^(VYfJ;&P3y*;WN+?Iw0)ev7pMAcmTPV(~BKS zA0*oz1(%mi0pzQaS^~^<$|TG}oCX=rf1mja8%wu2IJb6@e>^DNUdpboM+VAEN7*C? z>^OP+Fg>HVC%qB)0^;^Zj^l4j>@lG~rz%ALrekgix!~u^^=mG58a7_<*t9(ZR2*ts zbJ$%LPII_dV8(PaH`PCSDvw`fTJK&_%-aF|e^j3DG`?%+#BkMm`fURuQ+E46FK#xZ z9B+n|$TxJEBK)Zlbsq^!e+!BQPkuhD11&w4fJOgvnM3kniJT<{WMYSG1R}odHGX)$ zRR1{2YJEDEhvU)5WU{QSAUwmZ#&RLGK}u9EBM{lZt{R^vdnw*rh5qQ3XnjU>^h(OX z`y^eKN8E6~2=n)I?51+Bk~@QAcGPp40A2^f^e9d{i45Zi>?YengjmoQP+N=+XZ6 z{7rQ=F>2vgTjBj@RJk#BLjk(Q3FOT4G78u0@?=P(S&yG77mw9tOwxw`@4K9-O3zF# zuY3dL0RE6Yo9D^ z2hD7Jq4JuTRA&PUE`L>v+xQ(M-Yg$4fLQndv&1ZBNQo!E5y4L}kG+)jSV&Lhf^yj` zqD^aQcU5VljLor)@dq;jV~po~fQ(*vi1BDNUG_>WCeP8<;iX-Ve4tfV<~(T|J@SWnGRC7Q#&wjTO;n<%Gck=Eq#KJ~+(n-(E{xZ3>gz}h;ORc<3-ISrple#1^& zdzN9Sc7xE%eFOlmb!vdAANihe+cP7v)#>M_ec~82mY;4?Zwk;}N$MF3w28!Dh337! zJ1`zDMGM+x_rZ)%#CMw}yW~oR=n2T!-6t|iQw?bvRiPF>&qQ`^z}D5JO9^%#JscFh z+XT!LI?JZxy!+6NKOvmf#cQlGe2BZoZ?Pw6jBkp2`&GbB>shi!@KO=rW#hfIc(piP z^!KZ?@d(7GhL%nP>%wVp9S1KATN9W&$jmua*Qp7B_EiIcW}#X@95{+;dSmmKdD%-= zTy59TJ{@6yrmr$EF5<1GO8`aaXum`DGl2}4gF$WPI$m|QP!k~%h)?KwwL83Hu9suoOGwGf*$;Xrbd}$*LR5A$8!= z4H-8i;36K}kF%j(L3`{r#ralC|q3&%JVs#w< zgkgI^;A`u^M0B(8^p)>sC~&Z^EvVB@nc7K={KBg~BH)8OeVf_~3e!Vd`&XiKx>fEL zd{xaG2{phK*YZAbw8<=E>8D|LEa?++oc@N=o#fAB*&6Cms4jlBlL&P9eA%w>y#NxU z=n>%}YUC^a{uF6OroYqAH`&WMMAkwyd=chp=#hBYgxU;XmO}`pnSbsk36eG(@T4gX z{$fuUw#$p_emW}>!~sgiTRMhyvme0>crQUQ2$G}P!CPRvUWj)(Rfwn+ebgw_BKj2n z*t3Vs;;@WbB+*%!g2r&|7uENLLQ0&GGI@& zr=KWqV(;zFvS!%Y26k{j2$V1fB48hhhobG}16+V(WJLq3RU#uln%LRJEqjfo0x{31 z&xGXUi?b3|OGjsUXJ@@9XN^9%-<6UhO!MY@mHQVAyClguWJDP_yoY3zhFCWiS~X@` zX)~tx^W5tbS-?xp;6>=-xwaEYz(g|hlYJkQ(f-MO&klXhE_~0RkV%IO)E75h5Q6bu@WB{IDR3)_T!^G%*k8Sa!}#2@QH^IiP++xPpd<`Nk?sL_;;(R=2x zA5o`->@d2dQ@dNKWb<-~5B#k+agML*47Q^v2pZl-4R7QRmF>!4h0APpL_*xL?&J^B zOgyDJH~Wrl{sCY8XmWo*h5KvuEw&jc5cez?l|%9=LEV#F3}^3cm2>#HPqo{uWKOh+ z-Jh8E@TnT#S+k9%s4A$^@N>`1V}-5(bFt;G9m@p+V&q>V&nME*rDHe{`v9(wY9(SN zevAMgyQ=i^A-Bl>D4W}@L5a@~3@uS<%vrIv; zzdVrlI*ys%M~+NyL|cW;?7f4Yljy$YBeA|O?S4G6WiFPRn&M?9?4MJcJfC&;U`<8! zfLI4)Yz%vscgiNKY+pB?@3TV;Jne?LNNA|kE(%ZA1I@tTIMOt*YI^8}x)5?38wdH* zw-)`BHNZP#uX>(;|65vq>Iz#ldRvCtgGF~UC1&88gcgIxpRpnibeqMwX=mdWCA%VZ z*rS@s;>1#aT#B;sKTLpLTSQjr7j<2^88!z>2^>!()Og(;Ja)JedaCQ=GiX3Xa_b+%;iqiv8?AY0p4ccK3bg zRUWkczpI8rzFC`H;AuI15t)7b*|Yh!-6N(0J@_fm#vENMNs;zUCD_on1>jb>lXzS1 zQ?KZ!zRP}|*q^4HZm?$_&1`CKYYfBmhu*5x&TTJ7(f!l+%^;H{j%NJ@EnN}~qoU5D zoC||A{&2&~Q?y6tQP8%6FI*xO$#QJS?#xzj%L`StTEhOpw8wV)(hol`{U<QrL;vZV^h6=hJt~ve+W5y%0?5>B_*@Vb1i6Kt|`JESRnQGQ!u}uh!vgRX=~Fe z9JTIG&#Q*iuaVVn4+|gP3HIl$#}BfXhQV?LRJ-@`MstxOLBPi0SCkCeKgv(544#%% z5Y}e{*J_FM!uKUF1{(2G_-s@b`AE&=aV!imf)%}Mok5FS3$a%U$mQUv_vL4+!Oz8u)A4by@NflYZa_LLqOj) zJaf9;9>yfL&Lw93UVygYc1Z&P*vvQRii8#Y=+ZMFt=N%2|6n552 zo2bzHM}tp{3~xe|rE^F-YZF>Ok9z%YHDMoVP*^#Mr5}p zSVDptU;_Es7Kx$-XvYKjnV^+-^en-g;`XDXbS4p4ieK6AMGWV@1f z0q+nWCI2&(Gc~qSx~pMsH)X^F*5~^H=+a1U*LzF{-a%UW&rH|*d%RsTD)G4$(W~g? zSz^C#?rjBrvH6wZTE+qggx|+Q>r*9B{*hY^{HmQawRHkoyX5*@Q^cn&kyCL4W1P-u zmfJnQ8QWnvub|%}%RkaM!R#A5!CgOG8>YxpNSV-N;Rx%mY6o{lvWKA(BkfMSUn_5M zStBOj9%aqp$W$1F!^E+)&i~cO)CTY_ZDIf+Avm7CiF4%KM;gV79PI_q>*%J02l@h2 zxbZ6QB{J(DSrj?g!ur3jJx;GgI;kk4|5ScGege$ztoy#U^8^Hm`g?D|X)+6U6po)| zAlknkhh0C!d_oKZ@;_Kyzvx`u#uWAWuBc@=F&tc{m{3F}Ou-c`c_CZ$4^e#$Ohm)^*Z!L;>lk`O7*!9bfA- zXL0q3I;JkjP>1defyCQjwQ%JMMVM8L7bAWnK9!L3G5Kaz=YZnAGE~vx^L*fyOg9K+ z!zFELz)3BZkdNVDhh%bgl&}R8>q)Nb6Bt`D)T;{aE;qg3uC7?Rx{-~u;hyAQ#5{*CN$8ZWGr$nkf zE|=y=?Fa=6yu#JEKB;0#Xee9j;t3wlPtmxuLx`AX_ZG=M(M|86;ptV#&WZbA!DF}4 zOSp{mZw8w0@_?aMv?e?~5MHM;G!nn{B7XOY^dzaGj6v+UR8l>sblWafYr=fgaS69}VL3Z^-$z4P!s$6c{F6A^9^H$>GVnGgv>jZW%`Q~B;Dn+is&_oMaD@J=i zj}v76Gkj+@0ye&Ij@fNW2UfLLvFc6~jPi2jP;BwHI0Tj{VIJXD86twMhFe=OzMzam zBU%^nJM@exOHg#~S_V;7!>o7wl>`2meY?X8VI^>NPr{#TW`$v5O=leV_o!u~w8S-^ z7nt$lC@->4AEPE1RVaPp!^LdAvnp#U13L<29>@Tq`c~VAb2YvY75Yl9sK_=itk$0B z)uujR^k);Q8WpG=knsEEl(8F(N-UVzxhnGDtvmO<;Tk8rzVVcV$atUJ>T`Ii7ToqK zb~=|#jPG3?+t$SltLNypK(!wwf|bR z=qw^>>>HA?dqt!!%IG53bFsszTGgbPi&4q5X{aeyO8ivGN5d@Ad}_+FloDE$($P>q zltpN=*W>Fe!(!5MV`5BITxG1f&V!dE;LIr^oS52b1!@L6L%4Y+Qx|rSS4pPo{+rLn z`5R&*a(dgXeR3m{<=r!6vCB|6wMFjgaifXd!<3j82Sh2_Kbhr7@N-npL^r}h=_ogX zYYyPiqT?p*ohec=8%>SdDde>typ{PW9!>q{I)HT;Z}SRq<}FTKca}1tvKGDt`wGLf z>Su4_CFV(eYRZL?ygpJ>F6pG>l{QD|H`m^!a;=4sMbgvQvST~rm_iDIh?!>^DAp#3AdJztx>P=u&^NMI z&9F}?gO9?E#^IH1meMWHq;f^#a`TOFjj~x5dMrgWVGlFa!9ugjly75uKE!5O8%5E8 zh8h!O<(4c@h|tf%3A z3}0w!7&bYSKJM&S_=utGGMB99cn`HJ@ut15alyivsL9qo3X)<~! zAh_wCsmumFKXQ2DK9yV~&vPot8OFkSpH?sX8%p0>fAehzTqo$0j)L#Se`Y zG{lcehxf4{*QFqLz0G`$G0Xj8`5pER#2*KhoLEkD!D6yd_bRwP; zZ8+DDP{!JmK>Z)3I7x%tbU%1>1;U(y)u{csjqjDEbc4IQb(1 zM#=f-VS-a;Nuo6|@k>-)4pxg1+k}BeeujYB=V`)^Pk}R0CufoVR$TzcCln-T&`z0v;F{sIX@<022!DYR5>wbWMU9}fH zo;gYWbJl%pk^T~T6$<$KH-7S4<8_t~)BS79VLHZ_Jkk#jRNz(} z)HM^*IyExZ9W};ovN9AN8JGhnlGh$P2AId3_&#y?fcV-(AQK3rG2<&!!kf$&1t>Y! zp&8aE_838kvtVgv*h4%@G=3PhLNv7lY=O^tyk?*Bn{e?3v>*sX09d-DGP&o?S?~le zov>??0D)POD4vfD#71HQJM6l2cVRceR0C*)BTBJL!cqBK&XW~mxZz}q4zy{IfN2mO zczrVD`4KRLcMvf_(YAFu-$20s)+Y9Rkl8OUfzN|OpAdiCHrDiVUb`s z#r1WIZ$2x19_0$GpjT!!{#)7;KI16Q5* z@KCj(^X>~IM&ioY`T^XFM+x_Xp0`*jgx#T?nEvziSa!{$#<3v6EBB!%2&?f7gJQ0K zLZ5M8lYzYwrM*&Pvv6be8GKAQR$=f4oMQms2c``japnV!8Ayky(Kx3~=sCcw>++j5 zhEFVuulMaQ_a9BHFM5Va(ONRZF{TuCIOQ1-P*b6^*|DHupc=@=yn=|~5|FtT2m&=h zNEcl$HadH2L+CTWeFnl~#>+#7$5RHvea6dDhJ#s(tx`EnhG)ql}3W)Vge2nZtEj znt*Vnd@l6`A-yTQg4B@vxuVFZ4r`vo_<$Cf*0AfOTlFDb&23JAIRf}I zd{yM3qs4EuC=H@jHSH12Py9Ra2D>TX;PA_Cb}~I>Rt$Fj4|tT)U6zg?rYm-g#brB= zvCloOdD#0jCxZy3>M1L2@ch@|^oZrl`h2@}W^B6xbJSRkdu-6G8$^5Y)KDj8dK30B z~p6Zo*f_1SJjT(U^iSVCN;sP5%h*EF)=-wQlErx<^X5 z`nfvAGh>KVZno5IBb^ZnZ3R5IO9nqF-ID5L1`fYNs=QQez08=hngxwVS(L@@SQbRO zk1T0O7G&=bm)S~Rj5Sj%p#^hAx=_jbU=K0y;N}vwC2_2d@8iW*ag=m*O~*;_<<|}3 zSJCqR#SKDCrThOL3^ zVM5cHzf|kV9r*9KMhzKIAe%Om|KNFCyk#378g2D%I%H-&)Pee%CG%P1f`zrMkMnOR zw7+nS&5!el49$;oF_!fzpBgSRdv(0%<9T0idI`~lCFFTV zwSOleN4wMv|40}XY9#f=5P$#e^2h%AhfLVQ44^t4_V+H&s6s=HX8A6N()qAY`#wxT ziz&=qz5@{rs4v)JbSD1Le1RP=D_e9dE#GsgTifr7uOcw)EE!ey!2~R zywY-38+X>e%%WapR;?piY6`Lwmxkk)swhcwM()YiLjDYEFHD)E$iCD@Q&h)Dm&`iX zT`6*e7ClK`>6FgqD0fU%4&8dT=Qst(wSUP?@y$$ei%;c2iDATRAw`*TCUxV@a`=oc zdtFYMPyB~^G7UeofSNKGNj{t?16(8n1BTWp0R!fMg0xZ|RsEPr;-sPPM@_=RNPu`& zVH2iNdGZ}??R|&tD7sDRDC*XPE-VQmF9C+~GMOY)3Bf{fUZt>gdQchQpUbvhQ63fU zQ)bVytYz`?qypC}Ed)Ag$pyNVV&pNU0Gzc=8I;ol7UYr$ zJagR&Z$E5_q6znlSH^o>xU9|nVYfB#E|uPq5;)h|kQ|EjCXv<0eLX)Y4Z9Iviop7~ z;w(|XGunnT9X=vKM~{k{tISqj2{Lz30OUjDr*FuXQsZ(ajFd$D&jFnpF)}HdXxcY8 zEFom^1@~6D9JTPlflBo_enQfwbJW7~e3Y`t6Hivg?5`u3BB@v$CMOb;azvKKuELhX z^flUpnp`S)pv?I=L9KX*Fj@FpI~H&*gV!^aI>E|3meBq()f_Dawd}pS? zlZtU*wwjz@s#4DP_i4FEXUqGv?hgx2klYF->$nRJc6Q_>ed`L+f0AmOg8|sgMiKug zkVoUYowoJAp~3}6VOFz1ws>oc^Ym(E(@=haGA*s+7D?PC0YZLiJUs2e)ZC|pI0Tz= z_-`-KiQ7dfaG5AX9xbd;NWUBt3k5rd#Q!Y){J_TaGoX+ZNjx{n(x5J8@e}_G9)JoJSpi9B19tTxVUUBZ33TN*57R zPf3sHGOf<*7b)F@YxaLl@6YM&xm?1_z9k}1HyzM`UlV(_vgnFn;Zz7#3()3%A$&)Z zux|IZ zK-6vqDMC7r`H-HFyDyPzY8h z@*K09j)o*)DUu$`(=BhtCwWMX!Qoprnrv%C{9cru$euV}Qs9?8^1)wLKJw}B5~ria zt0*H=n43${(2}m29Xjc8^6H$R)xX)-`Q)T!DT_vDCqVfvo_pOafYXCj^N;ZNG8^3bK=cbVN~?we z0@sxpLY`f8|E(gYHV7-dof1RLHKvTuw~|bgKJR9cjsRyuRqHiFlb%7d<2GiyX8vCV zmc9pKYcZjym7L3K|~! z8q|M0!7YZU(@|KHkQS?S1u3bb%1ZHgrz721gn^`#mTDIj+|KCz+T}|s;Gjfq$L9Y5 z=jm}a)5X*-n{uaXT&|vLV!@o7&JX3Z@-og&RnifTTMAljM?z!08i$PeXUQBX$j5Y; z5bcR_CaaVIu9#J_)Q4QNC}I{0%k;UWyX&Q6e0x9tMWN!Vu=dCx8aj&l1%Vq?9qngDsf0;}@k}i0Xn#L1Wsa+8sKCdta zpmkhDeO(}$QuJ7K>m@3oDx8i zF*#9T;ce{el?1ZJAP|;z z>;OgoDM`PcvRfj{=M|4ME$2lh?JfjvX$-AG`Ph^j?!N(=K(i+c{`DYJqz0;Vf3#Mc5HBiDk&7CJqR}AE;6qIxG8B=&B<_Y`(*R zKDghftqe8b*#^2Z0}$n9eq-8@Jg+jbo~ zs2%uvwM^cR?nZ*bqYRyoMQ=bj=%ORSEPsWRwixqyp-KL|si>r=tQ(~UMGvRfZwS%g z$mxQB`ZooyQr`w5Nt7J2QB0XZszmR`^gj?xxoY%@JL=>kJUVKYB2=jJ_YgfiMmR~J z9Ayx9FCOakg_o0rIM4(qT`6{nQUn0hpqa1U&;_9D7gXR4 zu|S^qO9;2W&lq`fQrvk{T-=Ga`$rOgwau2)X=%9u-g+%UwtO06W?)h4Giy;wWzLYj zM6jYk)Ow90gsMg#cez1+#{ z(0}vS9e6F)nS`0TzBB{|b#3UC+Xu#Q@gh-}<|DDphvZlfIDZikYpS}4h1#Gj+>D;I z(dw})BIRI8vpG=;-HuVurI6!+y|MDy@+eePJPTdo?Kq(&Liy*Hl0ZDtK|0DXh`~5g zL4#-uuW;oM{%QUi8{$yc|Dipi_J5ES2^`?6NTJt|#X~JLCWT9IMHMo~SJ4$CD}+kq z`TU_{3(>rOQ)o94-1VlxDOO00RdhZjReA8xtXrDcBF=c7(?>Ps-i+>Etnu+y=eJQW zzX~WQch+J3_S@fjuRz#M*F29k__j;WE$gY&Cac4}-!HzA3b2k0M$VioU{4*Vjdx%k ztVxjGe#wklb;}H>#xm8H{)HzDZ9Q7Sx}>@fR5h)76A}zoxQGn-JI`90a>4H2o+fVzltyytlTddQ2@lk@Tm9 zG!@P;jymnvDk97jaR9<&g8BbJu&M}}O+(iw7h(Pk?}CE+MegpxCjM{cyq7((IZKx; zQN^!Iu@ulMpYv-X>EX}kH}QKh;*hXJJT)_Yj3P9)7SrF%OJg2O>6Y;_LMva{kt2_8 zpH}6M;j*KwN;&%sQ$|_B_QW~4RYN`&LE#))L8va99XuYhPJdLqHoi)V&ncKWnu9$l zRFaqexX6UWO=H738H||Mi0Gm*%Bc;JG3BWLCx6FQK8{hJc&2(Pz?8iH>pRde!Gtt z3Q#0~=O!XpYHm;@xN>6d7OpdI7wjs;b_fda*OiNj`rx=&LSN~0URe7&8Tn}>1f$UH znI@^d7D4&=E<}db*M3`?htn=+ePmst@GNOlS=PS5*$8Ba;fv3hPyO;P{OJ9#j7VGB zSP!q6w?)mjU4y%m_y`sls7I^8R7BmSXzsjU`~1hr$RK_DZvTX<6opuS0HAYE0Y$(1 z8P}(VGv1|AzF}#~JN=d58a%@8K>m?E_u7eOGMvI`BPaCOOv|SkjU?*ueR6r0D&=g{ zM7%LC4A5|(60?)ES0|fb_PWXRWdWWC?!tAB1^bI3Vxky{`{Ei6E?1LidlND2orZ9h z>e8zGawD2}MhRkO0P~p4FQXIsClZ7F$SLcGKN0xDxScduLPjMGQx#g(G#C3yEVR@E zb53lM3tnNAmCz^4+d|!VcsRzAgR7lkd`0_QeT?>3yhp%;wJ^rb%AFRW9)Ie#2oe%w z`DsX>F6ZHfd*BMXj>a|Xtqb1cX~fer>ked_YX1rBFSm^mi?z1aU+l7m&DO>(_|?7Q ziiastK6uZA!AQ8(J!W)A$@x1-j3hv|2>%6NKq4P%Ipx{}d_|p^$ckgQ)j$}Pi6FvK z5)M3d*()*lB4W1apgj-@6#Vffn5c*#h~YyCvY7UvNl}hSW@dD5esnIVU~wbxs^r{# zb0QdnxMgn!C~mBwdM^rDz`N{7V-j%yv*H7aWU+9VH4^`-`nkI5?@*`>dJQ34#V#-U zU2~lXXOzs7W-+ElaNBzNM{=Yzy?gZfP|A!$ni&K7gWIz(nj^=08SM!9X2vYJNL*N0 zoL|tg8Pk!ry$a1HmJmzfXxWMf?F}xg(1`qTpTUz&|Ij|4ZM@P|WlBJ^(v@Ztf4E1Z z%E<9tMQE9NZN=Riy!K0=sm_akojPj-xMb*8TO8@XendL(xjkWW-f)HBjFta&5P8hR ztV6?AJDpzVx|x@sIW0(!^_78kOWHuD3q?iQnp7T$?Ub^@2 z_kt@{ZYnwlTInh=xyF0lKq+q%_x4MLnr@JOyS_m2h<<6>A@zrB5ITqE4L*-v^C6X#V#v{{ihRZB4*a`FYgWuHr+Zh$21%K?%iB}&;29RC)l8$i;{TY{kIiaI>5yEWsTC-v&b3Ui<8o3Rg;$t(tslow6XOh z!lajNH5^LkHLwMoQ|q81*NUCSxrAB|EzgY2a*x)YJz3S^1EQx-v(D+UV(%ai@?8)C z5?n|Ud7<5;3KK}*xgYr4puUw7Ur5LkF+Y*W;pVSDF7(I_)ZwTeduQ7I7W_T^yb5so zn+DMmqJO%m9WjrvIS`<_nV0PBx+UgRZ88?NOlDU*s$jG7OEhMDWaFVIR2hvK&k)nj9|3gxY4znBakmFyf zM4E{?E}4KJum89pIahiZv{&!sW?#+SN&Eli&T%Dn8y!JH@H%Ahe)nNP+4I%wRmr-3 zS1MeIc?!8bkq9x?vnkr%Tfs8`9I3=b!IIkAAo^Do|GA-%o>8Md zpvAtyN&-NX1%s;!g;xHb*T`Z(MR~S|oWDtk=c5sE>n=h{wC}ftQG2`tXZI%=IZEQa zpNx*gnV`z{(FhwHXLQyQtOPf%B|GVw#0o~|^9dnuketH_o!i8s)2B zxaED+7;R%2JxIUbmZg_`>RDvF zl2W4Iw!6f0{ZJ1m zUplbr9q;`_%Fs8|bQC`tqmOemO*|oK(L3(dwqwVXW8K9bAZJgH{WW*?F<13B?M1%w zeYRx6{!M<3$e;CXLK5bc)FbKVvoPa6i9}6@ZujS%68}wJ!-v-}(nH6bmwp8Sql__k z#)Hna-#Ts2Yq=jfcaJXbQ(h{-RQk7KbSB=wbRHZ+rNN8s245&hIVX&!b!= zRx!=T@89D<1Tpfyk^2x;yKml3B^W-J;+7Xa=4`ML^VsXFgHIFVy3%wpH3fDs*%-C+ zCR1&GW)O=s+>hkxXC2N&bh&P>#;DXf-apu~{nw8`b)nYPxPsEP*9m1c76HCl@^zr; z6#4Vbc&!T&=&0x0Mp6(IU|rtv1$?e|P?DfqS;vC+g>i5_Ni^CaU-~`JS>>;@O(R1u zu(KO_sMa=|v*>}2L|!IRGK|yeJ-YAOuXs#(%SQp>FG&C?JblU=QhKL1R&>#1UAW9c zr%HUi3K|4;zpujY%k;+*;b1#$Gc&tD?D#V04PUQSOYDNL@eIZ49Zx?^nmS7ltbUhM zK#B}(00FhFi^pNij}SPq=_VB3itLBpOB#Ap@5Yi_uF>70kCS5busi2#`5k&sDiT$K z5-;3vB3=)4PXdyz?{zZ}_SbOF?mK77Xpiqp0fa`B3omSpEz25ge7NvbVb~0R1t+M* z79Jyu&6{04OAt;e8$Pkd91QUd0qIb~DxmMZ3-^q2?n!msNM1n^R8EmYVV>|waT$Pj zd_kSpJmBhx-)~FB0Pz+fNP~3r?L~y@jedi5AXg{*}fxv&e zQWf=L!ueg2;8!!*iTo>Z>4$qky<@Lcwz#vcJS*hM&a<)~tgMdTsoaMPeJ5ZO8Y$i0 z_M3ZNnYORS*?=%}ulq*wQgf3-y26_i`t4P&0WCjNJT6FF#V(Mg|4m&G*){yujW^~q zQ2MRgU_?}y=n1Uuy0Y{$pLzUqm;>JQO9Rqs9x9ZiJbJMf)@9gZIN z?LB6Yo+@ii13LTm`-;Okq1S<)#!CVf~teiW|^H^n*m>cd&MDUSFi7@9{#P*+f1sqfJ@`$7m8k5Vs?_#$RSb7R4R zV7veH?eLg*{pv68f!yE7KXxn)e5IrJJpQ%>x8l|TO$HA|G+}U=-+kKtX9|s_>J!TM zGAohvE(ofqIamY#1>Z&&6%CiCTD8GaK-j}zA*d>qUD+ZaXS@Jy9&i?~mK%u{i z`z+&nsM-1zMR1=v_)dcRjsfA9FyM@Okf<$~aIb2o5rBV_D9{aPffpu@iRBJsSX;4M ztaL&#t!j@EctH#7)rZy4JlcwvjQVLKuv;HC>Sv6=9{j5tSjMOy)A4QG$j}3m@I9X2 z1*WmM->L^@u)N=<2Xy?v@6ZEd@CBX&2IBY;ezH$g?K1+?+7hGwH4%8t2+U|8Le>w6 z6$3^<*H&7M`XNy@WCX@E=yUy15jbiD;u`e2epIX&)zBQlVqAr3Rt8^gXPx)~>Beys zpj1CCphJOaHo_Ap$A*(WiN^+iPTG*4C$%9q)`qNr&c&%ZQizP`(^5k?VjP@!n>2#h z|2RCe!)y=eD{->*l{C}Px8*kEwFl?xi@sty)i3fx`oQ`aeM%`PLAb6uGx77v3F-he zgGH!6YtdwIr2`NLO*GazdH%!$jeP1|As#4*g>03S8=xk%?wZ)PCfI*ug1~{>KS=Zp z*!v#HLNcY*&uD?k{5O2nF0E?X9$SPJbuZnDvHZ(Dfc79Eh`^b`VprQ|MX}o#IMsGN zFFdh*>9bJEm$M~um#9C*4-0d)i29i}8y4-R_mPiA4`fCMz0g4WKcDT#4*^q(FX%5U zZHCu4wBn0!(?U06u%=#E+Dg|t4j9k~P-+fz5nlSzJCwtvTDkk|_mKvby6xRWJxOriun+`z*rvScw7TT6dwb@3oZq(+8iZLyeD=H?nkV{mY!6*e4ag0h)F^$pcDrU4$fv>^U{v8;c zD0hicx9_OEX9+I&jyz&&^JvE|&s#VM^ZnfK--G$iAHMSdj58Pc`oYMxq4+<3f(R^d zYv#yLsgaNH$Pr&wOkZaFZW#X?IR0uJzr8Pt%?74lJr9t-Q;!90TK$=+tBC)oKLaf< zJSbM5*K(`~kvgko;UGffKqf#0;c|JPtRB;9bH(aOt=1)0pV4aFVs%`r%@eDqwOWr@ zJ)_kYAVxloKnYCqG0fpRB&+HY)t;CQIh%|-kH##=MDJ2uK$_L)W~2qlx|=b!7M~Hm zLT4BOu|wTUAEhfNT(#3xHe7YmRgT!Yv>U5~S?gW88Lxsac-BkL-0*A*Jx`x5jP-NikFxlq`!ii^9|a25E-KTu}j1dzNQ$$e!N;ZLTve+ z$AI7*d;F7f2S0!acK_{+U~gzoQzK7Pf1jpN{8t}UA9=_}V-vi~_sOx)v-|UqbsMA)K&Te3Zo9M0LE(~+xJ#b0wVCIE6!KP1IlaZ0moiBieT=s|Dv zYm=EkKkYp~`a?qQ#3FEtRl3Ms^zQyWJlFG211quaUrx%PI2xh;I zgK6^Dyzy*ENJg=&)aBcVLAGmHI9{BIguJkSj|BN11@b<;inUu4;s~y4`bOJ`_8fQq zfmzbMCIfZsFC+uCmzbs(=odejYW7`-G>I4K`TpGu^8GJl%+HT*cOkH`tmH?rpyEgO zrXUYKSNv!>w=Jl!@2w5@M2{z$@dQ7UUxlUZljt84 z&Mp9^b>YE~uzZVA8LnOtwl48BC7=|xSEh1Abz?|kuWcV@gzw{8UtX6Hiue4KbOC_M zz7AJ=pD1%>uT53*ITco_cpJ!(zMB;))f}0?GAsSeYv_{$|CvE`kN)I&?Zbu15#` zl@;)!IsSepIUm#+S%^Bi%!qAeOd_8i^h(JpN`G0DeA#4V`O{zelP{Z%tP=BO);Fw- zh4N*KkrhmT8BD%xHL}XmUzW+2NX-CgdsmU(h!#z14uf~IT5#3j@}mox&)$jo?Cmn2 zJ+8l6TzQYd%DWp@UYJ%`GyOKvZzKKI(QhrSJS>B2uDDS+pa#QVTg?ym)6(W*gyLqqE$ZAf1 z<>ZcII%Md3?{YNo-6tV3PyC^Qu4$yDyI$+}I)oqg=KRyu`M>dI*NET2K2)Z`GPZX+ z7vLdE(N|%|zkuPl&|5|<5tSI)4=D5Ni8o@L(JJ&Z3H8hV`JIF~+BJCe2H5_pzI|f` zS?fJ27H@{Pux6{I2RDXwg=H;0w1FC_d?^lPT}nAr$hee}q>ybXrBT^QR~U8fQI!CN zyP&_obtH3Z!IQ20M5Wk*Co&ZmW<6;f&pP@Yk7I0@Og+e@i;NrE(Y{gzN%)8n7k?+? zVOS`eD;guVhMHS$DRd;>*wy=8ucNTmW$&h(^^0p^pyH;gs+W7=b^Lo{OVqPf*`en4 zuPuX5#9BA-X&w-BF_U6DzWiDS3cJnd3?D1biB&j4mGO;lzn>6`@pVW9LWz^Io=>jJ zSeNl~9oAlwti38(`{Q{%39a1~ihN3}#1{UFTDWI!1GlF%usqp7AvI8h4ZMBYY@jCD z0ARJYtu};JeA|4rK?he9T)K~4zxc2|-ZNjV zyXtm&9nnW!8Ic;~NWvWPOH zcWqHEJi;w}m0Q5nyU*etv#;yM|Wu@G?1y)MdqtJCs ze;5)y=&aa`7*rvbPL69;W#Xi4e4AK2gsng30_bm*To;fl4Kc79dhv?Ui5j_-j^M65 zZ)f6<|9>eR`QB5iBOz`c8^(PA7i@gucjEw}R(KoNp1v`IyC$|_XE8No;tlSfz4yNY z?b&rf0?6KtmIpF3k8Jds)JE@m%A^z3jX)&ikYOFJ!(eUeGGt(fb(wz_rI1ttSRJMl z%w~*d)mC{Nv8`kiI5u2q49fv0Gw%^CXw`d@x%bHCzm=#5+5ATg_9bl5F(wUJ7n+e& znv;=Knk1nUg+^9r#DlXLGB(DU4w@kS%r+CG&m=**D{q$q<75U&N)0g6KgHSr--@2& z$Y39WY~R_N0`C1WoZ}XSZ00;y8k_b}mx2@$a1+Xj#1U=U%U2#_(;mKZ8=H3XRj#q= z1-{BQHtpmqhp}lpUs;V!I59Xx4-RqFRyD-vP?c%k^EkHbYoP1SB{zAO*Pr!(Q6t zLW;D<#o~eQa&v9BLs2?W=(t6Yi75ym7*gBDXuY4$k9O65S51eF1=og1$0M?On2of( zk!R&`+x!b}(GOB4<&(YU!kc&XBO_Z`BViQ*VaM#ay(h|eL5C;yzBcR$51{CIag_+= z02A3*@!|dX`im+3c}`q;f2iShAr_xku)IOsuob8A8!!b0(fw#~pRwUioQskUmq>`x zP;BMGidfFR_#lj1Tnpl3DE<#i_-MDIq@&>X=)QF)9OQ7eTh~d)B<%{eWO+%*6M4uK#Nx2s?|4X_03wKUE6_^r`1Qa`bRZ7 zj@5S1+0za>+1i1NSnJtN*E{K#j?s2JOV9VvHOa<1NdDeIdWxRc=z4&DNf)$(&U1Q3 z>G~x74$&{h%z+J_%oB31M#28#^(;}V&Wk`Jt2y3QFXTu>pjGNW)C)N*(Su{+cQ49! zDZ>;!$E#rrXIQauwsYNTbYNa-<)S;C+wbE>@G)F7V)k3AY zv3W-&UJq~Hv4@`F5<1+2$9S2>4lCWPpqt^tJ>_(<%|=gGauIat*Os2dEXi48)IJ3R z*hX|G5+pX7Zb7jEXe-d5r@kMtQBZ4fS0Rmm>Avx zm(ha`EL(+7zuPXq{FmgH)4wvm{Q0lUFaPyd=9g3-2pj=qXPAJ~l?gB#ex#j_oUgG;4N-5_cr5wePA*Ec_&p3>7wK$5( zI*O#ci7@kTe2ukr4wGqU6u-aQkSB~MVvX<2fBO3uiVZ&1k#vg8nzMD)Xw zPg6Dh)Vip$$zGjC9>phDjAd| zpQn;#vgCFuxk8rgqmt#aWG9teDN8;=B}1}g6P2uxC9A1qr7RhQJMsGAg_-da_b1?+ zJ@Ok5JnuS!%c=(MuE$bnewbo3Haok;=;$f|tWp(7--TZ-0P(x+Mgi-h+4Yb*Dj$6T zZIf)GJe)t^JKpgb(a|V=R(Ny779Gfp9g(wJov$Tc^L2P_rM=hv&x zWAbxeY`^_NBGEGdg;rTAoA;W9nq{HVFo;AC6lld(eN_lG*Q~I(#n?#k+SUN$O0nA3 zgpG|vO4rotYwDC%V%(Zr9=G>kXohG< z9UTtVvBg_msx1$lfi4xLjeIuPcddCg*r=6nbl)DbQQuf$UK;NB z2$@x-bm_Nj_jdQS@?0*AZ*#h$rD4=~IoBTENV^}fIvf5Y6DSyCL(Y)I%)W(IP-*jgH$^p@BZZ_YomK9V3 z%Jsa_;YO7Es$0BH@Gtx%_hZoFI{^K-7W#1&^`njZVKu9{URLwfyq@1g?oqYnVr{Qq zt`_QO690E`EvwwVRe7?FTJ!CT{1#0sTid=MN?Txduhq(1welvdym<-`tT}R@ue3=k zZN`~wnmdz!e+|xLtDMQEjvGt_YgMz^gb3CoN}CbEo+kurAp~n-1Z$ZO!CD!??z-AU zFx9Cw*r{qnu+~d=>$+Lpy2;f4Bci2rO(NPMt38ID+0o{*kI`(5a5a$&8#`OAt|6+%so0wjglS=>dxT{FoIIWNp$Gxh#;9<3z zCwI_lZkPj)YS1+n3vzzNsd}nNw`A$57ifvWerjknX*5bg@ zW$+DhIwawgxIEg%7xgT^LMdg%boA>G2Mdb z0h>{KQAPwQS#;j0J#Um;1%EEI{o?E{A{vxBVZh$~ZM@rz^#C|gZmWd);)~3IZ~s>{ z@Oz~Fk{W&69jQZ+gQI%kIiHeNI+KGuENH6bQu*O=Tq^f0RA%lt{3G-+{s-0dx{$9m zv#oRuA!B>Mn!)y{w`9&|GErTlJvftpy7?s(2gvVDIFf&#Z!)D35$3cgz)9BjcKW`i)S3Bp#5Cur#xMc?>Mr1G)bj2Ii zIaE>Fb=yn6KWw;72x`dk@|{1!JSLXtuQSJvnnf^*h?DIn(RLb7f#=ByA#EQy2qGCk zD%;00*_{vbMs-q52-$fG8bqC?V`mlh@f(ZRj_VEM`m%vFs{t)UDMnkHBxmUe<&3k4 z>ImktQL%kOR_PgEVUb>bH&YNTr|G2cS5EpzA7dwd;CCN0o%AuX75b^Yic9RrD%?kh zci;6H^GCL-_EK!zf;q8q{Xk;Ny%@(jZiJ6HMX47*4|h56(HCqSb^1xH72{4hlkpR{ zcmKug-OQsZgvgQ7GVGgi4&#V)1ZAK8w0OOuJcvPwWuJohIrS+7eG=uK>iF;8MY6v) zlK`&v8$6E&2u-{HB#ghY2r<53F2;NRY>f9xjQ0}tPGY=QV!3zr_2n_1iy+2(6~<$L zT&UqPHplbgJa>o;j_1MoQMfx}j7$uxuFG%)bS-h;O930bN!y=&VsHruUZRHzOp6)Y8c3R=9aaOGN=p|MtoCwQV0E_GVkKL69*K2up+PN? z7hRDT+uR^=!x7cRry^-V;%wAABrq6bC1kCZJtG??>=|-9d8Sjphxn;%vKoTsvyK8s zNd%hcO!!G_%v>6YZtk6R&+o5++^=@Mv7Gj^CU9 zbQZnJA)UdCA5O1UAietXXMtYD{7HJXP1378MX!4PRC<*s=@rFvNTFAGl1AmtzLxYV zHb1@MB1o_D6uqK|5r66q&E5I19Xb!=ajDKrDwW3pPLnIyu6F49=ui$*D4o?cvL|vq6CoN1wlKE&>-j&iU z4E}Y9-eo7_UaBSX4-cz2iY*T(SFyIfXl_(n*wbeRc9^}Y^VF)gr9`afcn>&VW;k1- z#yrUR3G$yXxH367*WB<-!>wv?Pqn6mXIfvD7DN`7@SJg(L?N4rf=3YrrzsX)!E)Bl z@xqm8BbW2vrax8uM|3(D(A-4{K;>dKx=dEOOCplP=&~u4>T)QQ>T)WS>dKZV6>t9+ z3U&wARx=tsgPel~akd*32*%H}I2i@TEfb7c(r3UT6O36?(JOkvN-v$W+b&Kl#K{&#q0!(F%rYJLK5r|#DO&YS)iZ>N@s8^Zmtr+vZAZ?cSg z-BSyQt*VPLS63IpPQvt`+$CE_4`X0M=rQkFDp(i?;&&OHgft(cfp!vx ze35OfJL%ASxz%?_{~}^k(T3X*?{md+8)JRbR~6R(7rw%nf)81Zb7c9>?z6XP%Pr{B z>3m}0K@-`}(KbY&Vw4UsXNSxng@#AeA0UhJBKUh;Moo0YPM6kHV}o+KAT#fIi!N`5(kqcmBm!IVnOU`m{ugyPC^ zd%??uBb;bfllMaNoN$C%LnBNS1x`u_L9>xPWjD^9U4}v6Pcx87S}JEH%uS|(g=UYv z);S6x{&)7ZEIiPliS44aAzesOFm0g>(F#bzbi`yPdC`8uK^bu4v}cE1xl`@N@Q)?8 zt0)3ypt!;er71)q)fac-=aLw3=O)t#T8^&CoF|y&1br+?bm8rD0EmUI5n1GKlj5SA z2MNTGi71saBbriacz2L%r_g}F#}LBj5|VTgb!lYk(sdz2MJ+|~6v>Ok`{g!Tn}$lu z@cx&TEO+qB>1i3x0l|T_x!g+8UIFu2_`FTUGev5G@;8u|5gEaB4skgl2jHi;cZ{3I zfvTR7DQJg)Tn++vz_IV<$IO*P;t-#mPohAG_#a{d1SD;_MIXVyV}mFcP8^ptV^@j@ zlo&p?fWTE^po2^S;LwPazAv#7n54a>NZP+jl6E>3Y=<9I>!SC;CCcx`#YD}GL=7fU zk+pNsrBNbo!?VcS>~L;IB|x) z9#qqPs%f5nhq&J)$g4aYKYigS9Kclw2ca6soy^<|b8hf-QJy38)zjr?-i5y-n=_^bV0FNruae(KND5;z!z( zd8;#yqt*d>1*Bf89zuj-95tR%J(Py(lt&pzFCzDG+4(CYzlzOoX5#zBFDWGZ#Fr$S zuShQ{a*_B}H_t_*WZ-_c_Gw}VbKpvT2Bnth|`;tN%=@eQ)RUIzOp)0w&R;ggMMhhUk~3Q&JY>Cqt5-4F9|MNn>zoN!Q!7b4(5X8}XC> zmcmcwF+bVzr|^^FN!}a8rzVcALx!?GlXefqOR9*PNnWxfm6r@MF9{1@lzS^yQKZLk z{HNX<8F-TzS`){y+-Cp5PtC=kWS!LY>%uj&C51Jae?=T4P}dPaiJ+%lPCx2hGN$fq`M^LP{%xL5ACZi^o*J|E=ZFW(nV0 zM4&e;0eqbs>>5)<0CU++Av?dH8ufY(ofg$W{bNL|*JWUw)L^UwBdxFESqq`l156pu z)+auDBM1W)H9N4O*?({-WoD9-k<&#muIr-dK!q*<1!nk|oZm}K4XSx3jsZ;ZYlQ2c zFi9rqx&)`%1T(T4XCw@~V9ZG~QmK`>8sn`EOEAnEnw<3vV^GeHrpTu_D5q(EL-Uau zoU2Z+`gOzU#~5oe^cQ_`kMoR(_0R31J-zd5^) zbib%VT+>O_@-d3RMY`W$dd#z3@$Fs*yEmh20!Z1BPs-DcR_`4%hW&5KX8${nIRbDd3hMAeqZKm}z6aCH3 z@0Vw$^)nOw&Cc(q^7YvaRscI_(<47%mE^;3OQz>YP4eOX@FX93AD-mHL)jDEhMD9e z&oGmGcntP|E-~0OI4JcVWm8@=gP_gKAm}hN2s&l{Z;D|%!&iBxy2X>4CKA1jDN!4A zgOb*TcT8A=bxUfX4qqrxeb z_@oBnWOK^vRGtHmk1?K&8zK{^4dL3jUI~{H-`1ug+^+5@xzJ>G+$Sn6@NKaLK3v@V z#NRsTqtLis(X5-6I-h#%9W;4CLzcSekWz$mULa&rhv;a4PS*!zNOI1D>V!=T?}{c+ zUKyrqzo7(DuSzI)iA}TsGlwGakelcg1wty5K>DyfRF*Q&>(sVcG@;lruii(B++<>I z^MOjwZ9bq_b(kWLa_&{i%_)<~)X)PWc^g4i^|n8c5H zI+&w$rf6o6jW{kFN(xVn0P9hJu{^Fc*et1$0*j-UT#_Tj5c3BODWh`2N|_f5RX&oo zD_r0MlG#C)=1yVS0c+eo_=_9oCba~@tc7`gl>B=Ib7`A3pBNYcuZpD6hkraP<8jl` znEHf38UvuKl-4qT0RTx)Vu;FiT4H7aZj|&0Rh>p&26HZC0<|L#K z6aR)mu^S2f!_nI9^EHl4#`MqOYg4txVb0l&8nvx02HTspZV2GE>PLm1RH(5O z7pyV$|8T|dz;(=W90rNT{w4D%USX~^tODB7k0Z}ZX%um+*_cTY37i(Pz$?n9h?%@F zmtl?;LNJxV)7FXOTF_yPQdX}zIjWqrkQNHjUBqj$1L@dey4q_z|vXY={<|I4g? zf!r?VyE3?yP<}_5-qFJ89q}&A8Qf}B2DfIzF|~-YF6flaF*WH(H6LNnl209>`Ts_ zge^~=g06#ahLWI`pkP=lU`S51%;ZEvCMOzB=A-1~ag;m@m0=svu`BVW z=N!>v;EW{9#<#lSa?ZxLyL35c@N|+lMC>};$?XwGHICa>Hw^<`pf@RR%u_dxb4ok- zLsbILf%?n*;hs0_TL&riNz8V?(cMeoqh9MpfRFW3o3UQHGcvVyn8w$|V-~o;sHMeL zalchRp>Yl`l^H^9?UVz8t~kS^N}hKP+GbK(Ft&#W#gy19lOc2RkJ<}FoNF%{d_?8H zX!tL{n22-BU}i=~^BL6NOCuUlmX<8#y}{ou>G~D1GjELpUB!ecXJx_^soKe5)sE~t zK+ivRbgax(tDl2kKi)?VWR4w0qv}5_NWM^m9p~(YFv{9#$)ncJz`j>Y;f`Qtr)j4B z=V{)zZzcNH`!FVn<^U|5$xKNLr7{Q$Cj(SZJ>#dDOZX6pRYueTfpswn5RZWX@qj4J z8$T^V7SeIRj~}l{l&B1)3K73Ks%*vE=h2n_JWmAtP)aiB52WEVv$)SdcA67N=V3 zxJAsj*lS_FDTC}hvdcJ&441JHRm;5*mU}N1riI;@_9&Sc31}9+Rrnyq6i0ITBSJ93 zWJ^hA#7tF+iuqi!FwK**;W9HssccYX*s#vIL*=kTb0FoHjL?T@w|L&4x;O&I-s@nxo+)wB5#t+ENartj;oHRBk9VeWPd>D5`#yfmcD&MIdZr6qx^%XSZ|v0&*x3~2d>6>DB~P7 z2A)d<1~G^MtOTJEkc9JKIA^88xvdrA^&24=T~B%xfmYlXK+ zRFyCkVWt=Z4ZZv<>9(hxtA($M#KZPg0q zd#Nv7(&#Tw?ull$u(1+CKV*?yV{-3cPRTvS+!Fy@SD80Mj08wm!i)hVbf@Lr;`~^% zGjXNo=5k9>sG0DD#aI;0T4nCaI(o`Ut(lC`Ru05@1o0#qQ5sr}0Uo5-(S%UiMNwD7@II|t&;mCR zEo6neN4200?%K7GLkl`l6Ma3}*JrEj(JcPlgyN5JNE#aRo}2EkzAn-1oyS>;fUwGl zS55p*oE3pc&pq0>eQ`f5Cl z+p&1h{(}p#wF{$fU-Z4Hp-BsDI~_&QKO`cb6&rDXW_X<~W&OKt2ewoZ|9&Z{@pCjP zBiKGb>!;8o8G!a7hEWQLaS&)j&V*rP5iR+3D`8?@jxc8uQ+fO)xmTplieBY zWKzlBK(}F-@Wqj=&ikK4C)EsJAOb!1+j|!$5{dkG3x7z_g>DxK5!<4MZ~Gq@KEho= z1IN&PRH%I%i^~|S_h*P0v8~SkQ7Mkc?-@!Ys365)J%cG{!qiM3naGhp+lod}&?QQ} zc+zuSCTRzAD`UZe_&dQ55+W{EP?DynF@H}QfMKRE>UG8(Kw~l>26ShD0c{~bhvNQ# z0yLH}0iDd#g@DF1RI>q%#peZdmBs#ps*KryuCi6~Hw5Us=&?l4KoZpG7ZLdj&JC27 z3CLaui=6zQO8Bs>g`B)_qI?o=y~i=(Oqt7*7CbgD#s9Qx8W~n}2Es5)INO-MAsOGM zY~*H+K1?K>k&KG*HK%W|ci*!xBcm$+H<-KudSUOo8}4hSEFzJAnp|#o7p`rzcfY-W zLq}}Nf?hpE1hd#CbU~3wbo6<(8O7PrL7U9NkgW%^rHU#OGdW<$cn9-P_h-vv5qM#x z7f$&_uH>#V^!wophpZ|`Rpp^)4vaz{w8%G{zL?}%`HfS)Q9K#&b@w|#Y~slExvH`v zd;}c>-tK;<>{zbcLW-SG4ZY`YfN$i(rCbbs>gL0Yc-h4pGD7f*l83*w{r!a0uw|@L zLb2bXBw~sxNei6Hh?>SqQBx2L+A1o=GcPb}ijz`NiD4X@LY!6L$yeT|L;o++ahLR0 zEmO(J(v3Bq!q))*Sh3~zMy$?O8*8!E#ma19!;*=T)UPusDfvS7P<4}s>KQ?p5W6qa zM+6B)GJD@B$s4}G$MYmMQMiB)j@~T81@sX<^$|Yx;b&(t@_=t65yltkWs579NgTSb zv;6W(Yet&;`jO$AVJTv18QXb?SGcp|UJQaG#)d93qkN`cbi%GTOH z+QLv4j~9p<>$(KaPe_znDnrdUAL8|R^S1XB(LOoQW}aK8-jt%FZXyR5;?i_ZIqsGWIEy62 zM`5d!U{oJwjuBgB{@;eS;gUVFrh0;)jn!Es>PDt0Wk2-+^N8`o$HAx1+*2omT=q4a zfi8QSbLrkX%_EjZ)-JBa4JPd`8w@48)bSy=57hzB`|cIiKXfHx}diDLV*x z*Rx7vfpNVt3>%xTJly_MGE+LnZn-JF2%Bnrb)P5I`08F+q=i|0rgvTfLJ@JNez*>k zQa_L01r6mEP$rGffNP%ZhB5e8%F%YyU!}NVu%m5_^7L6qOy(usfb)m*2dYH1*XYE2 zj$*yv{)3;8!Q8eC^we7q+_l96&)|VgKIq&6CHaxXEK|S_9IDJ#vy2sMFR^%K-5|kV z07iV9vt2{sqTW1fX<~O`Y5#?iL7lutm^I34D_}dzh92VaM~K58CVm`Y_^82}*>;+L zc_25e&R(VLn1y4NlaRGtT^AWyJJq${$a+Ctml#>Q)pgLw+M}+^0E^Uh$jEw0U5AaV zed>Ci6OwC+)U=)SrHKPhny?a{uuJ+#=MRtT`2$l|sd)a?K>}_^NKRbNGw=v*;b|RB zz%w@U0Z-&J7V|+PKX8~2cp9IvaVV$lZ0D(SV& zq}Pu0*0`jRdJNAlQ8H<{!R8N2>*+_Y9jENR&gsi>2zpb>Mo?SCJAt2ff)erdyq zj~E*{V3IuK^)wiKV9F*O4n0+Wii7nR(L6&tC9261c{IeHT-1)0+p%Fgv*Av;;mcgP z<9b8d^Yrt`u4$)~Qt14#hWQ{meMHB2W`x)m{>o3nuJ8oz3J(-R^H&?5zuMZ=GblcQ zn!nn-Io0+IOF3rViVLJJ2epksgAK}S>1^qMK57h-;%~dlsdQDWtfWl=b%uizuTT6E zSgGwUI#nB*7?E1R?XDb>^Zb9}0k4W)Jr^`8n)Td4$mrJHDx7_uv`wHvn+mjoD?M>- znSWwh=Z7C^v09Q!+g7El#emo{C^sGP@bQna(KEMQaZEqP%(xOB=WD-c{`SdKL~84b zsGB8pXcggAPAsvPSxKkKITkT1nQcDsgY;{{kB%@+5IV^>p-wje$tzhp_(@h-Ir$;0 z>?ig(U}a_#u!X9fl$yRrnM0O@h1p}LWRICLnc)why0f%?2xmB;MV?x<7u5tSv%&)N z%$KG@o%zyas54)l4mW?$A}0o6wo#sIMaU$~VZtRBrRh}2xfmL0z(aLt9t=EJ4$1@9 zE=hC?;F^baIe;*s%r)6@mpn#gm25VN0R#{?S?m@eH-Uoqc8Xa|!nkSUP^*s3&jGjg z5;JZ4_|I$nXMq0<@t>p2;3d0*E^Z@f_aVuh^F0odx$&)h{zt*FpjCNffT<`jo*7gk zPqL3e$uXroz><@EMS>g_J1#~bcP?x;k1`NZzJ{#lT z@LUe8*%GWjMmH8Zj^UFSKIph+ArWF`9Lo?*98oAatWa_|6(voC^v*%Yt%Q!Iz#|u& zl6mB!Q(})?cuMk-7As2lk?J`m|41F35`m=Mijsn)<%$x7*mlKS6`}OGyKr~Ps(AJ8 z`KB+#(^sZeMgA~)z%~SNK?DJ3a9H!mK(kgvP&9<*tO$D8Z8?*=BIL83Ci+{&kk z4ybBPfRKi(LYn7IeFbVC*P(u{adw=qd(GOdccsMn%BH<8B+s?dWVv4Lk1V`Z*jy5WUl@#sK*o)dZHb%fry4yZIF zj`HS~dNN<)Ip-ArkKHHyg;CI?8JodP2=MH|PC9ZGo`hO1%Yu(SoPm2i}SkR+rh zC8N1j-4IW$_-W#$hXKyR@(>-j`8)|>{TNQ6Y|F(a6>5;9=A7guZBD$3gt7sF9F+$y zrMq2`v^p`^mcXLO=60s1$Q*8Gn#z_1lCFlNAq7bSe1cpHe;=K)jN?>O!Z)}NMh8i$ z@1Le&`OlA|yWR;L1RjMakuyY&(Sye7X=8}2EpV8?&yJ%xKfRbC#N!tu^x{!EO<#S! ztrr#y8Vj&fV+*m>$WM0@LDkVbX*WY?5H1JknH1H!iO5osJ;@?N$s#~Ai11Ee5yS+^ zPWKXpHCcLxWT_FSTBjD7KJYEb3ipycl#kx8m=Y(OId|wG+~g=Wo-lE&H_7brqN%n0 zdljgH4mW#-BR2t0X{u)|BNVe$CR*$(;vx<#qBK1fr&5ai$LMeFo<{vm!JnK9NBdx0@Bc z9ZSxsRBv&UxC>L0XxM4^xk-O|levbU zoAh&&{^S{}pPTe^lTzEGY_u8;w$W-b*hZ^4Rk_8@XJ0|{d6#P5Gk^01k{Q{OWIMKI zm<-0VibY|qlasYhMW&u48;u)Eq)By#&LhcVKLHmvN}47fQi0T}I3>&Ek)+KfwJK~c z*G;^P)U1T%>7PO-)-yRxJyW*SGtG@mcXra8JU+W^G&M|3riQ86)G)D9$(gEDk{Tw0 zK_)}N)FI7+gDVLKc4}hP5%#l5s*bR4%~ExgbNN9!P2EwpsXH=l3Fpxr;i~yDih;f( zN~g+7qn&(Nr!tA6Y+)*x%90AEkdkvp=T|V{$WlWSw9E>i)M^}0xu@ensZAQ-&s&o6 zFU#;pS-z4!8z{ODy46uN{N&wgt9m8Y8P4c>gbXEh&%7zr`O5N;S#E+z+y{XcY zNH3j+k~%9|`HIABKd?e8@TG=>@mulj&)(ogCP^tzVgj9lY@S-}Yi&<+NsMn9JL46K8 zpBM~)vEym6ow7}5UFHi)X!p$_9vK~fgVOyuBzYrpM;}K+{b6}-!Vc=IZ5WsacE9{# zauX;53GLVfhPx_JCS4@`CBOKBW3Da$_#fxym~-=$>H>l+=%ZOjMrMjx$Ixyv*Fd^N z3W1WOSqGIXX^vX()9t^J1?aV*!C5r4=B4reL$Jg2fX>; z{;xjZKaW0u^TCLDP_lJZb^g0G;ytZxpVs!0*7jQdX|3&|);6TI4QTa8#d5b;3DQNa z?}Sl5L&;%o!O&^-V@3e}&9I|laoZw#IAfd>D?R$Rk^y+FMi+(OU8aA_oBEJE3zmw- z7>j(qx^&zi}u4^M;Mj4xoa~=|+_<0=h~MqR&kaGLPHo z3i@zBtaM*i0mAhU!04dM_;(un3x5Ghr|`WMJ5LBD=>qo}1Lp-!n!#r3^;lImjINSG z%^ZXQjM64oZ-qLBX_u>y$KSi-g9Jvw30RXrMyUF;72I*d!`ZO_NR!cT=qb7kF&ZpJV6V|&HA=0pm6uu=5v}@l_yA91pc|!@iDUY8 zc#S_+3&aiz=OcJ&!YP+IH3Eix?je&r&lzrY#dds1xHnD6#!&Fo4=2{ zRUEUT=s=NvpN*EVMZeEMtJBJF_4}OgcD@RxzEt=xXoA*u7w^-p&+1O z-)Q138cDB4xo`#jLT%bU+OJo-wICAd3f!o3Tw=LLJViNkPu02gpI^gq+ z$j5#9BiF<>lOvPyh%>rjC=t1CRXkx|l0k(qZs2gD=Ur{Z#mFtbnMbb9qM|d8Tvxba zU*ziQ_)qQZs`dAeBJVsa8qCOl+9iNA_dK)XyiGl$ivwY0(OS z7Md!y9qqc^4olHV{d6FNLi+1*o7!dl!OZLwX4c(hb{vPD^`q!IY-qt25G>Vk^PsKx z^OIUY@Z&;fMY89dd*4gaUsLAe{*%1>-hJ<#d+xdCo_pUp=ld06MB;Lezy6tLMcfv_ zw|5bIdl%j?`lNF|me@;lg&wl`8i~%?4zu3z2$9v#VTjFMje-zKj|y2-yw1{Qutw=o z0w9uN_u|mP*{kE%2`c#06Aq#vWZ{HP(k`MVc$#$}MtbT+4n_z4f$=lBl?X=n+h;iY zMX(wo%uUdbFW(S8m#GkW(sZ!SWc>$i8XVS zn!BkEVRLrTI);+8@|rrV`NcnongmYYMd0*Z++1r^Yi3Y#3mq5%O|slqI* z@UNmm#Rdtiu8JDO|D~&1yecVItE7rjslrbp+6Ja2;T!Rb?6@kZI0(?(h+qF5jfMj} zr^F7LddA1g@}X7piZ&Hb-Od3Ks3o>j&Ym)Smu8Z72(IAe4|Yt!5E`4I?+QG_2?O(& zJH`#nojax*m>c<3Ok*ipEO*7lTS`NZ;Q%f8H>uJhRoWeuuFW^I!Xkd7ThLcoBvlqj zl`i-N700hFRIcSol~$=TS8gTpD~n?-_-r78zr?w|H25=EYZ368_yC1wDZHA0-iW;t z=gWi~W5?~Zu)}WyIjmqOe`KWCx+r!y(u(56Blwj3u+LLOM|}8#yo)Rfu(W9RdKdk+ z;=T$@|GQ}Oh;GNyZYsSXO0;#e}z4nIUV`ifFx`}>&eg4kv|#rezTe*9A~$jhw~u*KM9y%srG?d&mjJPYkJqZTwhM`4_oF)P`*d1(~TB63w42|&Mc`=w|SP7H4!{_+24a| zEHrSvcu&Y2skW;4-75@2A7%y5u-n)Qol`b8Bf^xUPG6tZTc{^&ls6 zYZ&W@kjhzFwrP>=tn+JQ)2%3ybw%}$vqBF6o7WXdbp=wLYqZo^=nAZG78Xc_uFV-z zVZlTq?j<6MI9uupLbs335tCU8zWItGK~iBsaFoGFTH!(t;m{9n@gV$kp@*}`lg^?A zFdh{a{iY**L@Lw)kBX+IZ^zR_A7@fwLFfj~qZES9aVGir<5^(=D}^D*YZMk}ZD-j@ zwkg+9XE!$g3s{jC!zg;NB>XXY$3L*hdCb_0_l#V z3*;)K=E3O&^3IMlfxMw}3VHmbowFKMoj;cuS(!n~*$yJv*oM5-+fGS4aVy_Jb4UFX z%o}v}Y`HFVDj8kt^fm;_oZf)sZ9v%Zz(jJMkDxKrs2VdBfzX|!1|gk%eo^qd8h9TcyE1_z(+uqs<~)VaLu@~toRZp+ z_6gKx6CQUuZD(LMoLv_G1>-FA26zROS*+v@Xh)i0JU9Lma`KYnCHgMa93o2tmnR$< zohIFvXx4fO=a11UCv6@D{LU6SOC=1&#?nM$3y9cGl~{UI=oXcqok^PM3BgYoRbd_Y zTx9Ga(&LUUqzMtx@t+vHACJ;`d{?UBuxI-eh6CcEQycycncX>qRy@VhTR{(-fQLSz zEhD@OZrd(Q(k8XZN-xkUElxcg*d9})8{60aA(4Q!2Xt*Io#=QL)doR|J07w`77_1^ z&2@0L))bfd

>dR;VLCBH#3OMV?2gOyo=EFv)9d}zKQW1g1MMl9G88HaGLL%q@i zdPNz@-?bfv5j>t3wcAsa$!!YW`DFSei&X+ulk1a+;qIvv%K5f*g|ZS(Kj_SwPM^Fd z%}k#>+mMID72=UTOdMKYoT70l zW5NP7RViJ<+pS8v1bEzXkF>l9W;mb=XE>mAO3L=O5oMf9UXPgJQb`gs-1=6bGdy^; z0F;G?SY$iJ)A4gD{lV8DrH4^Qi%~|O#2wRQylquPw;k$Pn1;D5AO%fJ(0K3{lTl@W zb8s9OBe82Jc2pYbtHz7p3Y&!cAGWtsXC09vrscn?{@L2&YL|_OvRb{i;;Rt=q;+7|>EY zy$Z<)r-|70Q%G3PR?a#!jZs%fIy8@2!ypvQ+HGr)mP(+tp_AaT@$UsHPM69 zuGkv4rL5R03qZl9*gN0gdM(lTr>xgL+&aVc+U~|F)@!F&cjInu{dRHdw=-$|_H4On zqUAVJhnKkV_%|~%fSp?>GabJ*I#*fz^8M1QvxjG94Sy@u8ven2MvLxsK>FiUU#4_rMQK}(o}RS*#+trKJ|3^d4CQQY6HJ6m z%Y2qveU15t_nKge@?ciM!~l=9KjpDD*b)*ZyTvcsnd>&)XA+<41u{bH9sU{l&v}4; z5~=G3xHo{G5Mv(=9~u5|_`;}@%{}uoyNPoA%G`}YFUMo>%YkUG7w1kETLb79Fgk~h zI?%P$vA>9u-u=CuN z<`DN75E$RMyQM8+yEgv zkxy=(5*W6f9979JH(xzR+m{peQMmk4;4dUF!4YC_OGK!RPmF&hn z)?%gXlilb~$7IqQuSF;WIdFba0t_w?VD=T^-Q4ptE*!|O=- z;-}O~p9s!^km(Zxg9|%}vpb2iJE!Dq8|UnNhMe6wIcImOoaL|J35ALJ-pt+bOB#>A zcWA*Le3#k-@uMAz;HZ@CAmR@GZIx;3ec!Q$HseTy#WhWT^sA91eE)D zx=5|G4~r7Tb)oV;Nj=cG|3T6&W;XX_#V62!BkpA};!>Kk@ZS-cGBA%PxR*Lm#k|?F z9DcQp`d4Cnzgw4jl9#mOQ0 z=jXfZigI&VoxSW0Ya`jR7gQQx0KjN9fZm!rw(i>|MU@RM#twxG7zsT7BAgyvd>ZLl zYt5VV;$D6+ce*<*lV04(FPhbhr3h)NWA38C#n`a$88gNfLlqOo4Z}Y=;<^T_t@fcD z>uNa82XzRCtN5RLe2YI*ywF4P*lJKl1RTMgWX~9W+^{ z-^A^PeuSt(T)ebfxc$C{U5M8VCK4CHDAb}S&wtLjoWM20n-%1?v#-r7rTQ2EmvoX_ zuinq#I5k1)J`->_<%}(w?x3&_qb5?W?<=m}o?6D|%bGQExnV!K6}ohCIRe7T<=L`E zE;rMO96Dh^s}Br8u=OSavgsMOa`h8IEWSAKDR~oqn)D{j=iY91XQDF_;vD$W4tasPs zW&9p*`C%FKbB{KB$nLDL2IG$~Z$94sqPQnNbPKm%mb+N?kMV{bF1o>4ksti5vf^RU zM&`CoXip>?m&%&E`rN^fGx$YGcQ!qbY@Hj5`|#ZVq>YrDb=%7kr0cn_8I@3WP8@;UqfAMQ3g9x~T^6cP;zi6)dcZZ0nwgJq;a zUUC_I%7K_!gkY)S_b5c&6n;}1?zTHG1%EvhtE{*;`Y?90PE$D)!>sqbkV?<$Btf+b zK~~Iyj6~I%>z6qm%1KgYQYgz-C^K`)Oq{Z8L79@d8>2M^WoDJKJxJLjf;2sm_K2V@ zJtMy@>Tp|)yVOw)#I3Z_p!Mn3$Ua@<$%Je!i-fe{gwEKG&L<_TJTH>{KO_ zANYJ0d;ij@NPgGDECHl${p>Ue?#&R7j?$w$3bg0Uku&{vX~^j_=Fkgb&rM50j1#)d-Fvg z#hs6Iz&qS_e1TuTc?eV#*Ik-Q)KlGr-*7YqUxlcxYzdaYd;rDvJZ6X(njNO*!+{$>`#Ado*Rtz8oK@wDUaBlSX zc_ua5D%n~!o`vdWkIo^`EtwXCz&%{;mf`jo@h_jvZ3CMHxLAwDf?6$bGJ=U0)>=KQ za8Csa82@5kRa0<_!z6YcibYVnpCZQu$R&ocVX$GMAKode?}W9;PVvru-e<2XwnbXU z&Admjy`|j7Q1dlRWbCAgnNk=#O}++&rJRulf`~dQPlV0ld!+3=MpVQyff$5UF*vS+ z-k)!(;U_XTUI$Cx%vgx}EpPN@IkWm`UdoE}8ah3TcT}6wQITTnx^Ps_JprflCB!8m zp@a%B(o%}9Z{mj*5lD4ZR=|3X4j<&K7EGoVZLPNIDKQ9&*n$m8Q|i&+sutdrHDZoM zMURio;T_76Wz7_?(Jk|A?{9<9@@(`9=a17g$n#(1(l2iQS%=ROln$oYRFW7h67X9V z{#02(DYcL|C;-kNA`0!}jLxecYcm!Jo{S&|^od0wa456yrz-F-#aK?P6XW}IJk>tW zY)Xj?Vtn6jk(?Y=3T!NgjLj@)%~!@cNLcg%Rl+JU5eDw3=tr+(2|D5k&`0DYI}GRiBRfMrmJT9CRJkO+k!fa3Nz* z8TkZrN{KlfnM}c=twA;PE~z0odDr+m^n;nmM73iv1IG!@(xz9hBoagNH_XWH52s+H ztPw*c1*SVUg>yxdbFO#-=OWNLLOK~f;Nv|Znk*PeSCVqbcBN_~5D@K(>ygDB{7B5)^OC(%49gFXA ziiOj$_}l`qa5@&Bg@<4@V~rN5fysDm31=^U3EOfj*hS7>{G506G5$F(lqZF(QYd%i z0Di^u<{F#!(Sem!eYw#aoIgt%5dp|I{B!2l(#nICY zP4!wsIkr=5d0xg*WP|aM%eY${?y);VAVIbS3^Wt6?eXt${v^5M;@tW5qk=mwOj{bV zj~pka*o{qx=pdg}xFoL~Ig@WY%;MueZ?*t;*%&?{Xw7a;^uplA4h@)UI)8biX?3lx zEdIaNa~SuOfOp_FcRtp^U5-!*_pfn9bic!UGWwa$%=$q8{vUH+02WtqExem7WUa*A z#!W;;v+BMlZLp;cZOIR97u_XTAzcU%VpzZyEz)Rde|XJP5 z+M4#IeW_yW(i>V$FU!4i&&-@T zbLPy8Qx|M|8tAotY+UD{u0{ zSo#($x$pOE_%trLalt)LTX1K+RDT6$@`dclLRr`FND=m(W|HK6?;BR|Uw!|S+iJZa- zb-bar<*mlAL+&hfeR(Jn%~GNJh|_;b<>|-rd-UD7Pd-DX<02^*UN8!q0~6tTPW?A?14cbur_=K`z?> z?Dk7~Ko$gE+mI2@lj<6vpR6JU*8;D-1$^W-@Y8l$<%?fri5hEm<% z%E)i=sCX&t_Dgw1hMb$3wcpV)#alP`OiT13w;obuMI{#u83r0X46)7h}KD+)aav?DMuWjLsUtc)CeIQY^{Z=!ci?Hqg zoYDAnXu>*S%X+!~O4O#0>1;V69v)={h zy?ckH8n=A!m{jAD?>#Biq{{cIQcaqC@1#@%1y4yeH1BPvfP7FLXSzA%Vyg16++>*Y zXwjUTBA4Q>Fr|p)y2G}&EPP+(S@NP-gC+IKxjYIit-b;>U$Uh0C0IBB87XQ{8|+&G zcm)VdnQ29#h%-?8u|ggI?%q}BqVk!!=YZCZj@K>ZkazZ>n~=_AFm&%44Y5iI2wRZ~ zJr(Llr81Hlr2Se?#)U|IYq_?*sNm}hqI0+)X~_ui&%Lm|xLm9+QZH?NaglU>6zH4* zZ6c4;NAJ`dqc4l4}?*x!QPv@?1SQ z3jQ`p^$d(wuKniok*4^9`SycZ_D_Y-D5>59a(W=q^S?jqe3mSBVZ9`+NP`}MWfrUs zr4`w_6n_>^jbP4m&XIC$Qm$QX_{%%dfP7ee>y2ns-TxP|!m`WzDrWh8#;<=RN5Iaj zmsi-JI}U;{cVR2*uUPgAv{-6LT&a%SIu$KdU-|kuI`cWK2C*tw%%Qici@a5jmkDoG zFWwbakx6XY1}jLq->ppdRs}}&0Iy;t8vcBm^r-T0WBh5&cMqvPonHiS6!kFf4vgaFO(R9Vr7^x~zp zVgm!K)E}a(=@^5T!c}SqUX#*_i#EJg;Ru=v$dHk>ra)35Pj~Er zgIBD4J-v3^!ok)1>9zAy0=SX`e_8+s5W9|a0ej*fFb-sf0Db&(h;Ll|HxcCh@J?(% z_%=HZyZ3GZobCXk99AdraU~GtusVj1x1}M#%LHIUfS?C0`e+?{FH#aj37TPAkU^DWV(Rd0dGwM86pnsr*A_dtua!GpU1N8SS>0{w|nTp z&bFuGLldRb)TkNLeUdd149$;P;zHI$@H9(mW_I)NB)%+q)L0*#U2I1J@YKysXc8A2 zaB>qOYkW^Py>Ev6ZB1#h{PcdyjQ3CJ@B5mNFjl>Q(4=$YvzTOQsA4&2;+_Ib3KXz; zbk(bk-`s-d6M5R1l1*vC(ags*DP@}?^iID`IPMkH=WNF((Qc54GfsQye2qkfV+TKC zlj$wizA(K9ntGAZ)Wc_^QS*ehY-@(gyXZvuLE|tBr;WQ;J}56K>s&Uu!Kz z6#i8GrW@pRqP&Hh-BmFRhJYpf4XrO$`@1i)t-AbgKGk%&VD7R?tZyE+z4F zt93uN*vf0Gjk0chXRU`YHOI!f57Fxoz1ms#5S^DPR&IQn!%x)gP^aRxBjN$NT-B!6 z{Hw*YYBuxhb3(z->F>1@$vO3b;p~R<)@0U%I4VC=EF}InvUsQaE!P@;qOkiUEy5gQksT|Yw*{g zHHiHerb1%rczFGede8UIMQbyc9uGf{eZF$lKw^IVXPs(vC7Beg;_`T49!<>u`hFCx zusU%(RSGHU3&II4iWF-r{I}0ysSE^P~mvRs%Gr zRv-TSR1~4-AtYXhe1O!x{60dT;XcB2`jRcaH*WA+WVR~&6qejD55)cMLyIgdk@+oj*y?1tHs-g{vy0`@g;1{v zocZY(Vwsz&9E4pGooDA}=y4%(-FN|18WAWrQxD;n>jCoQrVHw<$QB{|@EDdKrKO0A z?cCy4J35$PL9USw@&LU-d+0&zzaR7d%fS9iE0piBsYf!P{{VgM#0Idhrt=CnB414x z0aCyfD%B_Mwl;Qhwm>z7^!f*=+s&PfdGo2019~SH>!uVb?1%e#x4GF55Ai3@#6mzy zeDK>uy5G)zh`cC2=#vkJ22#||9$aLB&0%@?tF|StgzG(JFX$I-VCeOINujr`EN6;G zI6uQ9oL8Kt(B`L6NZvjrK2Wsuk4>?h(-`0!W2S3g=q%%7ryG7M-)58Vz&QZwD=N|X z=3CbQ-&~1&a}fCk_h)No*n5N~vuS+gwZI(UXnxHiMpH{4?+rKhMly}2oTbnkB=3Rr zt}s7!f|+0sPvMsqw)XHh?W)8i7V}1MQwMlN2c#OtA0L_N;!B2M{AE(tWb0kyjg3Dc z4X}=G(KSvk52OwC>hFHO(84O#;o&f862-{LhW29v>#v{zHIR!=I+oZsepMB3Cenq> zlu#93>&7MMxp!blQHzn$Yu%^_#>MC#D7`0^UhGN1W$83yXZkORPnhFG%_NKQv#-jQak8^3hyQ zG7XmQG{eh$_@+>M0&4f449y3U@ChHyObG~yfvx`TpA6I>Fd7;80t|*#0EreLFr4q? zTXbx%BWzEVtVCSZpFOpZh8*C^Q_u%d-S(8R)z~s9Z4Q;DT&T3lxTW3xl#v$2o@8vo z9wNx_g-ZK)+|thebRjui=_O)m_dsbf?VvatJ^l6~3v7FE>04_>Qv5q4-~e4cb45VL zVvW!t?*LOBQLlS<5h@x`Ub`@X&m(H$tYqvX_3hJ8EpJLXu)g+mED zMcr5Q6Y~&QAaK3&NyL@7k_us`crgIGvV?Lb|HM4#lt1&FZpx`#Am`|l<~qAY&NqIH zIr*Y6i8?w1zDS&7tT;a5wIx#Ncw}!|Cvpu}?jF+gEm;@Rx71|fA}%|0^P%gC>!7_@Jy@kpO zH%?fEcpRM`{y7=1SV~|~l14gPZa-iXcMNCaB+Cscw4>`Zrs;wC(RGG;|1WLawm-M)g!MBxK@=!hnk*8 zBtLMVH{xF{y5HX{eX64c%`%tqLqkXNkqdw}Tqabn6LXdh;wk?+5;vaIfK2zZg@2BR z=_R<~3`CF8J=dutuW=(K--G{jQN?oev@lJyq|>h}fnw@cGPQ`dYmE$zl_H9Rx^Fq( z(Wfh4#l2bvnsA^=5ls9#bw-rfvo3$ReW1+4p{*>D+0wV9r*J>S4b4h{Yveted6sJx z?nIx{UQ@!iTVUIA2lACmzRf4Ev=<_K6#+KkJX{(BLkd&4d^B`PU!^R!1DVPL8dsL9 zPu+YD{nYZ0eHbswAh<#%)AhQq8-}3rroD+$|pToZd(#w1X!!@^vro>~$koD2% zBpJ8SPKhqs)$EtFg?%E0ep+F}-GyC`n)KmShhDUX&doJdAG>Zloy@8-pm5wYT#xtH z;+CPphILfKhYGu+z5D|@lj}i^&>CTO;X|^_0QCeE1iTc2TekIwDw0euQ@Tmn66I7#G%k&Gg-BmXS-)q}3!N zuSRvTNk-0F8X4b*x9;(98FHe$5s!RV$~<-<+O<*@^FaKeX;OM2Fdv>_WkZ4w_MOO) zK48LZHW!og(O(otQvlsY%RR->p)9<^D9zDwBllD;MlIvZ|4dw&&8{VZxCXh>hu8Sd zkn@hsWE?nC)_-S~tVi3RbX*x`y-1v%0VJ8G)459Si(5&Tjo#wb%vq`ct+k#Mbf83% z4sdYPfH<1JjT$i3>J^9S{eTRo5Q0i9h$;}RwlVd0b~ter55SU|)i@(PQnYz>C#p&>DLWHW!nIFmR8 z)q^+TJfgK$;i)9Nks1xqOl|Zo)&?k0L`2KvL`nYg#SrDX%aACOUn1BZ+>4IOkObx9 ztVc9A)4KOdA6_iNZU2%+=PW2s?m&rySMq(1YeJpTqp~+GD`7YQvqn2IXI8=(K0nMu zZc(?az;1D4x8UV9rFQ5NWkqrkNJ-?+v;cGm@Myg56rWh^^%lA$QatxzC|ulTTgxYJp1_G!`9E z@Wdmlfg}^j61JGQ8~IxAInLkb>$8`#E{0?n7dN2(C@V&?oJ1&E6{{{sOMq{8Q?g#d z4NgNt){74k2QZ{Eg^dkmM)K92;IlCW}|z(;eMOqmYK*-w!Bq8zeFv9 zdNGm(k{@FwHdE9n$@cva09lv5rH#mTismLK(pcuhvIOU=LU?(Uv%)%fFDqhmU?>1YZ814L01U}rOM?+`kG;AQ_Ry;rZ&fmg6;bj*$=I_?w(#8{PW-*v5hFdAt zl3GZH&maidAnjfo zIr(>%*p}6;ZCO)eTh?ZZwni}{n>}Kq>ZWbRG2^Z^l`1ywTI(Buvz<5?@BbFx{1^sf zzIiYP_+YG~!C21+<2o@IEuX)*!Ki#LwkDc*KwIMe>Hfb(5Hx@8;_5CJb;FRcRIUNx zhMis!6l#>FezX%c9wNj3V_h6&PgTk63z?p5pbME&`Gri!VKGDn)D`60f8&kJam6{(-O7S99tLGONk3QT%lhF$;% ziOCL;2L0@vDw>y+m4&q~)R_cQ{C!L=VxBX(PZAPlN> z+bTS_S;kL)!`=9&@l!J|18C%z0jxn6PjS|Ixx+xS3W%n2+v%cdS)$77tCCoK6nEjDSj0;-pPss_xNt*t9HG6L0(KG z@GNP?dI9E(QBmym5i3A`?P8*K=rRx%V2*SL)1vWuP4G>b_C^c82n ziI4AVrJmtQmRK`*XN_&_LT?Lah ze39iQ#gNK%ivCI$}T8sRW`ofPFS^}$T>Ke3rNV@mE z6Akq{p|SO^L{YTiT&fZ0W)$HtY3u5Al;yEo+#Z-mlMaR$(j-Z9gk*_hYhO&9fFP;c z+y_D>Xc8ydeAJV-+-xtIlXre&N!@-2PXw+HMN*k}IKV1w;T7;ShJmKMCozJQSH%cY z-bq&BLjTi>Wb{9+NMYVHtisK_)2zay{&_Ciwal{k!}9kvWdK`RrKW6~(UgY%+f3h# z_GM`IW3VqH+p^d@4;MyQC$lD62bbAc6Rm>d`rFIp9y=LLKA#B{wM6T$%{p6u)y76o ziiLvdJeY7Ux=OwK7Mk{3j!WGLR*z=sz)88)f$)m_Ld|$fKB(^cD>vEG^pA2=rm0bA zPlhRuM}$3@MAZ(~C-IwH>eY{(o{q*kjOTBO)F;Z`dMIXT#N3A1_fW(OpMVi;*rDTd ze;tj^_+DoG{`#~0dyeV)NV-oVPcwFd|H3)>rV7J3)uwm7{JHz68;tL0#izF~Vf-`i zO-K9WUb#)ATmu9QR2PK<`Lfp*Gh(ZKf7)O}xAECsD6;D3g`2#{2s7`m(Y{|HH$85{ zqZ?AA$G{kJLXJ(bDmiu~2Tw8SItC#XAF$JyGSD?NiQK@$7KO%M2%WJu+$(pC;oEC3 zWbJP#XRWCy2Ul`RdI?Y35g`t5xq z$}hal9awS0Cy(d)^kQ2n;7qHyQiFS#bMzm)Ox&kp?`tL;8{TGgUZb?0oDKcJQ6{ja zn>$52L<>xSKPe1%v{}jePW{aXB-XkFw)NM^9aDV8gy&MXrrU| z>M`?SeC)(&7xPCDw^r2YAlRS&0{Lkii6U;m0*Q@9f)jIhJjoN(8xg0@J)$fZzO_a~ zS&8ps*5O#zm#OfvSVBJ~sIi3qq=b_+7z7-TH6;$VTnN^bC| z@Nswne#ZG8SRX5OP{}wJvE<)Q!&?Wg5s*~>@-B83&It(lauRtS>=K~={VooTj-LtG z0ebEHxd85Y_pI)FK>i@G*S1SJKAB8^kwN8Rj|~!&r0+k*-;&vOx}wO!wo{-lv}C3` zWzh1Tj*sWC?G)PUR`^2K?cQo9JFD#x@wH$3x}Se#+bPO@!s+GdQ0tH={g_s|qLn@@ zzO-py+O;oqU$ijUPuABPh_P|KG-i4aO(ttiMkBE_Xf`gNOW{2YL`{a!Jh|ETagsQW zx~1(N{*=nL(_{f{Nw572I&?=`U$Zf8HmiJ%u30u# z$8Q5sdWXL_6rvj+xI#j^bb-4RM8wrVcCn31V!aAcg6~_>@eUm}XPDpkb*pVyTtQ~1 z2T|Psg;b_3*V5Pli2O8h#O$mb9QQh+5X5YIKA&yjDIlm{tWm#Aqkg$nm{V@A;!nVL z)tX*odySZ{+4i;k33Al&rzEx=ElV^mTqkO1)N0tM)qtLT6JNM+lhgDjqY7*Mi!iOx zoj(c|*`~37yT<;+zOaNSiw;$wZ9TCtYdQn@#}jV6hBGv4LV}FHdxQSI)v&(r($@En zo^O56a>@j7PB|?cQC^56qO=D0uJSwDioW(%*rtt~<;MAL(!{V%3h3HRtVdg2 z)=fi!-~7={qk-T3(Vc04$u_`v4KO$~=yLMtQO#!}G?_|E(&mIN*r5Usdy}%juv{b_%xdY`IY@a((tnnbBQa%XGVvvu(bK?t8WuU-pLKXCA3r}tV z)%qNNPZG9Ilzsd-sk2DZl4+FO$perQ$kR{Gs6pUsXR#X|#m!!%0!S7ir?@)$ns|1Z5OM2jMh_a|kP=oy#BD;vV?rc#{AG)V z2(MP}m-+h_Ez`SN{pU|!qkrNU|+(H&ZJrM~N|1m;&bV8+$AB6?|Mbz4N zb*$EjHtb)Dbos6sRhpehJToDu_n$*OiY}c$p5n?n*U2EguOipcG_q) zZRNWb^bU*d#vV)s^aVg0{cf27j7P#hxb7Oudo){m8*Wy%t70f6JdnbgXc^f=t4AIF zYXA@Bo{FUeAf=GZe?g4Mxjt??{6OC#i@YMQ){QHX`{0G;qP>HYjLu!S9x0>5fJg-X z!fdxuVzEdBhQ#cjqQo+D_Nyte+??G;iB;z8Z|~J=x1*7_(3FsAuY81x3#i}h*N`EY zcE0}MFjx%MxV&G(4cLIhICrVi4DYe7oBB0Wshq9x>Wb=^6HHgUx{m7&A(%&bZ5#&M zigi7MB<@66S8K;S^RS(#v@DOu%hKFu1ffvHM|Au+c6XUTcEv;POF9T24jLUqKVU`I zno8Lduc9+1bo=8~^s)(Lf4mm`(gboSUPTX^Kzia;wAKXjT)c{IH-SXNmWuAKhU&<| z`X2#=YfWnDYTSQ{7_*#=hufcfjUZ|~+=fgblksr-nF(YnUPYTtAgAM1^bHdT+3C%! z$Y%nvTjLPn1``MwLCmb^0~1}&#B0%U6UcPD7WJ4wEb%IO#027w*P`#3Kq!9w%!pYs8#weCh)F! zmDZU+9*tLNfeGZXcx3#f4#K!uc(E=MwTkAMN_jF~MJNBuBz)sl)carPgEPYeNBA4B zPBPt8&bK$K(`WAwGn}`$T&QteO&Z75q;XuG&2qeKwU@!(d9{z_ z&SyCRwmQIairMO7tAf=gPf+MWA}= zAPZFe8j+F$<-BAmuSUvSE9KQmdG%7>Iw`MFYTJkaZ4}a~jW%3cxU!&zb%!?)+Cn?0LAHf923sDZth5Ghqm|yE)V7=6_Rwz+{XR#(v=M0Qr(e=(Y$1il zMp{&MkwRl5mC;2?jg7QY>{6&!vT=r5>5OsDNQQsq~vh zzqI$-Lc6Ywn45Lcu4^Ne(M7wijmh-v74^{)j*hw5mNEJrCj7~Cr)`RUPZPG2gzXu6 zo}%YzdZw>kC}gEAw4L5U8|W={gwZ&b+eW|b^t*+Ax6*GH<$jcKrR{eY z#V#Um2i8SyCL3uN-u0wt1E~SFkXm3%9{u{Lq+!8fW+pcyh5dF6K{W zYm%8ig{?_pem7g=W_}M_<6-_(wkDPN)7Y9c=1*s9(wRSlt;t~iOtvPI`Lo%YZ07f} zHD2cTu{A#C&tq%ym_MJb$!GomTN7aZVz#E3`ODavGPUr{Xf)hH>g%wKf7CByN|@Bn zVHy9ZZ=HaYN08n{VHy9Z$00>-K>;O&@HhM`q(k^O`1|Y?kmt|;zB!CK`ZgJV*Fr6_jLrko$zcSkol3X{o%242{q|V3OWpzE)8qZ^M{sxaT!CP{ zi_h&IlmZA=iLYBdNEw6-;A^u7We5W2z$>1>gKYP*3UnAql~$CqifYzOC9u*Ocq08k z=~{TAm1JohJdw(?q8^@Tj8?3JCmORAjf|JiC|)?@H?Oj8VN&d6+^439Q9wVKez^>$ zAc5=vqmX~Bnq(ci0@r*5&=@NWhHM?%Z5bD-guK*)+So^%9})nt%3~u z-xLi+47v$DY-=Zp)^5ZQs}S~e0oKWP8}Lp#-p*aTCDiE7-TZ|R?%u;+IMzMcZ9$2>iYQ$ClZNeL4!kr1|*ofNhEh80_weQP`fj+DZw|WS?<-v0)* zc$aALJshkK!R~y6x1Mzm^Ok3_?lJz9&AL^N4z;m!lD|+JJE!;ywXyRw&rrwc_=QXZ zJE!^6I@XQSmOGv4H?BM6%jZI3E7mjuD-3?NqbZWzBY2w0)yNcKfpsFFGR_e~H8Ri2 zcN$m#JD|LVm-p*Ax{2dn9G(ytefJOlk=&GAtPimgcQFhO>(@)8hDTQ^YnKQ+^$V?C z!l8|;lQY;Ug>Vw;I=mvCQ!?xJ@TWA^oywomg>n;ZCxvR0b58~@m#Z}gg-Vl@nd2Qi zgU6uZeg?7Z;~1kpd-S9rVy_^vOV@D!J+-9&f}No1onZgF>x79Uu4(5{AvKBi@QK*S z4xNaN?C+k4jqGopppo4#M)ouZYecZA6WYl3S+$X!XVpeFEkl|qBkL^YFC=d|%lH7V zKv2I637k%GG||X9t9UXA?#^odLiX$3HSxyx6*RuFK0dy&zIfxCkF-t%#PN+_e0&qC zlr8@FrWI8D@y!wYD26y9aZy7Y0S8*w&Klx%I(O9zZdxa}t5Ljf)VZr!=dL!LyV`Z` z+M;vUR-L=L4BYjo;Ht;OUs`*T?dpYw_}Yse;(r${x$qDV=$&ByyXyoo#6=h-ZHVt0 zjSca~MrrJ(;@I6fN<%ywVcIy%T7+rjFg3K_9~I-fbGJ6WJNIbgyHi+5uVtOWLb`@^ z_L~MO4bvMi>5%fGa!d?O_L6dF^j(VGAciJgI5WX*t|x3GBO}A&st9hla7cMgIT?ed zEkz$YF!~}pBt~7o7;`v;4~QY$r_h}l{4_QVKS{jM2t_teCv*};)N z@@&Oykb}=TFF5uwZF@LdcZ-X`JpA?tt=DLK)QuaXi5o{4Zx=5a`Do&CX!EP|zt(p* zVZT^L&f-1VH(BbFYm?M#6Ui_LK1i*&J5awpLfv)~cH7It0@Ts`ER<{+&f zAf#pu(uLo2W01}8*0c|8L2wtJvC9?IGFAf&C0D_krH6LH(Lyuy=pkOh4pSAwvy_c_|>L}`O9>xkoF zbQa4x*x-_f*c^UbZEdJu8X5l;rXOi=VFBV8`+;(~cYFtC9m0DOHcy9Lo3&3LkL6Ar zQRLo-(Z5B&E1 zaQ5ZQ*25w~m^fss4Nv5kh%ly?$-md_wOa-sIxx2ngHIgK>Vrc40V$Z~=s18zh6QOz z4_jN<*2aP``fNSuc*cU&>Q$EmU8h3>4oV5VIi)5*3iFrAi&#W1a*dSiSMIBphYLe} z4#*nzKNfm@ilul0@?j~MLgAc((|Tm3An{1h#{AQ0ckZ{5zu1A{gffg<7WA^n=%C{F znxEje#4G!~BPDs45<;hha-KIXnw&U#LJqhJM+d_NJtM(#=C1~{3q=U3pgo3j65;Gb z4Us>I`JHB*t2w6C<|p_qsg~h4!b3sH`-cZF3TeuUNFR)Y^r6w$Y3iX0VoxLfa{e$} zkOvQlg1-#F2if8Qe?Cqt1AbiU!mLT$unckZO8#O;`zcKImyHC6UcIjvtw3Cb;euVm z1w+IB{VZ6;{8dJxq{oPoK}5+!jhCNvT_&PbaZ*&7pWwHoN`~L_5JhzWU1rC}lKsU} z3Fb!MM@}8U{lWx7@YoY)Bu57txgzTpWyX{AhIa)B!8JyQmh`__m}azwbRYC$!Hh1-;>vUs*|YA_dE2;7;4+;X9+R78Fats<}t!9)=CKVL3TTf#pWz*1F=CePtic=93dgO0t>14tO3U z+3-loP~qqw!v%IW2%-#ySg;xdp0WbnV;adq)ztNfylfhIYv1zN{&sB1d~zA zdt2wD!DQHcvsdq{(Ck~-xe3N>M3 zvOYgFKw>SJ+WYpL&;ZD_Kse>U5LgOU%fWyq)#{{R9ZDp4;lfP0YwFboK%G)apdko+ z*5q02+oa(e7iQWwyu%b_8h+Mf8h%5u_C0xhsvJy%#=z{E)w_OqOt1we5=oo^c2*K+ zby7i{Tu?nroE21qAQMHBID^taqP-6St*)_<3$%b)p!tjft&Rj*Koe+IF3^1O1RB(z z4_MwL|8B`EQb|Dm?GhL)1p$(4R-;^l_LRV!43&l6PYBxJPcVbCAggx7YX>1df&9}t@cAB=Lqy6mQ=ph&l{UZsuAs#&q zH7kFhSd@VLUS;PQeM{`piW}IJr1^bb#rA4j-vZeB(g)i5LM=O;gg-oBPztsU@3#<} z-km908~LTwv_siRm&$L$TdODBSQnPWi=;j8Q?Tb{(5U06lH9ny5f=Xs0yd!+$mO)b zRhu-Ts5k%RYFHVeI}GhEa zw$KvxD*=F2yVmmRGG+puq|>Z2h5660N;i4H7PvN-G|JYt@8iafHj3f9w^mlH z3B6b&{~?G@i{Tt!ps5#k;A-KCXta;+ugBLwc!f{>5*~JT82tB2tLYB=-2B)D_CM{N zj>fz`LX*WZ>I2Ayv>T6s0?_hs2dTfiH5Uqg(j}BBctE9Ak4B^139g(J2@%$c=_8u2 ztlWwxSR+jCP%PX!nGW*#4IX5?J6@2;)i-qD#qFh!@}c+%nauehSI2IVdk_6J<)5Eu zw4nZku%PbMP*Q&P`>~PZJccVHr4%ItonO^$sU4f1NPunc(;EYKkv(f!m3nslep(Ohr1reC({`sJ8M0$T013WHpyFv#_Z%k8;2ZtR+Kr#SN?&7@+{G?PfR z@q`3Kh$BH2bNl7ie!XauEw{^P%Poq(FrwUc7|*hgj+YE~< z&AM3-ZTNj84<{TFi4!kIdoA`a)P8`sMyLyQQes)chuufr>=It&`DE>_U3q>HEx8zy zD>3_Vmu?NtqrQ5y4%EBFu6Ik4=$8ADFlJi)+4`ZhN-@yNt<%bLep)Hx=b=^#mZnp} zjpgaoz{``hHgFhzV>oI4@jF+j3u+1Td_gU`U|y$|e$Bj2RUZFVivsN#|NOaGJTqa@ z=rB?Yxdum1jJ~h8tQaV-8M9N0`raR}wrDScaH=5OYcmOl%^XIb)fuGYBvR&A@kkSw zOeDO@Km-GhM=E3&BG>rP2Q!-cz%ArN!$&%27b2x#7$T%#nJnui(dGKT&ZG-(xyiL4 zg||FL9Znfe2?J|=^fdWM*yrM8AS}QzGiECmwB?~0s3rDZauFv*Y@!39LjQaAz zv(tvCFQNY8y>?6Zc1zKn3!LNduXxP@=f?5{&bHtJ=XWa>IR9Ar;r<+JJ!0RD>o|g#X%;G5@tEiKb*maMh8yRe%u6v9g>^+ol-Q(m_ zmV!QId%wQG`6m3krh0+%A^5i){v`l6VETv2Afvr%i98uPd8TO+g?ebcoet67aWr2( zCnoS)zB`93{Cqexv+1g}eJ_E)&&^k=J(-(>NzKQpdAR|F->B+hxy1^d6V0w5_IRf%*P)qnRvoEfxO`<46mrhBXsqc3t2(fasnrGYp0;ykLuGb-*1XJ zjqf%2v}W9HitQmct0Y!!gaB|jK*t}ZNgjy@bmZF-Mi$1JKW^TF6mr1>s)y zS~SY>h+&}dD~3ANu4+l=H<;j&u!n9KK1ANbDsKt9;V6M*(=H*`?cu*G7GX?U1myS6;+T-cQUSnb;r7ZdawCAA712PxlV>Lfi?-bGeF4e z@(6daTqnZ-FH&zZfC&C^v$+7I*JGVg2(dJISL*eQc9=oBL_JlpdVXUBv14c&;>F?) z;g3@fAKm2NxDIVA+O>2~tsA~zO=Lh_Z%8M9&(tDZLaXI^I=p63Ib??DGnbRCm6K*p z_sYV)$z?3Z6)R^tY=TeG$~hU1hmS`+6gHHT8Y^dyxtw&ZoJTPoNCPFfxotP870CIN z5tk!0uIOv++_W1=NcL2_#d@ESu8B+49{!Xr47$l0ETZo!`uS6eu;a$ne7E7w5N^qR zz-Y_O_cH2_+Yxz*FaJHUM*Y4$erFA|$M3BFXpi4n+uGxI)X+sC{d2P%j~=P2Ll*svTRZZd<1Xia z*(~5)eA-Gs7zA-I-|>^zO{o4Hdnbp<=gw!Hm;jJv^V3idX@xbyE54 zG=l0r7PBBuRR)LaX=%xCPU;j_&2VfeGt)Ar8tjKTG`H5)?1#O&{jiVx1G33@IFF2n zQ+4BEmu@`FZ&AkWzjkY=Pro%3uc^&O<6-@}nOVleUb0yA(Qh6{b)Z$dY(^AcFHD?i zB^TiA#|^ayg^l(B-8|dNx~nwfzHV`IOr9{zCPrVY5ni)}%@($>g1v=3YjKEmDH}w z9jw6UwU8>j7Wnuly$nhm1mj){gd1g>A$5e;!j?;yI-X0DI_(!Lb=ryHch8bKdvu2H z5e)vEVEBl5@7EcAzs~T7bcP?&8UC2g@QP0MS$5Zh-0~X#Qpo7;N$Z7O7It5>)cLz; z$%UoPR=pGKe|McQ!)2k^lV2nsRF@Ud*1e)STw0A=_dDru{Z2YuzmpEv@1(=^JLz!! zPC8t_lMdJKbnEJ97hUCSxW{?vzv0EZ7&5nNR{Z*_|NlAk7t-$Z9ku?!Xf(XRCm-QA zj;a|Ec>g&bq{}a49z*3?X?}RcT2@-cbuFdk zth9`I&!EO-^%+g$GKC5h?`hPRyv{mtU&zg~I!Wu%Nt;dXiP}lpg-+ZSO0+Qtu|~h- zurIDEBaMl;s*H9l;;OPe2ACcL%yR~qhykYG0JGl!W7xlN&5M9PW`I`=@N@>#N(**i zU0T9-k{VK|s7YfZRMfO+7b@!02ADGjm}vtHE#3tTE#CzUE#L)Ak{C2j1FXvcn{0qh z5wK+2z^{ty{3x%ATP455HTpuI-}zDM^ZDJju;0O7m35LOQYTp=L6gYJsgq2cI>{F4 z)Ih6dR>UGO>!_o9g-z0*#DSiR$VNHHmI(JgdZc>3f_C%LyUCQO8%>G$MY{ZQT&Ww4 zUDN|FM5DE9!yV|`v9hqO8@)lK?ri!+VbIo%eje(-9Ya%_q{_DLpHU*}=j44DqStf- z0s2F14Nb1Cjn@1oPFz5w5ciXah35DT3f7f1WB>G7r?lg_4`8ew-M5Kuoo1V8c_~!C$-jqf!a?~g!S8TF+ONsv3SR2b76Js|j&<~4XFKo?ceaTX zH#^9>a1)K$9b`SdiAM2`6ntJ@#&)>rp`7jT&_fm5kxCEMY=@5?YS@lEdRWVLyBmdM5YG9o5{{QZHis;(c0W%ifLr0uh@#v#HVQpc%v+}v6f0DZ;DQnC zeFF^d(|C~oi9O$^5XrPwGBVCELWPM-I9bO=UmyM}qFIc0jYLKdfOI@CdXU=_k0}pR zEj%-?w_t3v*T9`0UfbdisVy>kVxpZopx0Q`l{1PVGdji#8YxM|ve0&fN*XE27sq|n z0qTzjuC|Cl&41O=*MXub7bnXp^ZQ1EJri?fzlWU)=UKCWH!~!Eid0bL2qz`bd2^{#_NOr0_rkXC z;h57syA<_{rW#TyT}_2V7>~Mh*W=24issyPGVISZICrIS3wle;f}WPpLA&tm^4rZ$ z5dm&BA22_`Z%F_hH};Q|4D-$6MR^xc&ht1$$cdu|$tB`MxFBhCa3pvLja6e-|2t}M z0@Tpnu~wV->hPhZhJGorg;Fk|6+pH9d_I9j3=dvHGcIWSk5I9v5&t3nFkCPU4~T;Q zB(&O}IyyMuADctdze4NpWZmOSVZkBt_<9G^{U=9)yU^ncZJi6lExTbsAs-$t*fs2b zT(k#znRl=n>)=W1;3>56^-rR8??*tI3pssp>X5IGauH5nW-=-0?=j@)SV&G^CxJZ7 zKO`57DLncf`F$yTpCb8B>V988K?Qt4DtbpSDL5nt$3lH)dne|E`nZ?G4$2TbDF=rP zzF)<&y(BbmF5&hSEH-#a#J)`$zHu)J-3#o#fK|AD1$Eai{D^K6O)|6y-6Y1cde`S8 zzP_eu35HSMl3vL_gfd}>JSFVmTM0w>zJZSf;4FPh7fO1%QXu5Gg zI7$>Zmdhon;X9I~f|KD}ZSuh-2jr5m(BA3ZKhBW~hC+L-;iN}!i76En%LOM{&S|+| zDCRMNNo7+=4cH#$pwEPaK;$;@JMse?TknAiuv{n(>R(b&3^GbC0jkbH$BA*xA#4mC z!W7LR>?ApajY$QHqhm2Tgq7gEy0?;)3kHZ)s;Ki(5P~>#w7>sXkih;3q{#jnxxgcs z($xkMbhxC42M=`NX>>tVC=$)uFBNzk?N4GZ1yaL-D9G2Wy^Rjo?27~I@tQ;oFVip| z-N;I6P}I+vSRfbF;73TK)uxixgy6K$nH!lK87Z+bpB+e>qIm3cdtrby1P1&8TBZX# z2|?|gyO-9kCvhcc5yHAk-Et(3uzv6e3hNqOSnpEqtI~wEhY#x6^7^8Oa}6KFHRdPy zEvW%Q!qjj{PHcQRK3iVPiL4i7wQ`^^9DW=ShE>T46xlsyN3Fj_WPg}5>LrS7%KD)q z+w8A3;_u-P!v&8cYaow$Kq8?}n^9oH+Rr4iYhWyEBHIH*dG)>_O=Q;#k=--w-=+Jt z36Y%?>tninGUyVX%YN5KL7MZ5>>-UhF_ArVX>M)EW17hJkjSoyiR=NthlDT4E(3^Lgvmz_;^x>+0W(-n#cy>F&DteU`g!4fks!X^o=9 zD6p%z!1kfQ_JIIG*ES)rp$9y(1-1v>bkU1lvF6NeMW&@ zGfQCC=mHxN9|FX0)yIu(7x5{2uxMY8ksT6s?y7Z=n@O?k`TT|TO@J5g_h2ers!t{_*Wuy6QhUIGp-o~BN`z&(skpUrG*@Dci3po(lX6pqdZ8i>k*i2&x&N2B z>j93dJnlOCOb#OFj*ASU7DZ)_qF~cFSZzd6n*(%*no36gCkrf}G#xSHQs|^p8yMjj zxz_3V{5Z?}6;3GU5<@Amf$-M^V(X7%Tb6BHpwssxodSuIxkCbs<78+MR(F5%0Dm72idtunwI!5aQsV=P)GY=7&7ZW{H;%-I8!nhgCKO+o+MaFH7z@XT;O)&5ow*iAA zL)}C0tpxKUp)U^u^COeP(nvS=BxGE`gp${Y~=Esi0q6kuZslgNzNuKW{Ts8 zT7l?rHyFYyBeeE1y|$F?#Im@lFDf73*TvN)7nQL0=u$JdAmuuGTR4MVM?{+nKC5nX z9sLxpqpc%w9X)c4T}S;iXCXAir4sZj+EysqR)V&rN__{*)Y2&m{2Rei%#t$9l5&2_ z@4)5@_%$|{knMmRyP!Bv32Ej)h2oh~#WO|V8QBo9O8v3;+T3wge^G#AR{(Lh} zM`|Y1Ul@%hcx?Qhd3|K$Mjttj|2gCAQ7VeB+F|+&;;TAIsl`|AH2uZV8|kN#H?SX> zhtTg68@uy24m-HkD~EpX$8n{7i2A z(8F=g$FnKEs>%#(kFQ!_`bPohh_zvQDg(}2M&i64hzAM<&U$GjFsm5MCJW335|+M; zpah-b#u9<@z&Z;`9SN`iWkR3?#R}*35tOQzMZd`a&WWy>@Pe+H09$ko3&b@7@f?CU z8f627OpKO9+pi1qXr-F?X#3b0j2@YaB6mq{vzer!XpV-6742b%Wh}6}pX>+B(D&!2 zObBgLYV!wce(7h~5$orq?Ffzcd&HEu^kVF(Ib@RpT_X4yj9z2L5Y#}TCWe>>4SSw3 z#5HKx^Nb<3LBpPB4Dk&bVjE)k))3tg!?%X`h8VszL^#Cots%xChHni~4l!G-L+m}< zdym`Ro3OnXK^sdh0@Tshuyu|7FcJ~!^!lZ&xlGnv1~gY>c_lLK)FhcRh}N|W*y?&a zrRVZ_UVHfN0?~8U{W|Npud$lDoXxw89g=(;O^0PYVVmUInb375d!g&{_p+`VN~$y9 zO4$`E@n56_PO&9$U_u5xmiRBQDZ5fAJD3Wg@}vo6XMZj|H0VS35+eu|K^i?+Jh`^h zS#h|O4tN@B%|)8)edCi1on}AAti-8LiPI7#PUl0MW<@NRZ|Suxh-KGnUl|jN#x(i|i8K%_Bx( zf!Swph*vw>Wj+s+VV)Ck~7LIuSu!e%EZ!= z?5dVDD=7oN$5IBnp>20S+wO$6ErGV}w6tygB-*yiIBfP9M{$y>+6lE_lFZ-DUpu6A zr>F&5A5?b|?~$IR-|Cf8*d?kqZi=!^%(!b3RU2PbECEP?t!JZc$H7@_9R{l|F?$B9 z3#GN*ItEr<#b7qsl$+Gp2{Q?+x4MAa_cHa3>r`YA_Q;QW+^;B#$$TCtL{7zo_M%X7q4xb+cN zTD=xeP_Mr1u@&xcoONtF(r6FRKhm--vKeb>S(H`54VIRzva~GXl{JJd#SpWsA#5pz zxMdAtOEJVQYlvHp;afxGatz-ZVwYq1))2iM!?%X`u}am=xMZ0}uXdvA;F zy$HzWe1P#Gp&+gQd(`&Rps2s=*EV027UCZ=V&sal|4WLj)-yVDaT@qaE;& zZ~+)<%?~gjFnj_AWJakt0BQt`2;Mk+*R?VJ>uyCb8HSZKz|kDv$x&cxm9F`5E$^(r zHNVrunN41{*T~Z&vB`&!1X>5ij|5r=29Lu%zO<1$q()7~3nMjrLEI#r}`3 zaUK8n0`|Z$eN;FJa>-l9(!0bhgW?nD6Q96<%})APB=FhVBz96I{b4U2$TsvVy`71r zcYsWXB7riaH&R{B`A@}y;s(xQ8Yt(#9;4dN8v@!gunsg4AT|QntkUXnU7(bX5Rpf- z#Trd8eU}rSL=FcwBwz*HBqPa>1V_nW@>AE~zpE|ou#BuSWn`6;k;Q&m1yz|1s&X4t z{^V_Jc=Q-xDH*YO@qX!MP18EtG_6lhQ#V5lqU*v-M8S53a_G3;!Qc_)grtL?5&eX! zL`6l|J}aKZV1y4FeUk7DHb9^(B1BFQk*P0Vju4$O1}As``qTjQA(5;fsTy(~1S60D zmyB3j>L+OWCs&?6TT0BDdG07d_T)LDMIhe*@X3$Q8Zq{D!zwZbz3IFmO+T_MJVl6A zjY0@+0VT`?VKel;P{U3OvK@pRDgWmzHVSP<^|XXYS%{NO&7^}NI4eqA?l?0_{NTmu zC_xAlK7wtE9D}_=5J5i0BIkq(7C)TD+?ZN+iBZB11zqDi^!66WPssTIq2-J$cO^?W zeyV?XFda#1rGB%ln+kalsSFKi;0E+|iC@z@5HOy4RNRSK(}7TP4d>34()e>-Q#s(; zRSvigP>)kaR|w*&A6->xiip2an6nJT)%JhNQ*?xscL|1Htr6d_y@OVn`14%1nN> z3^{2H)a%xEThBv^Hx2Rp{77VY1AJ3z4FYr`NaZ$={7D1|>@#AvI{yGml# zfm~d_((zg%htO&84@49p6_G}$m5y#S4znAZclEfmdH&;h4`QwVA^ zI^Z&J{6My(li&V;7N#^b!@GFZgrkI=#sRfz>Q{#t*WM>Prg7z(9dx&@$BeyS?(Z`W zTgw=BnW|-s;YclG;4tOPsx^kVN_iRcxFfxE+U`g#o%XTyQX{a4ua}oj{MX%zGdXZI z(?mFu+dDNr!C@_(o^hm?PCFf`r4y~>Q}0C)&e-HWW?nkM!OTl1c$azU^rg9$4=J~@ zIF#W^hPP=jK44!q@x9g->Q#C>ccE2JM?(H;+zo!>YR+j2K1qI?xA&F*8U=aOE-H0@ zQ+4lD-S=hb{wabOm`-X5#nj1hTlGn}nl}wX4MscRYL53CcUGjfRY6K|d4iJSv}-@6 zhDCc541+nZbvo2~oJMb-#Z)qnj$1}Y!Wu*P;;)j8Dd4LVoHX{%gHF zt5;eULwyFAM8@Hpvi~N)TDt?C>A?iB3Zlp0f)NI*7&<=qS=zE6>Ko+zb-k&r*%ZT< zrWwA-VtC)VAzgOw_EV=P;ON%nAkyT3chai#j|2F`=Mw#I_KrgUOE(}eon7AjXs#af zT#&T+q_bg4SOD4NveQ|>ZZ64|J`-teX&|)rOC(qFpJA21m$u3icB?#xoi7)x@~wrP@aT-PI2s`F+G`8*(2$@{o#o0) zjaHt;mq=`<_!3F&6kj5tU0NO$Sn{aQl1EIFbJ-kSF`J z?K+(u^uL)3^bD!zAX7oj%aIJI z$s&ht!f|QgON@+SO^l4<&fn&W-uwTN%O>49MC~m2Y3i9cd_lS>pRN@^;PDN52kOpS zjsUW;qp#sm?w{&M`FvFfN324w#GV;*^5hV(9(dU-q+c~Ffv%X{buQTwG|#Is8|paj zu%Vteh#MLc*W}O1i{9PDdqLW*AC9%`Rq2C~O)}caQ84<=&gdg$#Dz-|YwrDc%nWmD zeEDv3)?+jbEPBnbK~20z6h2HB?#(#MAKu5Wob@=})GzYcy}2Df9K;XX(?2vflO&(D z!`ASRW*8Z$XFX$UTxN#fMdMCe<9Ez1bJh#ChQBbwgShKuTjM2W_&qf4wt=IE96~d{ z(?rND=eg`2dTVGuEhKyC`v{LE0EsW(Aqep)4m_VXM|=5x-i+h(C2z?-2tbT4-zjMF zE+yU^U%r4O?P1vf?V-yfQb|LxO_&L+I44|=YcNmPk8AKxA@dH$(XOTuZQ1*rjMM=h z_x-Q)4&(5wIPt|H$%o0&*#1+T46}>(W3hE;c3}&2Rhii}bX2xz_cQ1U*K*zv?Ufzd zKHmJ~gq#vQU3ZG(r|UlD5bCKW zQ#clyz|WxD>iEE*Lb1+8OAOrEvX>9n=jp3ncDMc|4N&dUXq1nVFV@@nynXZ$KC!+; zU*TLF)lh1&=5^;A|Kdq>JmBCAB?%+Ott!UnfA~o&oVSlf!~BezXmpJ{iVy9lszt18 z7$v^u1N5?hm7C2lAD*v~MJ&y#um?9_N`-N3ey$nDsri+}6F4=$(q)DbAsE9<4Z~86 z;rM(QN9JQVJ|D)Zd7@Pqr{;-VVVs($3F%nz;g}rkcc|0<7wR|oZg(9BjBSoowRt^@q;Qq3ufE5#Ua|^dRvRn3>KN;Mq7*5 z3>KT=rgY1_=j#`93h}qbj%A>0W<|FrE4sU~q5D`Cblsc(4XqPU{$2*kp$wF(GEn~O z43tYUP`+p)%FWJe28L(6Gzpt+?ukBC!AFd$Pc=nm6R4c!b>q+PkVLQ1zsuWqpVyP1 zkGI9^X`#;n`ZR3tdiHLZKDY7M171(=g^!e^XUeEXGllQRK^W%r7?>qdM*K^}G zuV*8DF8_ho^C*3;{-M`%(c{zi{rZz$PX~S6Kk|Am+2Qq^q|fM6Ue7)MIep)opYeK* z(I@a&iGFb2qKZhcB2wkc=Ss3t0cSB1L$_IJ zM9GllPy$ISIxamiR!ZF}hb3}J8V=JcnOx*c%z2Gu7#_O`zb&@^`01;}Oeh8%?sT`d z%FFT>_Fh}*Tt7#zq1PJD&C!C*8@n9lpIlezTsOSmU*a%OMA&?Sza`m=gL{6YlDbGT zmo}W#e*)iaMd|b|ZO9q%ge`M~BzaNy^KtypEu1yU%CF5n+@-ZEqC^EER<)}paq_1H zsSM9iE6?*%68M563tN);GgS&85)zXChvb4v{*%tTnYidKtC~q?ebp@$&j{8T1S68T3`^a|T&z?QqsI zskJj-)!M=9jkb~0(-ffdJoUbUR5_m$aF=a;8a=ljp~02q=@)hg&wZl()*_>*I*RE!O?g^uYl%c%Hs?r=jH z8>~Yw8_R&LBhP{@zRXvXs!k)&E%Q~TY6_qw;opdw0s5#aF(UnLH9 zlurcBs;~oQ6Gpo&r!@S-e7)pbHAkc~zQ+~)IICj=Q}iCL=|}jR4aJEgFF{F$umTD33S@XWkq>qDh+;=Slb@UD zrC6gNPfP<+EjA$4VgphwHXzkv15zzEAk|_+F{xI1?2ULVDb-M8x)cy{FRqBzRxL@T zUwnD7trSqIDuF(>z1!=#tcPqc`aHSM>-pQCdp#z7iVoyvLy>)O``GG%j8ttKM_gns zL~c8=aA5TdoC|K%XrgiMXJh(7uD4h{a$Vp4z^G%5FY!;$k@0gvKe~x-_|ClFjp_Tx z+0_JR&%)o!&&An)-hKWducwEa8$t^^)lc|=;oE)irijOuVQz8D>qX3Q`ZAx(z2}D6 z1f+XU^VeuX@&%eVhT=QtCiwoN9;i#I1ekmp-1|R{#HfZWd z^|7uGF5d7<9H~Cp^&6yG_)R+1LaC%$SRs-+tNXuTYZh>At% zrIZS;2WMB_eT@eq692!&o}3f)9AF0!xr)dt+kh-)-*uWZj0opYqgk`Ti6)m6zcQMT zi?|qhcZ+R#cOPJ(uwj(%t@8QoYR%f{%qIb#wyI2hM#RZ=CiEMeM3g12h!n}mCn_?a zPSJ9Psz*bQc0hRMo7<58P#|UTwhcKF1xyxn+xTkZzQXQ&Y@*6l27kC)yHk&;m8WCu z=kg02;c#@YQU$xZ+v!1=I8lb>9mdSlUxXG{D2jt zktVDAj|@>o!#z5$T)v4jfFAJ z5ibVTbM>ow-P5CgKrXxA?m1t?bocAm_3G8Df3IE@6fm1%l^b@p?}#Zj5*O;zP>io3 zthb=JBIohe8=}St4Wo=kd%TFhk7ww=462>AZe27 znQm2I;c`?RGDR(w0Wgpw3d9irSR5w717;$v^&>Q_e65FwmasOwO+>@Aste%}>^hU0 z&IxZbaVKGVfDl_|4iQ?*#}2J#goRm!=J_+~*(xPPeur17*pgqj?@76isBG~-G3V+w z2l2Npy{~K%{|FHO*svQDdhJ(c-!?>T0(le;917oj=B=Tv5l?Z+fsdAAKUXG4z3Y&WsMErnkjHJ)z75r+_548fC zRQhxUSN!5!Gby2$ioNOG6+{+AU1!ajh+Y#m`XwI*vXrtWHq^*$8VHZ&%R{4RNpbs8f}zGYIzq-inIaF1bsy^F?E2uv z#s~@(Ha-hGh@}DXU@!{i;xkpl$UrUa4&+B;{Z^8d{fS=;Oy|?bk@WljvulHc3y8Ek~aE-ox zW~EPh`KH+uEt>q?etP)u<{qu!{T4l?1yC-J#L7|YkCt7`Wow#IGv#{xY-mUM7e9Ws ztVLP1e~MqsnKh^thmC{3pewzm8_hNR?BBqin4{KwTn)f+f|vYp^cq03|43b>_m|^a zOX=t8wh6hqt&RbMZ=JdRp-bF%=N zMe7}4=G7};h*|R2v%D}9t=92(a=C0j(q=WbT1Vhd?c2o$TtIN}`8x#d0m%mYY2e5OnNWgn-DZo2iB;bwK&{h9wX>9!|E_^qtn%1r0rkZN{ z2sbTN-HtCs(+bseA+Gu=Rm&2(>e&zd8aN(a3LHDHH@o)eX8XTxaC3B^3&Y%uxj}L)5mGm@=rd7mgiN=CEQXW+4=(a z=~1Iwz1)r7p5<=zevw<+*j>ggZRGyhayN44tVK&3x!-F?OB=Uym%DL$6ZdK3_VGHj zv~l~*IyY{ASEq2xLhg}WMamp`1gLV?UzX=UnM{OS?{lspTQfbHbDe&cIaJqmc#zJ3 znN$m-lIJB)^9u*<8)j^s4nZDX^rCqtlkjB{{!Ai}Ndz;AP$m)1BqEtaG?UQGM7fzD zlc7}E-N_2Eq0MBaGW(NN@Qb8R2h-029AD}#>6;ZJm!S@sUF}oaWSw0B+-FF?kJuKY zA>9{xV*<7}CTRO$(g$+|EGd1uj;o0_o3qJmALLy6sKm;d@==Kuo1N+VQ=XFEC*W8n zagpFcv?%lY%E~u#XgwP(A*~OwRF;D6aaFI^&w}X?%V>F0r=DOO6<+{Z}xHb%0OUF>mo-ewykO|6de_8*s^c20U< zotu*6zibaWa>*M~v^sP8uwEC}t9?z`#$etDypy__*n>W7ZU9oDMjPJjgXpb=bd84@ z(;I+hBjz(7W_)kpngB7s`7m!W7a3mjz2;IM8hpZM#{BY8W2rxTZ=|Kk{Dj~6^zHQN zHhi0PN1H6jMtwxtIzk9jjSnsJOe=j_GkM6yFn> zO6IPW{T#6uEmw)JAtRtUG+XFX#GHmS)`dlV4Z#m&#v;6_upG%TVro-?Iy%F1Z~NZ5 z8uVCLZ;ZeXjE?k}I0Ja=#ooMetkaNEkiui*q~u04rTxIHkFpcS?-XDlD0Xd)2FL={nc&Ep|19;9<|yw z?v>RR<$CwL+U{+l)h7BhDRp!^DkjI1Ubo4y(>uZB5F;kY%HSbhOR@?}UF%a;Zn=6= z;aZ=SygsX(^;w-?pNd=H8^SJly9|_@xj$viIui1jZR_><`qvP`A=@J9eN{w)7+`Al zftr+TGRRE_ABtmceh9Lr_eCK&k0Lh3JK+F~Ky$z8>V>=^Vj(&JG#NIj^cWd6Oaeq5 zF-*A+(Fe>*UtoH2oIV(1M5xcjYa@>79#y70d!kf+q+q^nNJQISW_9s zVJz(IEYON!N3P(3aPX=!&KD-hM3G|EVbCw5{2>u9csISTbT1I!7?w^6>C`VRzKHa# zH==qgY%XomXA126Rf;Ud?I>P1*)z4pT1YW4%%KuxrF%tS77=J6t?NeF^)UC0&OP=h zysaw3ZbTbj-TPwdKc}CCe&PM<=ky+^BxI>v^H%lRXxGxW*m9`uT5OJt&a3M!S0jFx zS?q`7jNYFG?j*hYw9&cM%L=+D_=gP*I5$F?ICTFUZ+%2envFvlpO%_`obY*w&;nswe@CO$gEA6ZDF&v*=&oTDpOk&C7IeZ)MIKZMKPwfGE`z}E2rt2 zZ+DR}TjI!@Y9Vc^`d+1c3mI}%tLg(a9q*Ko?OC%1oZ>(Y@JjzAag}LkUfniXp7or5 z>|KbY(%<|W>cg{`!>+oneSGwZ%HXMayyFhnqUS524ZER~db-{;fQH3|19bK z4LDX!+^nw9=cedQ^Q1m6fPX=s)s^f5jF{C`{3B|{O0)gUmp@smxA>a$7S__RyB*9X z*iq1Aj3kJgN8nP3HZqT8r!x_KC7b77;@RSKjzKDwvTYvo6PK@}%QDBiQf0;+Ko*85;q0fDJd!;A<1XKD;nkT!3qX|;Cbo#Tz8Jeu7 zig{4KvJz#Gu)gtmQd#cq*@yYsYgODl&%=_A-g}ImTQAi~^R3*x$@-MLdD{uY{yAy> zvG<>M36^X`ionh6Chi?c&E2ruy~X!J4qq5YbzdFAp1`Mnf1rn`Ra(fwAFM zapf_8HyWbM)HNkpwSQsvAgX>#IE}amk4@NEm`039`P3QRgT47OtN9^K@C&AVrZs+z zS-k#}yb%^9#RO~`iv`=*MEOJ6-B=4_hZ@b1rfeMsR|<+R1MP>27Fda=1%Y74cHB$| zamTfUYe~TUSM7MNX7kdhh5VJJQR`QsaFu3Uxf&-d4C{|;)q)@fQiPK;ZMq1Tm)`T$*f~V0ON{k)iF^n{|;wu<~SrbVZ zBlZfdK(al+HQVT5$l`!ooQ8FPTYQG~Fp(78cE#0thWm3hm*9)62Wp$ODnV@msBH!W zaV^IKxYgV!%G;*3O3`cyovHss^Xdu>lC;BfVMBu_8ajem0 z1xW1rsDr>%1(28eQx$TwpKW}vISAuk1sIuwWpcsMtApjUpzihT!wR+6ZTfUQqJs(B z4Uq$V%;4GESgC%Q)j&>ldZ^m04#96tn1j=rhLVwpGdS_J(c(l6yQyi-mSW(3_U8t~ zT}|Wb^st+{++n0lC(eE!vrtF5E@k?&73(s_qpeumj_7LTJ&h|GWdE@BmDQJWJA5kF z8s@MijH>H;Lv*x*_gu>|JT$!~BFh(EXp$KjaoPF=1zw5p@?j>+J9EYe4}+c3>&)ajGnO)wDKoa-Os>}(yEBPy{UMk-nFsM#&%n`}*@VBaGWB72l>M#QU2y(A zi`>Qk=Q}jy;OaIS^uwoF=l7WppXDEZ^Wk%aZUFIbBzlHn?X&;4P@=+rneD?vVe4?s z?ZDxTDIF#2wt@Pk%Y3>P^xLtN?o~Yt%pPvtC(B z7Wp!pdLw|oL$T6jenq#MDdIZ9eX((E=AC3!3RIRZ{3JfXNrrdp*$7WKMtHNMfJDqk zl@T5_A5})UHa-t^@Z`ZVpSr)yZ$1j!T>o-hKaxHWv6h;oaW!kJvUmD!{rhZ6Oa6pY zg;odB!vSOYx%B=(`VFrcJ8K~HEOy?ggFmM2oLL>Dr|?KFKT9{NBgXRc0O#7Xh&)(M ze?#;fPonmdb6tO>-{I3g9w&0454wt0-(PAMtzN5W67{1IKLjFyUj&m9B#kF1mF_?2 zov)$_Fp*sCJ_1kjJifm(e;+C*_D?+XRRq%G!MY6V@A}*Teqn#Fv60Lz-Ox{2zx$gI ztekB9t=>j_=coK0c-hSF{aC%%e5xp6^C42JM4Mo46)-eObsb}QWl zwwxZ(+W>kyV|NOWfDl zxYtV}IK=ybXiC3VHYTi=T_F|aA}{lc*=Dbouz>b7Al7$426Iv<0 zMe|HT&2`lJ=q{e?D9`l=WF=iJn$HVjB`ueaGRQ6hKf}F&VDg<@phk$ZVS@e>S;kKn z<;Q*D>5w|%HA5#qdS)^t_qrhY5qH9l;4^vT9c9E=bww_;%mOWdLakS6L3R4|qnSQX zrS&2%qo6zqce1WS{|)htBN^VCS7GT8$SWKMyYOaVunviyD1Pvl9}4!I?wDmK*kXoD zufDZmi1>eXYpQI2ueP2~4Vc;8?0$T$vFwb^LEvP(jou~8QnqpsODj>x)kjH71yD3rWr`YMd0t^8SYwvycm zgCS}*YW+@^Bg*D!=3memll)6MyotNxa|ZS1YX64a3ndi!+Z^G^2>|c(nSGoXu!KZc zA1494(?8V?MDcyTu|wlUW53n=Rff9(&h1VOb$*XlQG#t+gvm712#08nlgh4k!ASC4 zh75ZVFtWACV6w3YND;8DpjE-8^cga}7*uw2(VVmw*`uI~4!$@n7i;KZylByJb9UBu zx4EqsEwjD+4}I;&?6pqfb2A4vt|3s@==i@YnDMpYUvn%NM}hVHn&WfJB$#opH?vmg z;6Pks7v5#VlWeN-=K~tC=b7{q;Mpy`7zp9D3PH2_4SKClztf-Ds&Pu>Uik-sp)Ec_ zcCXgWZV1@VBbhB4TmKCpUgTQKKD9$$LZc>Qq%oU_G|@ApW`n1$-PpqI5-YP?v>z5R z8o{U<=@l?GvA&k!Hs>EA)vuQu&<)P3MG9cA_CvXGSU$B+R>oTTc~E|?qo0T6XUe+w z!)*8CNY1t*#?_&**HmmO8Qr*b54zdTzkKv-2mkV$)|e?S4*k0&yd=9pDf{EvzbE3p zzu4p1JOJbT+5u2d$;MB5+396_JR+=cv_uJGOv&mW1b}VPn4E^l%(D@>RRdAM8?jA0 zrVxTVgu`rIq%lFakw1wKG7%0>(j%P60jtNeb2jeiHRM(j6!MWtM&Vh(7@wJB6rPhC z9zo%GA?Qs4=Oww?B3B-13CDwV%kAv8knbt?PZx48(XdZaqh5~oi}3GF68Xo!x5#I( zw@JYF%2kzIZIW%1UaeZLw|G^cATw@bVU^l7p-)|l!>$cBhFb)~WIb=u&IzpN6oz3x z4-rl=E=1$`3BxijV0)V!{Zb=!VHlj+#xM@#x-pC$AHgtLCfhZ|4@wo0Zr2n~C{skb zonN3EQPc`15jepWM78H%nbBjAp!uZ`#=*rTP6 zJtF1$diAzTIeP}fGJ_mSxK*oCbfNaBY<^Q+z;EIXzX2C$qV~AcNRvpMCZPsFoTfWL z7(kL?`|H`Q3=Oq-S-p77VK{_=S^Q|7fB^p*-F#Uvkdm9_a?_BTiaD+;nAcRnysmf6 z>-dLwAV%vM|@o=89F8A0o4?v>qM7)PX~e!DCRS=!H1wr(L-62vhyEFOMmoy5+a= zBPA_QnEk2j+Mwr@?8?yk2Neb|5-UrzWR0%InQe2+iP0h2y!<%J75@Lw+^6*1&q zQ8YlQJP+80~8@fq| zb-kCCElK?A^lxBRTC>tQYObD1Uo0-k&K4T6=;or4jmJT4iU{eS``b4Y&JUGf4B^^ms8s@WbWW_zG>uJ)H~ z`Rmziqa!~NsXM5ql03WUb%G#yE<w6tPP{S?oM3nqFJU zbiv{6DoOto=Wv&dcmM3vGIU^Mo4-K%Z+7d}cF_S>k%Cn%ig~-qYH)|S6zXj|@8HC> zRRs98q%SW8$~*YxVJCtk$T_^(AYgrVbY#3}^liOJcINeu`G}|Ok~O(Y7FJ`d`n5wi z2m(3T>_lE-A2Yt0w*r9e%P0Ja1G%O1za{LHdX%Q&`ZeA<0v(Gc5V zqHfSkbqE!mwg=@C9(Gm+<@Y*z2ZOzhk5ZFF_2qdjUHM(S7L$P4p_vgac*##odap-n z4X^P6XaXbLXDs*Shc&vePAy1chL&%prZxurqjNE-SJW0z>SVS!j3WDn|I zMnXj7R?NrBN7U<92O_qpu8^UtrA3)>fE_AtFnni z)Jo&oenfQwNL}HG>I+7c5SZJgX}vw74jGYpx<;OIT@wU}Ig{B@FqvyylX*2O z?Sz`ZFK{kiHn}+Gn2GqlW(WQQ44xtKrO!Ra4B^#aCLMk%mFplL~wve&wUcrAW|Jm`Ui)(eE*GHZv2a^vYdCcMr;%1wMZXt(d1P~KV!^M z7VCNcChVw*^_mJ1F|ge_jgCmV&GXs@(F_05i@+r1jrtt(23Uq-d~n_-4YpxTIBUl{ zYmMem)7T*d0gNPveYA75w~gC{L=`%3F2v@iCSdbZQ(&_O5i~Pm z^G0M;xmzLAKYlyS>~YCuy3?x;bl;>7a}%}5LHyNz_(FU3!TGr-h`7izw*S)BDdOyF zT%QV>HxSjdBBy1mpX zN#d#k1IyTF;Uri@v40gWq8Psl8BsjH2GP-ko; z;$Z&ZU)UaGLg;JVxGyTt=V{I zk-Z3A$wH>h6*6@Wn1h79E6Nt~`{jV~7Y>m50B`wBH$W$qp?PQuyTzw7j`V)6)HA?4 zMRBHW4yfWlFyvXN96KZ^u?clz#IQiZBGJXlK(4~d7>P`>!pi#c@K29mqh65vdmK7< zvd+LLe^sH;PH19Nq8ro^cR~{z$f**bbgDNHMMiud36UqgY2EzGTn@Z+?+HW62Wn@{ zweRN7+myAm5(D1)w|Qn;_ajN3f@A>=cCvsLP8LvOCkt?;vQ#3#$rE57uW=_wnS6-E z4g`)6yiVw`f#AGBKL2e81JiWk7j_S7plcZYA&wV}3U?+zBG5dVS$EJBEMYe+kpftz zI$^;*(YJ71hrb=NgOmqDd>yvKln28OtcuCY>2%4LFqysS5)eZy98G&rr0O3udk<@Jd8r)2i6^C{ie#?m)0mP-phjYc z(JiZ9rH((RMxu%HE;^=6Nk#-vL~PEKe202tM~rByEXa*~NZq8|Kr`%H=Y?*);NZ%V z@!0ZKDAJkU7ifG!l`_R{%!a8&ozIagdkWU8N4L{~zq z2_i?t4O4fa$gxxV_}!^}Cgx*OnKv{?+C(Nuo9F~-qX|3il>d2&1#do)b(CMrA|ksXYc*;u%8|xEfBU1 zaU=H&>(K*w#((Gk{5Qe)Yr@R+dAa`!GyW@{9d~ZLNjnNELWb7?N69!5G7$wfERHBg z$Y?Wx?{mN}KxJqGD#JF@7x43}f1=_Ro>ic@Ic3~@V+1%SZS)E7R|2+D*IG^c%({IN zNP>fH+=nLMK0E>Uk?Do*A5Pv%jEz~Yom%!-*!%zXMeLDiuvJt4*~u2#r>-9heQQs!+-NTlH$f(9Ek04D}Hcb8x9 zlQXaG)Tc!Gfs7!>m-;iHY4mm+(FKfnrnZp-lHrB#Ol64beFf)s=e)(ykJa`;G?nGf z4>>Dpi`!cLZ-0%{|5JC+>Nt*Us^VD1Qae_$Okx#P9(GqEIY>u$CONna`X(7Mte2^C z>mg-vF%!EAYtrGVGYNLzG=kmt&mq{=knF^Rvacb!$zC-``@H?(JUPPu50jWI{&zbc zxr{AJ^ue$GO!<_LW3>RL>`QEam%HrmQusgT z?apgqBj1_Y*ZIdzW>!w!nvh=sm6i2~XDWy0bgoS2IO}vQd^$g--^IcM4hy&Q)7W`M z0_`@H`>RTyo0Yg^$)0jV2z>N| zAaI-Y3%NV8RY*%jK`DVCn}}sD*M70)PB|J-d^lmAc0mQFmcii__Ps)Ir z1mXhlK&F<D;-wPy(%VOq1bz+V(gMY!VYub zXnU|0(LyKl7e`0Gx@-e(U2+DdqBP1W;}K+CFiTJ?Lr`L`jsEg!Jp&(R!a1qLn5;OS zGyY{~3<#TXCzGeW5gIPpJxojBvwSeyuXfm`*|T)En<|V6Q!p+4PS;bOhAWJ?Yr{#= z@@C!!>m4fTeagcg0?C5Sn+7L}6W^kFX^3(*(F)FJT3NFD7)U@lC@1b5*wU!nS$h>~ zJy*IQFQ2sXrs^keF4=k6>~bpX;$j8+-huUtFehqbS!M^i0)O- zs+;Iqgh>~FpZBnncj=zdVx%PEsi>JqLp3%DsYDa@rgpg_fGQ@~CzWpdWW41BcW&Di zcW@BY6?}*79G4wrZ=P3-4{-S)j@{ryCay@Bkjn*o#!>D{Hm|;-?R8n&WD<(4hD|lP zS+Y6kY!ivmR&E<@6=JkqHBzns6l0Yn?k_IcyxkdoJ|=|UbK*jf0U4s8(wS=&5^-fq z>nb<*PF;^iHGzo}I3-Rx9Guv_R!r!%@~Uw{CYLIc@UI3Z9C=`%G2Fj~Z~aO?AK=ICc6Wi7wbS_g3_6_J2GEWZy*P$UC9UQ8PCJvcf+m~ZOL@%f%sIm`3w1($Mv z<@I}_!$!tCQ`?xyIfj+Eoj@&_Vg7&?7pTV2oqy6|iz1pXTHU^465*+@;+;<(z3^1dp39D1FQaM10h=?sB zqKNl`PkFYAd8_q1F>mUSOkTD5+-1+Hs#n7HxtgvIFYZS^I42$4E+-w_(Vd(QZo=Tz zkZqYZ6~F?69rjr0znTELeZGgdCQztgS)k7aFpX=TbbwhZz<}x8iAXLU8L^JSVeN2+ zwO;A=clw~%&&Gx*?~8|7sTU|Fygqu%+13v^M_(BB>8y&`%xKhM+*0Q-ZW&?-Hw^+^ zh1il*0lh==Y||i~ed)SeR_Ag#Ij)`I+yRyK+Vx#`&14QTEtE1XXf`dBxrVUtF-J+b zPJ$n!5@&XSRYRYCo&GgGB(pMiOlLCI57Iik$>SM5KX)+^EfHtQ##g~es+fX_nY_e% z8&kLKnxhA}-FVTW5uCum2`D5YO6Xh#F#8nFhOA8JH9kI%!#b%g=D&R36?Hp0cj)Kk z!ECdpHR%JEcs0W-F)K}`MeBL&%_cQX8X-n3uv(uo!0v>?TBUpS4d7D9GAc82!mxk2 z^Vy_}L3dtwd=TfdLh2D>OOnI(-Xz&V|13JLDv(GrbV-_*;T+oL8JTJcM3srQCmn}u zzcMF%@(r2t^llv7pHL>~b~EgCWh zkdCo)tvP@o?-eE|cF(YbCewF}|GvzB4gNdIe^2t?U-4gyVLiuxk=-fV$2th7ea{Rs zsSbda6DH?(HwV~vDa9s)?grTZIlw!0yiw++W~*ub0>(*vmLvcjsHPvQ%>nFL>H9N- zQHSJB_Sl*1IFbBsnZXMFi&STAcNgY6Zicqi5O19_+Z(6A5tJ&)$P1s3wv}3Mj9$!1 zF7nR^6RJ#bPB&ynUh(#soTS5%%sHJo$$A492PamlQ;C(DUD=H#I}I zF0FD{aKKe{x5AZ+ywc?l4s$$nMBwmBSk6e6bCxu+&&{HS{lvtl z+MNo+I2qBdl55LvC)xntBve#71wb4Rv}68gj^R^ktM}{8wr=YasSt48zM7t_+UYf0 z)*GMcrU#@WH=~3lsl7p2SjTka1*8S{%!TO^fc=lLi&*h}+uzCc4yA5}Ji51zv*r9C z6Y?Zl)~xHY<^4ByK;8#e@ARrKF?~qA%ZoGCyR4ff^)A=#xG=fi<=vO^^)Bz+b#A75 zm*rDG-t~6X_0!k8EUzo5cPZJ|TJ*QArmgcm8{bOZMw}?fY_Pg)NpZ(xnPB%#ON!Uf zzhAu}^UQVvy=VXF_tOBLc?|0bH4QS)?67~@`RX`lyqz!p_e&OUEcKh8K+5=yBdG-~ zG*Rp7NIh-Ir>pm#xR{%iLHGZu9>CGxo%y#`4}hq96I+N(-^iwu!g>DAT!M(YNf2cm zzvz?)kbHT0sbJUlFlTpqkkOdF&{mJ`+Y)ANz-((aYlEmGTN^?h+1jw#)^65D%(f1* zHfpx5f!Kl}$yO)6-I;5JD&1j1wY&Y8|UMFHl)m#u1+^E_QYZyvc*m~{fw8UHD1f@Tb zZK!LczRH?7Q+=ySoUr4h;482v0F9JB^aWBJiwM7165D@EOJdcM;vMwwSDTUa$@EHf z6oaNA+fv7$uVzBV=gBIK5&!fPg~b^Hnx4icy;&>ee8CCHDQSK4cNcT^H?O|WMp8aQ zoXf0Cve8<w9V%N>hnGT)O{%f_{uket#)k|4Q}f;R-pds(appV7@)onOfRyB*VoW>>ewl^) zKTBko&^mLhJdu3SWu^JdiF=`{4oN`jkW4;K1L;_xWT`qNL8ruCa0+}X*Guk25*g*` z-Qba))cf^*{2-7Y^!emr*Y$F57i{z1p1=(>{GvreZH4+pSfME8H;ecEC&Wvb#pgcD ziPyQhN-WS~p4{DShiFVw*$;v6=KpuK{bbn9PHDM+-L?JjpJ@?=7=uu4#q6J__rVFH z2R6cb)HB=J)wgJb==M0?tysWT}^e{Bcu_ zFZl{}=M3)yxN=4p!Id+(cXLa7Y@S!#V|&jl?y+@nOM7U4H-?t>(AMM18QQhK!_^LT zb?IEe&>qCtc8%r7mi@Y=`uy6Mji8M~srMAKPWFK4WIWyjd?9*a9syJ3!+rR>Ij;Sl z>h!d-8ZlpQT2Er9?}yv~x*)MQCnxSNChA*=hvsjk{)5f^Zdcroj1v92g#NX)MR0A9 zyl94GS;f(9|Chb*fsd;y6P;-$?Er%rWC{aJ-B}$a(x8C^O-Lz^bq z-K-AT$d+}g!G@49N!oBbwP=m>ZC?eghzss^k#&PsOq$X*ZTiQG?i`!Ms@?TsLnzwC zLJRZ0Kj+-JlcWXr?e2T;_j~2Xi%ADM46A<&5JXvGP@Eg67K7&3|Pg&^iO&ylbGW=qMOaz8gM@Tdm;9O3nlzb61 z9Si4Az1RlZjL3x~H}ioLl4UTCdrVg|lvf ze#rQE*Fu}fkuJ#D2X%!*`XFZyM-@)#gnYIq>I(EkUBRBHD>9y_E6@{l1$&~d$atcz zK)v<~<%#-^*`BDCCXrGBmc;uLCh`81sau2{ViIhZE#s29fk?0_V>%NgD#}Qj^r_xJ z9Z^2jn`aH^V!o-};$pt3@!Pq5Q*#s_6(GuE28i;Wei!#H%So)o5v+cQNz7zt*m$&a zq3zimmaxI9CtkFR%Cn;Kfvt1AWt+6543~$%B?2 zHl3yJHJzgwXu7a&D^Kh#Bwlj>RZxtyO+&=^{l869gjk#=z}azr^aD62i;qmO%YnQCk?&{y#g?2&gg(2>v5$>gt z+%+}9F%Zk0&?vnn>^RFWNWz}JL%_6f)uAd`b0{J!4%Nzf zLs406s6o~mYL=CTT4aSG=otBqFtRbf0mA$oUm?tIP-1>*iQL8itT!EBqhG7f(~nw_ zKqn)Coi}7EWrJ0!L9gBx@L;2@-gS$YtnyvFWiMSNvgL(l%lk-J zf=xTA={49k4YNAa+()Q6Xy#&-o5~tk`8&soq<$k>y9iO<9ruZ>6tO$%zPXegq!`i^z!;)j1FLED=-a?8w)vb2q zh)UYK=iK>p(ed7)yszzfXis8q#DdQ$!3XhR@CBVoQ2==$4nl!O!2L5_?w-oOg5nDq z4{ku4v%Fp%xsxP_vvY_gjHE;YqsLqvT8XGeimT+X#8q0qf@3NBjE8*sl>-JNabJZvcZ~!6DN_ z4sjA4Qle5HX@|7bLqZ2>SHR?kV%6fL3h(W2wTav(o-IM4T(KCcLAhxC3NyrwP^y|G zQn9>DERe2cG`c~EZy#ib&ZvzTHop9e^XbQSc9ysk?E!mU@88+z%Tn7!FY$avM zR)hhxswc^%O+9ofR(8V@x2eHbz`e>sFJ?x)&JpY!iXg37(xUbz5s_l*vl;m&Wy%`l zNb*tI`2uYWIQSUp5l%$+6sdMFEY>545IgmVPuEbF)UB5X6GIwmRqJjQa$Jv4v=lvp z7`39`W!XD|e!1OqqCiH!D7CbjNfLVcx^|`+Wz_*17aOcj8ow2WB7DPYkS^vM$zel-iBTsRHY+AGD+prY4DXPUC4* zb^!tX-ey6jrGPF5H~~F4;VaV-yRFuyaX;d2%+BsRYEvqQr*lXDr zI26TNBQnq_db2VeUsa_j+^O<{#Ia5-zL5B?O9cskQhujWc&TQ5Po?l?W&cxnc}n-@ z2zooDe{-L_K;hPh`yLV-gc2M@r-&wqo}nEaJqtLtHbrQ%b|$rlwykzHH6oNAE0r(i z@+D9{ulG}LrqdHYZ=~D4f%bPHvS08^HDK0`=fJK?nx%%SRAN11+fzA`m%eQmKl#MA zXHbH%7l%n8w0o+D&jU@Dtxm|3@rrU9jV_I=k;m%N>wKt&opfpO+oGJpCvGha+pyYHAGbu6UobUx=Rre_w({z$#Oh}N&DST4E zB**ExpHKJu@rYu>tJfn&%b7F+RSL6Xz3;oHh9C(p#og@j5~U<`J3X6Dryod>Cgq=^ zqI+&CQRq(9N|nL&wkrRY%>Q* zPn=lz!UbQr_zMkvdwtq5JL5+5^z~_{nbDv!+^d_c4NRn9Igue7sb7(l62FDT7t(?i z+DR(lhn3LTVkF5osZV76ZEheB z42eN}VLtljI(?;axBI$`5f}VL#Sk}i%#*tE1-PyD!SN#dNn`EbEF|r(<<1(|Yt_kn z!YgY?HYGO~`Fdt_VBxRMr^Pf(K?^H@ee^Nyff4}80sR9yJ1vh?>p!*PfcjU^6(&px z?vORvGnUzg?`s7V)7anUu|?qNknux&ifuGEY@@ zwVNmz8);jtdj}rRh+Rqu){w;Hi19aokQb$sI}k{%AWRSaz5F4a))xkPVQ#(`!y9s47Kn0G-4rG|N0(s{^4>nafQWZ2pnl1VkbeFoX zkD4cpqkHdGhO*q_-+l4zbYb_WE++q1-~(wxwnw=xnor4Sb5j)TL$B%RG)W^^`P ztg~5l35!T16;Db6r$fqBB&o=RQi+irp;% z2SWSDV&5oX;gFWkOg8BNad1A=wLW(c(KcEpi->vlNqFr=RF5LQZ{F5n+{J&m0GAKb zNi455mi=>D(Ybj&Puj9c<6z!Hcv>=0=$)~rOh9nD4*`L*UhZe($>dvZBTu!lt_61T zS8?LiZdPm?xrd2yW6m?j@VUrNHg8+UMQ__z4bsVWjiCH)4t;C{~og!d|A7u$L(%Y~)O^XT_*g!bZ--PMhRRkPCP)bz zITINrY|@Jf0Z|+gH%J}qsvl0JpGEoPlI4NUWu%5}QNf-EuxOr}+>5_$9qgh1r*yE< zD)9!mt!RrC%PqD%7+F~@HL+Jukt)4m=Jq}xzicInY07&^C;JAzkpo3TGxU;K@4CNl zOODjK^^WM95d-Ppj2TL))~A$e{Yt4eVA|mLO&c7a#ReykQLhz$oQ1ynhJEaNGbWeo zMefk23Cr^v}pTHyRJMBO# zn^~IbTcngqP6=D$Tw*Gu>b76*NjVi#bvt0;5-sxHXR2;L4YMG7pV#wEdSPJH`-mOY zWtZ&8QZdhHb+3$9U#dj{3U(+H?hIeT7ei3cRKt2W%*5O zI_AxpsS?;dbwHtr`jtJf*`UUVzttH#2m5wEM}?b`$Qe?z3ijpKYXC5Q9VUG!#Zto|r-~+UUV0cC4ibx7b0k#nF_fogTbmM+ZH? z0(Q{@tRF@M(ch(yDBTzRNJLL~NrN5WrQvw`e0oV|Jg?`IC=96zL{Ykv+)D8kYK%`E z$yRnXRb5+BhXg9J9wIr-T6r&^ZB+wFaAHz|6PG#2#%U(Rt<99+w3~}qn39nnunHO5 zAt;4-9Qr{DtEjLktjafqRbHuON5bsVqiktll>A@{Ij24QY~1H$pOi;LNusiL@eve? zBsaO9dXvmXV=PhO-Zo5dbMXltSK9>hcA6))bF3r4K_`$oGFzrHoEn1*^9Wv;N-i=* zk6tkXzxgtwkc25>bX-QiItXcD5~Y-|C}k%Sk`tn4B9&YoHV!^HB_(dSOsU7CIg%62 za<|7Da^wWEC|nv=ULO=$Q|t|EwuY(D09)xyBe>GP3PEUVU!>0eSPDB24F8Vb}!Ez+}oeG5|h ziTz$2{qYwONyItZsc=nbhJ1W6958zz;lx|?;W*Kt$pJ*8kvS2mP7HZ*4ZiUbiWA~D zXUn|a1>+@-H_rhO2f`O%vf;rgS~QyXqBJX>91wt+;|Bcsi;^&S5xZ3G5Pc2S)SpnQ zQ}i`kQ-5g7D8yRoYq6Hurs|7XQxmGbwoK|5?!_qEeQPZ#O{i_x7q_H*64mbd+AS%i z7>T>D!;7Gu12+QP>&Yaq&&4!*_6TSz_n5ECgQZ8~SCfM8ib%cG z?>+YqOij>N^;slJy`oTR$6>Zf;|40Y9?ouc>dLnDw`GjUd;!C@%f&>Pt_2e3=H>Nr zTuCyRxMm`E=*TT@?#uXSy%;4z76y-I70OL^Fh`vsJRB?>NkN|g;Gn3;TLU|;-aJ3J z>ClTmcU2xGzvOl2%k4)#{`KqVv{QX>&7e?&&-#v&=@TJMPASM>@&B* zHE?(=bfT!xQWaHnxGN&5iXe0RYEues6JU#Z%EX?|!%Dez{Z6y(b&1)|agm)!!o>MAq(0hK7-08EpTm41L%RC?h4t8~ii z?@qtG7p=fcE(A2zDtVyJB(pV;mmY~V+^|H6#Vmm{0-2r_tDY^85j_xFm}%P2lr2n zaqQbJ+YI7cZHWz*Ez$LyL6ikEfdNBIw>slD@9U#fiD_M>F9k5P%dT&tAWm5Z6lgSJ zEhWo51c$S!(PB?Qe089Bx;g@08)_8-zZc2}2(q*Ub?xDcz&YI+%VdKJ; z9-F)@ET4w(Q+CB+`LK{vUD?CEo>SeYKO{Dg;gjDwLl+X*3BnY!zB)ze?s+piJqGDb zq^HeNkHnM)>l3z-QAm~kN?z}uAs5E*0I}X`*N+;d{R?fqLwR2r!g$6T`*6XG8~bss z^rPtg&$vGG;x?{?7XT&1(!&I}be+UrqPkw*sOqv~UAS1r37Iy$aWk(K^4DeB9x6A0 zOJ;m`qim)RSIpQ6ul*c-H^T9?ap8>QtYlm_<8!l;aoLQfjb_&aMos3$Yb?vW_>2<% z+DGywG)gc&wH;;`?LnFlMQE%-I-xOwL_%XNQV5MvBo7)JkTz&+M#7-61*w9@7)-qe zQ%}LePi~mmD_mb=bXGDhukkb_b2w<3$BI<(MQo#m5d~S6VA{3VzRh+Tnnl8e%tr72 zbb9}M#}{sOi(JwN*nx_yMMENYJH4U)y~ur(o_EvpPI?}q=O^fyqHyLuMb8w5Gj|s~ zQyk9RXXyDDJrB||MX;2u5j7y&8n?7{($dx`OIwDettm@eXDn@XKHh1abxlAQ7Iqkyx=C`|9a;B{9)9(eFFK&lA?q<%0!st5z5mM}m( z%>jzMIY99?2Phct00rJ1=%U}P^t*+ADLn50MV1|)P_qM%&@a(c14L;J5d9@*yd@_y z<3z8^87Gom&Nxx+a>j{pmorYZyPR<%-{p)G1utj3$7D$A`=CeEKJkEvC4`14Y?Otw z%CZH$8+nx(CI_51C7om|;XCd_cS2#gGf|O_wk0ak(Vj$hHEl>_SJQ4pb~SB9WLMKZ zM0Pc8LS$Ft4$PRyuzh9eEM|(;zH%yu?JM1XJ%@eeiSrrz%9lGYXH0dQX0RKRR>U{3{zV(u>liFF$ZY>Hhd-5#z@{^V+a;orV3Kf&R|? z7oUSNrT1X$7`a^}C z_n{E;M`r=gNrqjE&p|8tU1xKK`+X&%{G4bgapde5}Z-Y42cIs)FsNtMAR+H zkBO*9l)oUNUQwPBQJ;R-cz@Kd-*vJ-8qn`L)gR5*?=t$M1^Qi6{ZXj+On9H>BqcWWyW)oY9L|&F)64?ZpIy1-FmsSMXUTV z5J=~8&X;!P=WYHa(wi@pzVX0&V5Ul+$KF@|RIWszssElrn0sR7`C-SGb_VpuLRi9L zSRh!DPF#`wGgrjVqc1wXpV5^nEy?943%K1=^@U91FaHj|0e# zz%nb~t$QC*5OwgjY$w|3jL7~_Z!mfJlUR`hD@r_RcIAmD&91!hq}i3HpESF&;YqVA z9Z#BFx%Wx4EBl_bb|w0xwJV=^Qg%hbH|k9?6pYjxN8W^G-HgG{tLu$n}_TB^UO| z!XYlaNEZI^*KvwyXJyaN$E@>n3%md#o+Kwi_}S36vFpm}=xKc0ET_iUiEjhDWXS<4 z`HU=Sdx7{mZ%29wLoeaWN2FKO_${Po zgkg@yG5y_QGpe<8DR=#4V%sk6 z85W1MKa8Zh*oK~N^xGXq`_HG->b&f- zZ|uf7-q?+EJ2JMf8|Tq(LY)2OL8QpxGnAj4qPo+^GXQB{H?Bek!Ql7I_j*6hFWL>se}lU0&C-K6;o>*197_ zvIN^tI{~pt*^_<9>}X9RffS0MZ8p$Ip<4850A!LaWfxv)vmHf;2Kr`f8g0>;oQc2< zpqD!%lA5`7J8|(MQ6wi#i}_?XwvpLC$?n0d_r#Q&x0udMJfPZ4Q&rS@l@`xVws>Y! z-)-Z})p3e#C;7=GwvlOq9hy%c&rr{BU)Tw`a`)t(g^Od|{LARC=*IImXy7BX*-aH@ zwcCv~%clEJF*d-hFQTuo^+i+s?elr4@sHw2eaWcXwAR1L>^xNOQv>kVSJ$kNoiE|ev-bk%Q#BjvqdU2p#1JXH z`DM|=&(5&Ym;2jn|CU+p|J~N}=?Ledvj=ljLyVUYYjtZ(F|l3Q@+*Ce;#fzUNajb81nkt&6+K5||{pk#SR)+S1J zZ_GL`%ASriZ$9Z-QKzqSi%>x_gj}hDde~a}G0D%=UxyW5{H6?Cn%rF`{Tcp1BDZD8 z36V@t&7l~h8p?udkyRptq1sd&)NT@abrV(JO;XQp6n#yg(swt~_Y-J<*zMx9X3_27 zC%@=E$GMv5c5|9rEOjLIxWv-4XjpdIE|$(VxoVo4AfG@JQfvx;n>phQJZf{8Sv8$q z_pOPaC$|#Ab|RJR2((Y6c(^iNvkcVTqvCht;Hiro%A0z|;hQJel%kA=$97_c`eiVX6k&*?0nShyk1v0kwVY86Dhr}ScEjv4+@p(btNJc zME80qq19JXvuZoMBH)84>N3(QfvI* zZMrz}n3f(H3(4z=)4s|HAre&uDxTl+R^wr`HD73RZlN6TodJBZm%n zHYE|Y8KZ!*ssdDN>qZ2MVv3*CLufcq z*v}ehg?b2uQRqe6d(q|V-pUdb4$a&B2)_jg;&*SinaT!is=2C=_~fgKN$9hxL=Tm* zJp&ougfw3BO#8vNY#)zC_oEDqp#7a$?LRTA{WA2B?rPbPY= zi48dK$*(=d^%nqLhMM^t4z475PYi8~aWifpHLNcZ!*l~hh5zCpABGLbC5a*Hr$5PfWHjG*8%_^J9E*qe@;68B!%}9jXyuCZanxjM z7oj-oK88Bx&;ZkPwwKcW*a%n}Lx;B!0ZW4(aACsE8spHZ)Tjh?ny7;wc1XNUrgVrt zgcFkoQ=19My(qO%LmjEj1YIaQwLFeF1Uy1O^mjA$$W}Muz87H@F#>p+4adOM}=Xki;WIA>F4At`fyy9JCD;9epaCzp!8z=B({3l`!olm$0t72KL#uvjc( zhvI8caU7ls#TtiLhTa*A#Tutx8EopGPi(?`i~mBxkI9p0Hv-xfNW;~MZk#;S8;T_5 zMcj?k4ag!1XBqo_jxdXEnq7tU9^UOe}X5V!2+za`PpY%f2aAogw5LGoo22`YJ}zduY}HMc+zu zFp-iozc(|9Ji|2o-2dG+)&Cc4s{dm))&C)z>VL}iU*7+sSC`xyAOp{CIu+ftnY&5Q zN@GYJ;!%(yD2+s--(@@^%-?P1zmUn-cudeHjYxXUpfOD<1fEzb(y$wFz+L>N(ShlH zhTl*js3dQ5#pSd?3X6_UY!A@3fHMt`(%D1_Jd*U5F;e9gLsCH770onc%Q)0ve0BnN zJ|nM$TQjCQAx{AxFj^NzYbVSTA(u1EcfMW@NaZ|768<0G$^F|N@Dqy!6HanMfD)WK zv1uR~d)&`>uugm$csa&r1AP+f-KY4halQ zK4CX`#O@}};qdq-o|F-IAZH)LlqSh17{8rLRl)R(ND2Y-yHq|&R*_(c0>Q9}W`*`v zm~uCApL+ODKL3%4Gz)uxFd2qT^CA>3;dDR$;f4`$N*yNOOxpuxm~;>Zw`cP3>752uHP~w*42v!3-l;o*+C-CYXh}{x#uAzs}%EKE{(| zRp=XV`~d1lr=}4IW!@*J#o_UcQnGtOAvlGWNLfRZhsBs+-9&PpLu)adI*k9*lw9CB zDuj#(Y%kbil#FH)1Q{c;oP*c%LS_zZytGKDI_whYj;a$fYMv0}gjmmr2g?F@#q&Xn z52wW}Wu6qTD#fQn7L3Y*>csfCQ(pe#v&+8W_(sWn*Y-dG=XqpjX_m$ymq%47o1qW* zU=?Fh53QS=Ja!n<|CBuolwifskqA7>TJq&S?x8wTM=joF6jz1bCST8N6`En(t3fQl z3%5OhK2XSkoTC_>^l@d}z*Ay| z)yU0z=>8l=L+Q}a$&2$9rO`_}zXa)}gD0e&|HS!E2QP2TYL+)?g()hG6(7jihY=ve z+dYhvqgIkN>rl3*Ud%d@TLb5KS=o(8p)}fqlSxb?c`l(?OkK`1kLMxX zG7qWSJfwc}kUquTdV=BM0e*XmJK2sW(cFr=Id7+2OsJ(jb<{koL)=;{br_E40X|`v zzBhAVp(E$APbpRjbKXN=OMI$FeS))|efPYLgZhFz?DT@la+CNl&Ru_Du49^->zI!7 zbi`(zR?Ffu(lI$Ey9%?!S38QR1+qyb;S5RVxK{ik#VCt9wr6d#j_o;H_OU(fLTncP zqkS!1N8KC9+qCM{bQ+ygk;1UUmazw0J%>HmcrK7zE9;6*B>9G|*I(Pum!`M7(B03s zYd_ze`?-h)|1W6@Lf%>UxZ`6hApKk}t3CJgtu7Uzolu3b=4nz7A;ssZT#MrK8q#3W z?+hef%gNjHF4Dv+I$nARJ^jM}NbY+(Z_z6Y6GMQ0oJ;IMuP@%jP)X^JSr)cehK=25YM~00i{c~6e6BrU1gur6;PtJc0 z>HL@BPcg%vIDeA-{99X0s$hC5gx(%y)i1pghdA2Nd>QR1l!YL>}8rQyUMg0(xuE0^)5=)L5O1_K4sxhYOsp?t2D6B;Fs zZ=?=Z2@jm{VQnEryNoz+J`f)2UmRyL%A*dK;9Z_ zyZLnJB!}4f2%OTuj z12K#?5amtQ3q-VUFy*=%B-3au+b}2se}hADl{QGeQ9Bzm5Or7KNQ}5d#aZ;O^`c!= z%(VPOX=ilGLM6wtspKe&)Y?g;mQUm{0;=fQbvq|SlDT3NDcG!%pXwp|R7u~IN&8ec zb&5rw(sZ>&bsq`B29iLArFd6k(V$`&Nt4x?fT@sdPm(02c=QVV8U|7Zu=MtvN$ZOQgMS?)_#yCXHmmcK#APLx|8iKLG|)`GP{ zmGQgTPD@`xG1TpFqvhAIJD1%RXVUtC?%@LqVL^Z4_rM`;|IDrKNNm#$OWdT;OG;L( z6B&_iqSitQW6zBqo2>S$NIfU^y0FbhWSev37&jc0Wz+w$0h@6t^2Dfq0+6 zr?o`%o#LlL(Rb1=Pe|Xmq!0udgwsLldqEKpeaHC8Df&=B5yw%8<9PjjGsjVd;}}zO zccaLCDh8mGReq|SEP+MiVRc*A7>}2|nQm$%436XPpz4{@J0nG77bzOMNzvFtipE}2 zH1>(8Pn7#b)Gx{dA{r3o`68Mx$_qraKmaWoEfnR&B3dlUOGLCpl$VKU8T$Q;HkS^3bW z!8iK5nJj$R&WGBj`>#pEPcVAP2QUFoxyP%1jht25VRc3&8^lloU>DwWh;!bIzgo*`h zphOn^(#qdsTKRjWl|M=Oq0Ud)Hec`*m1bJTIQrrwGp$B+dpjfClPRwv@eh%n z%;qK(>S2j$<@X19wVAa|sFo9r`|6~q2~bgV{OL+=C6JyPWpN5$_2joe39zKcKqreC zXo^VCz@h}nE7zpdi?xCHDIx_jQYmK+sh4t2ivB{Su!}_l(mJ|@gjRgtqLp%+MBg)- zK{ zGfq+qNXA>rvb?4&%W2B83Qbv7*py{yrY!5fvh(^fH%qc+2|Lg&>JT|Ji6NJ1at}Pk zNxm>5nOUfjcNl)#h}DVAn!{3XiyLxi@(BE0J9$(bp4STEDS8{5A#!u^gf{|uqhxQ0 z2lb~E8ssu1Zj4JSi5t7wB#E2;NtXV) z&kO`zDaM2)B=q2oNZxKs2$Dt1kiFxw$@opALY~+Ac{>R%0s=L4{V9e^%+g|BmgbPm zECx%OQ*xVmJ^C~2lr>QOsecyR@qc`S*Khv za=Ys?7tBG|gc6ZDX{zSIy11|>OL4K>)X-9=TtX)r4y))>nnNgu#)l?hgEXO);vuZZ6LYSIH9jMT8YHU;3^-WZ^&FX8GR)6JaW z0z@Vl+Xxf&KU3M;MmO#mb>p6+8+Qb6x(VglYt`8asb`HjhbgC>SIc>ZG`tT=z2ITi z`lg(->%IXv!v<2G;E*70S{Dzmix=-fg34rt!M9<6Z^L}N4e>Id8?o^8hs1oY2r9_I z&qG8mg@{}V5xEp1aw$aQQi#Zv-&;cP?NrrQWlFwn|#^<~;Qp}c_p<*-_TxEPGTSLVlqf}!?>3=*_48{^l{ypwr ziYD~RwG>TgnI)9aGDjwk&@$(LeGnm^ia{*hZL!zDHB2O|A+27q#%aO{pShQ-0tEyo zuI!pl!>o2jjrUypM+D#T%^iHlf3X?me$kBjc}4>?qpWYo+K$T<6W-4ncZFi6Kgf#( zEb@tq1uUYF5OW4B;vO|}+=xb`c>|}LMIUYL#;E8cDx|SN^bxVr*epG^*X3($4?f-G?|Q(}rs z&@Ciy2e{m}NwpS-#39HO4zUNks9aecRPY$FgjaJNxA(j4b?X)+uB0cvr;|*UInk>g z-{a3o3!y@jHuRgcq1U7hOHA5OGu0vUCE0`2ouUfY5*E+g#5SVyS*_nHzCkZpQAz(b z2)@XgC0*De>A~1+dXPydn`visRxz~{P3SdgLd~QJOH7(De@?RM5NS#<$Ja@O6}u%Q zj<4Us3SPzR-Vy@w8j)4`imVbas~toTMtDjjQJ5hIx$lxB^qVAMp-B>YO_ESENx~A7 zB%G`7+1%>?c;5%lh5LahY@CxO4ARMt$dlb5Pqu?kIbNPJr`dwf@R^UAXTHV6XB=YD zIn$obqEY3S&4zcMY1o(oG^G^e1yS6a+CcLjLOjXDfcMGSb~BJ+Hfc(&&)W(SrU-B?ivYh1QGpU7z}+SR?q|V()n2Ve3I--p#!sKW z+J?76mGM`fI*%6kh+{n7V3&unj}Yj`oB2H;`Ur)7yoKNMPb_naT=b4-GRvI80F?N5 zBJ-|fgoIqAlyHj8Z9*M{Gd6Ze)o)^eNu0@>V!MDH(l)C(b>*>ZWP~79bFFFe6Ezj? z8wfg3uT+r9RGiNBTla^x#EButmI<58UTrda#p>+%+i2N2YQG?^pSa(NIO!{SJzqnN zQr^P7-{xDmmgbvo;SKH6YR+2%;fL0^dQMP3dD>e5g5l=nNpB(G$=fzaFsxRN2h$pe zlz*PsBg1q#^Yz$5nlI=zl{J83xC1BXDixy=#VnNNaUR!V#7M#3R>{H-Apo&;#wL%z z-TpkCUuLY#ah1X~7jNhLO+U8yXUQth3w#b{@9GiQ)z8ze-j~_cCA6#CXYXp~u)BR9T7RbAh{Xatp<>|Jc{_P;jNn@&zX2a^&|WGa2@O$cOZhFKBmx3 zxjgZA(EZDWEZH;dy%g)t6nvr>SOSULUo}R3?H}?NkPr1=sMett(DtYV|5v z1{W09?vBjrW4HV|Q}<<18LTi3BR{hhuo5_xvs%i{>saUiL zpQcx?z=<%HPLqfx3=_dAAefrI(%-T*`Ym3fV+tRse`wrFi$fat#e9qd=PR&tU6V7QS66(1b>S>Y|VGhZ!qx+#F(4fCUUL7z0 zkm)+oZ@h7v#lz&8+sLtF09~|_%Y>?cuiA8udHl94N1F)bjpx+op`h_Je~r{jLvn`% z^EbeU+bkLEFA|}E@!+gv3SjbAnI3tJ#>|V?sK(2Lz!AO@go10TAz5X&!+C|-D8vG_ zfd(L7jlD0zZ0#^XJbHMdPjw?1wA=AJx}V@EVLbDd)e+-M^aTQUE=k`dYwdRU7R%RA z8)KF)n(2#EZ}G+duzc|W`oegNFJhK2E*SX5)LVS!8%Vl7Q`FpN5jAVfSO5~L&lENH zSwzi2W-S1@)Mt@2Z#E^(VM%oY4%W}iF|Q;#gMlD`yC9K|eNtc+2J$3K1Y%B#<;*Rp zg3NMe^lt%=eqzP-?N;hJLHb`ab!S*l#to!)m{{C>QbEKk*{Q??8RDm?)N|k=B{v0J zQe8*#R3u+;t>o59F-xCR8yPewQJ~bNPjUyjOVZa63i(YNRB1{rJ@s_)+oN1;t#Vw% zcF<4oV>Dx3U6Qe`_A2XYikJiVJ^gan7Rfqx?$wVwM*;-zc ztra!dT5C?Viv<&sY^^7%W7|n>QAPzExnUkx|GCX`2s14MG<46loVkLEvcPs&|`)!h(XFZ zPLT|*Hb}X^DI8)UoZ6%!A`^G)&LE7qvdZep$t!XLFWkbWDrOKg3iXN_yQkgk$8$V4 z5-N1V6r7~fSXl*iTMW<;OjHJ?;n|o}tj9A2$lSe1_nO2+o z#9^#T+E6`jT9bJ*8>WHG(r9tUNVj^g{92QYEhV<|;7XX?%cu57sl(i(0?1XpE5w9wUL)5;u^ik!{l_55khl@OP{tEjjquAluW^ zbhDwm)Hr7yYqOxTdzS@M)`F+31yyHQNIFWhoZ0i(J&lF{t53FPzoy+#fCI#}nmkAo zVa_>jf3+{O=iH25)h<{BU#!Cd#RKj7ongJc3UwRxWr*OjZAHe!#bug+xJ?rfk2-4t zcSiwB(F;Y?nc`cV$RG;zNEbQLI27oSl$D5pK`aH(m0>#NV{+Tu$cXA7>@&IRVZ?`| z$`$SU6vMI|QXJ<_F`(Nag>s%0gEl5c7K|^-ufIh`WLw4TH%p8b?j-e)^l<6zlzcm7 z0#4GyrMD^hMt?VF8sgAyU1%1Y+)A*ark+BqW;0Ibkf)%&hD#SBfoHHj^{IIB2lzyh zwQ=}=ii)JUyE<;gxZ27n<&$(s9H~#Wt0I=nLdtYv^sCvKS5u}d^>`O!0yFb}PY2GO zdc0@udO?YLCE{UE1T#O%Cl}{5W0s7-WT=`h?TX@X`-6W9e%sDpG(`=r~$C~1cxI6Dg94B>VZ z0NY_Ho3C`k5|edxrH>?JL*yvn2pi)|JNJCDg_nNuq$RLMIR&T+gVw;%8AAv&q;CF2Dc~W&OW+w#X|M9RK%ou%Gf+te;{NUsWoXz2+bfni1 zC_~!DZ#?vOEJ}+T{yWa6MS|RKu5b{dnn3iSMqlA1D3o}H-o7k-Ti^-+LE)}R(l<%Q zGHQ~`W7QEEO!Y|YJVnw&V5Z-J=PuG#k5RJodcH%u%dyLj6c4J0wc)Y?#RGE*6b}rz zu^pGTu><1?$+)?GDORftr~tb8Qp8nRARRPS7D{Jd35IQiEGHPS5vrVEs7AO}g5eWk z$_a)|geoT(T;Q-IU~s|1twXDo8mRfNdkR~nkN{oJp-W_2ef%#lzU*?RbfQ|F93aQ0 zk>qn&AJ5FbJ}PUxwC;SmcPM@f0;Q|;dHP-9^s!bO@oe7xGLWjDv_+~{#7gd0;Yt_1 z>rDDZiYXED>UEBZT<1R&y7iEAd{mn6jB+dJ7uO#VAz$+Ei4YrIiDss#MNn8%pIVN- z9jn+yEKy|(QmkT7fRIW#&Z%^oUUOJ&Jt$b=AWS4lL!ioNTy-Ax%1T<(;!iN_eUJiy zH92ZfrVRwK)Ey~zqMDr4p1P?*hPE%(kI3+B58F6|9bV&T0AhS^4+fiCcMDErG^`)M zxiKf^vT1W-E_A4gaV)|(t{#1%^lx1k6hw-O0bKgld18RJ!vHPpz?69{p*ZuA2d2|a z`-o$SCRE*faY;vvKXbl`5$`^Rorq<3B4zX-cl(M+^~%_w*@tEq_u+^!urr;O6*gsR z#QlQtYX-;C=~sgRr#*gIow45aI(+2N_pt+A^n8szdY%36ApU?OF%%QpqY=>v7w}He z2!zNJqR}n1r$nPiXuCwCS7^_OMxW3IMWbJ6yG3I_XhWhgUub(pV}Z~Ph{i&pjfuu$ zJ#wtSu0#*N(7(J)kEHtRG(9}tzdWc%PWIP@_3)|w#nRnH9#< zpCwZu*cmHZ(^9u6xfwTawTb|wAMGboBI8w!_E~e1ENb8+ArGQHNhTyKs^!-A1&rlo zw6`ckvpxO}J?FhPTh{|0#h-P5uFlxqaK>~etlRGL)&HhZ zdfr5e@hCS==W*Q@Ss7bvPUq*ZgXzpkR@%p}9QtXYdmDBA53fm7wp|P*E9b#fXL=_Z zJjL7o@ER{~cC!BCqEQia6(@!riPu~yCp?$2@)uyJRW5X((clVnj3a)F(*kP}JOAIL&Hpar`FEd$7nyJqbG|jTYrdd{%=`$fB3GNn&#z!ScZV;>+ zf#h?UVA5HWT=k&jp@*dqla(D1|FOdmVNT>!1s9y9>B@mW=hpp7OgCA<=EZvC zwlPcmJ0zbhU;5O58w@9`FeyppN@q3B6FMr$QS5>Whhh^{I2CKHPa15oqLg%M z$lSa!TZXl!xOIdwu>PcEuQ6wk8EnR1uAEL|Oe9GNhpLoMtx#>oMO5<~7^^1zkq|N| z31#-bZL^z*C_7_1Dj7|OBx%ZCOD47|cZyRsX)k4ZpNbGFczE&%ps>%AkrOix*fdaj zUFJRz)}VsNo1vAJ-yTi;32n3{ir51eeb;kR{rm`Gf}yC?qFGC4;Iso#(>VhkA5r>v z&@1cFH8fwNGLoHZaD26qK+OU?; zTC4$R68JjQ4miVx`$#Yd7=E{fNx%=HT(AHCP5 z{@Njrb|IvADaC1W;n?YlPiZSC9?(piPw5ImV^Ao+_M3cyeDfRR7-%NPfXQwIr%ZBF z9PLv!ldmQ@jy*Qhm7DS@z%K2!W;UTDydsK0$?gvy<;R#}Kl1jIe&^@fG#TdWFsY%^ zmWdGaH9VX7s?G2u-)vPY+-_HDh2)(vWpsZ1YNhtU(_@p{In0JfO&uKqwi`Yq zEsaU!L7L4@*a-^~`mDDptPW+n2NQl>%NmdQfNFi#}gWLgXml%i-y zVt`=T06~C(of$%B!8k)HiE#ft1IlrBCTR_wTp2!~uSe19ix6 zp8^;PZ~6Qd6%{Uci}2egM1>pP!u(byDm?HO<+l%r3NO5A{N@)GK6s1q+dD;tAKsez z&2j6A)APP-&)gw_RwvvO7?lvM#M8$g8qqfeFpS*HFt6y?AlVtrOLn98v}wku?0G#H zMz8BRLeJyPfUbY|cj>g6$W|v$rKN?7&8nqG>Eq-ls&>@;-iWn6QX z0>}H1=s~}GkWN9RWc=*oGOx?adHF;}Zl!N)RE%x;%|oeb{piMqK!q{BPrv_q(hS-GK`DI9wBX_{!eb z^fDGjrt^AUgW93M4Ys&PzgB+`9ee6W)5m&-@_Hv}zN5X;XT|5Gw>=s8xFwJuJGKB8 z@46(jO!E=StYuMVo1N59OCqtV8zldU-QhuEP=dbVOfZE&M$C{e5eAftrU+BPM16rc zsdq_NJ^o}BRZvh3%TMm*P)y)~#Uf>6s2Qh~qp+hsUO^3EBS$4ZRz?lNF-RX1(=crV zgp~{smNG!S7@%$qbm4DZ2LY$X@Y^}_#$&I&feW-!(mg=*Xa%LY!PEWj4rgg@82)zN zs%WD7i0{DU7f`p4Pcn5|AM186Q zxG4asWPd(DiUS;f_fpMMWy#IwR0VHEZ?}4k-WHCB2v=k22*4npJ$_YP%>Cfi?$&qe z2aGqaxCV&Tq9N4Y?C(QYBO7;6__7_>)9(%R>!aUA^oxACCVgMyYSZP2XC#ZiQOxl_ z0j41yS)V@sgUjLXlE(bJ!g`8so2KqOYDPJ^#SFF#75UMqeFmk{b+8MF0-Qi$8=i7^UkEee3*`(tN8GkkZkBh&0_jrHIuit&LKNisM zKGh$~*Y7s^V+H!%Q~j|*{q8gUF&TdqddX+y%|_-~9)bQEue|a`x@c%g2qUrPePw8M zVsG(?j9>hNOU5tu&*S*Tkyl@v9lv<}Ynk}PADn;5_{HwIvj?wx`Qq`52mQCth!6V; z+>2&-*zbJQjKcb*513I{ul=AIg|!1ymj|(c9a8nW7#8?hF~$!KwPA_ZZ^q~4pTQ#2 zH_wcpnTaP#QD0xW!))*A+syVBe8_BX`)y`>uTTMM?|VzI05!K7QWN_=gV8!8x6N#A zYz&RDoz!S&tT!FMq4Yd)qJ7ATK5Bdn(A;_>e+{i_?}`4#SlkEb8XylLPXzmAa-t6z ze{&P6*sW%^m-$aGkM$0%%N=a^{=F345aUh8U^l2K7Sq}N(Ql9Sj>cUoz~v9$Gb6wy z)fb=70WOP%W*PX+Op@g}0Op|kuQttyu?P*hTD4YN#CSIy!|Z5;V`DR3$9CDX9c)Q| zV_Hedo{TurABt8+5iiSxreP$^s3zv6?BaU#M*-=p&?6a!OR>k4dUVZE zR|DN({Of)1$AE!2?qs<$JDy*;C+nT!Q}*Ppgve;38@H}`BMK;lJh z@I2U(y}8F`#Ft!y1{df!(^jACbaGHhCyKl@2k~Snyd(xfL?qp7JPM1WFONkC)59Hs zfh19m<~QR=uK56N3K>QcuSk0mtA~9oWOUAQ2Q{KOVX^?*u7(P!KD-ZWurd$XffUFJ zT_4X58fnIhjDOgS(CFlTq0vfK-+a|IiG9vGS<^c|z@Z-z#Sg_Winf~z;~B$GtFf?* z;P}l-87C6jOLWF;@md)rG8Bt{U{;984e!Pfk)fC+KIGY(FA*Pd!Cf35vS?_QyomR2 zju|l0e3$!>&r9x$Ti?SGAYnh!wz?h~iFj&YPhzM@b7LKT-}!Xb27vvQ^$x%}H48Xf z{MT&)dIx5#chq!Vm$&2}cLKm(x1iN#OfH>H*BPn5fcfu?A;?G*82LjXf{|ZF<>g@d zulx8z=ikbIo!sA>Zs*{u_}4tr7{eA$(_ox7dl&weyTqa%dVrLTwEZc45tYfMtVQkg zOu36X;MY3mCcoZ02fv=e0nO&u?|gvx_2hn%sNk;7Hsp^W8ixP!dNv_{CFD#tl84n~ zr$WdHH4b+>rjhm@&oJx2;h+_@aEb7F+e4egShq2UzFU-AK6&i1K6sW_N0yFKEXiH zO#3RPEkI(%OkzC~@&hE(z7S`(M1J+&f*LD8c&PVU)LjAML%j!4iv>WtdM`$OmSCVz zy$4XUB^W4H?;cce0ir;?XEQ!cb_qGMAa)5cK9NY8#d8Fh|1i$;kAM5>9(#O2>pbJT zjnnCwEL5|mC7qi5#`J&AxvV{+p`Ee^Z3~H`S8=rYQMuY9Rkj&E&tS zrRxElzIURj{R4Q$o%T6AH~E(QzvsEB((}LTwy8+Whg$wPpH1ko=}x(#5xvrptaoPc zf`57!A@8$QJYj#YtkdIx+OAFw<1r-v+PQ9le#F>&#A6flSBPac{YdJU$tJcZ4q03# zg)ZVUDTFSQLhLdrlyR99LYGM)c9|5)xJ(M6%cPKUnY8>r>N3gjK!2r|bM!7we#OtC zYw?S?Lz>IMl%G8zY049S*l%$?w3?kEt!8IPt1qk@;Qf2OJ_VPkU%;Dxn6Nh_Qcq8QuU*L4^AN835V?WB?cskfAbwkfAakAwy*W zLWashgbbC%==W`z4J%saz4~gVl&SIa8_4uaLaSshrz?$bAUZ@OJ3E&py+6$R~;sd&Bjxcn%3+>I1WtYs~b~cMv$2^{fdXB+*uW* zIMtI~!yZfxk%Juc1W|&77RlDIF8r+g_o_2aq!v)&v=;uSPLuVh|M#k6RD%ZDTm!a? zO{6oO#1bf!o!Q9v@5~-^5gZE_IZ8c^7|;IB>^>I>%xY)=iXg&~Ka1a!m?|UuD=e+^ z+g|mcX*KF~g~=+u(P>0yrMZmYtTeY#2x-z<-etTe^WrwHc~#A2n4*=_M>s{F%&GPN(^(ar2D)GH8irvjGMRn09M!)+VGpBZ8PlW}0$qOW}D7q6u) z`pV4jJ~Nx8BDvkKwyy18cft5e{;G6e{4)K3@h@*OkK0)O^6%&5V4ukKT!e!?crKg8 zC-1ALKB4%7_#_rSioc6?ej>vkd;*>aF?j}mGZ=8RwjqCX-Y2}a*xm<}xckeKB<`Nc zAI+3Memj&uYnK0ui)~g z@lU?@^|a0$`C|)TPZzzk@-mqKxDN@S1quo)~E^y~o+{13g4;Z`NY{CzdpNJFn4~2XxOiEUNpU=!m_v z(cAIcyzhn3%arl0g(OmVKQ{EY3tuNocR2`LkKUn}!>cqM9x)0VDrI&PiJKu-G z1h*c7F9zX@!MF>)IM5%B8?Rn*KAqTaFYWYpoLt@1Xt{EsMn}<5@9_?AX|?zMtD%QW z91ka5562fHL61!S#OvvG@6xNiy~jJRh3vY-5*zgXmMcPWRC)fbeciN`Y5HpJ0ocQK zbWuqUSU=Q$9oFvA%b{yqple&=PTj^b0jNH&_p2n#Tf6=?cb!-`2;UXGDi%INCuJA@ z0us2F&IzXLyHm^)i>St> z4uC3_oXap}5jD9KAmLzkmQySmq%4=n-EC&&e&I4qS+tw7pm#&rS#YYUr%OF4S=CiQ zb}sc;2b0)mFNHB|DoX>%xz`Iz@oMKgaWEgTty`EFvM2TcQ0jPPA~lHJ#+imp9+d8R zF!kr|#2$B}91{&F_2+6FG>Fun%f7?X>5gvheyZ7>7;?*wQ?FCa4(@EK*-0&NA7{&K zUd=tF*7wv~u5VPW@0Hf|eL=48F6p#$My4@)$IE7T`@4wY zz1?JZ7p1#8^D(;fGsNz`e_n>=RZB$8x*LJ!4S9=RD&3#A`6kFdJwN%k5?+enM~ah< zk6;J`jiQ-S$cO@>L^8}U??stm{!22$yeW21i{IBu#u$5+%&2R7i4WsvJ`CJB&aTe) z!!TT|p;iK596OZsVDEy-rN}ID>j$taiw~lyb+XDu;pcvkW0UIW$XPPg)F^~36(G^2+3i6!l&3>o}eMh#;tc!~**O_3u78DAX8qrW9yGLnX? zkYa2Wt8myYVigW7CRX9_+r%oQ64#1V*!{Rzg*08eScN0&5UUDKf0Wm%0Czx$zYNz3 zE@w@{%UKKJ<*bGAa@MNwa@HbvIcv3eIcrh8oHe*q3+P&S!j^PB5q}p{q+bTrpp42z zti_d*4|OwKD|iv^hl>z)?c^J?wc!SBS(!CN-7t&_PG*_wm;_&8-q!!Gb%Nxe=M0^o;}rA#Fs7@M7}gE z+5%!d`;aAF$t-EVAaM&lXR|ryWJ&wxW=Z?!W=YfelPqbslE27v$MFX&7C~M#{jKmM zJMvw|`hS)@V7?Fkd*dJAXCj>`n z|F_`&hk5L{3HJXpf8_p`Hi~lutdKT~bM7)}#i(TOXHD7rIaBt2Vcmm#B0j=A`4U85 z3vr2z@A;;2nejy2RAwCbuIlpJISuawo^e3h>1dE^{uu|PgAheJAnklWI!;$( zpMv?Woaadg&W)TG&4Zj5&5N8D&4-*9&5xWHEr6UCEgv~A8sH=i=84ZbN*yTwEYVlM zIr=^zCEtfT2B6brInOJcq{2}zagvwp#Arx7-(0%h+YcY+ZTk6zboQ~D#jole>bMH| zBd(-F8$OwvLn?NO`@FXJe4U;LG0ZE|ptAfzEDtoThd)UMoi6pwW$+Ck^!OQa9cOT? zmHg@KCs_W3e`m;_3?hH0K4HqA&LC?JQ|}rXLp|A|A-EHZ(n~75{ebY~ZSKcA9gtRU zot`FWGiZyq_P!J^*AM89XOqRxCL=zew^3a^xy%B|6!AY*5}x1$$z zu^ZPktxoKOnrxz&x7}C)X+LE3JDOsn0qWA}?KrfWs-k~#IScP#c_98JdEhx%F5_r1%LC7SVwODcDbkN9 zI75PQJ`Ru|xr#{6h2O7gG+}%Ln=7 zhxz1J;SsI#u)rHz$@vMkm|Thkr%yjH!TuB%Q#(K~MbS=}+QKq3APJcuMuLm%5{eJ{D;!zu-Js84+@6OxwdmxV=TISjp z%C~O}z2DT|fPECt+1QvrmqJHks+jaSG&nbUrL)zE7LZ^4sRvMrC4UXD1nrUVU{q4j zgPak-a$LrZe|EJ^$2bQl^+dsDr3dX@5No6o6#KxiTT1!4lyKzqR}l_i3a^e(OoaDB zi+Vj=6jwVam_e0(gymf>%(xRWu5g+eBq{s-3#ODXOajp=?D+(zdb3(E_H(MwDi%9p z{3E1NpWx^S_FJzNO3(Hzu)|V|f5xSPUPq=_@vLGFvzRketYlWPiJzLic1zi2l`MiI zz|MaEr{)lpxBZ|j!b6YH`iN|A`!gGgLlH?_!7^?tdo!J0OEl7&!X6_fZ>&t=UDKEq0%tk2+PH!g>*C+d+DK-MnO7H5PjVdA za)-@wh=2yDTrrn(TFZUYEQgGOd@9FQriD}tC8b7J%43r$80G`T≺l@hQ$hY|2L) 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�|E~|85_b0vBv-|6wKU??RMz&b$JOiX3%b~?< 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�N(cEL2P^Rq;Gq(I%(tk^}-B=;p1p^4|8hgPZm6*)7!{EYh<`I zGSJirpz*=ocS=+9X@TwI$pgSeHMt^vj;{Trm?4`PW?xBU+xE!%fg^xuB zg0f^K2+)4zO%H6!^>4> zc)7|9FIV=;j~riNxw`r~UEgs6buw4GprLiRJCy({f!)>g?Xo~9~Xduh5| zW$%v@NsE8ZcIa3x?FG}tlJVwbEE$fkthJkQqRBy(W1#2wLBB+KaU+RVxb~tAD#7j* zM#x-ynYD3pe1Cf;50i{RSt`h|2B$Aozl+W1=k_GveqrwaQU3lIuDiG~V(p)YSbLU# z5azxsf={*m2(#oZtNZg9AjD8T{zJ>~xxe=oR3Fv4iw zZaWR$eu_o}QztAhqg{D2@s+H@jzu8?TC(FL?)G_43;dOEZB34l+6_QXZP^AT$+&(yV*7f#e!yOX&7;04Eie_-2>6?BBDKxxZeX#5bmAV>3en>;e-E%7;a|mV6Wo z7aP!-i)q7^LR&9alF?|^J%+-cx1Ba)1-ORH)NXl8nti%Q`HJkxtOvE|YhhyZI@RQA zewdHMUf+E@D7NzZhx`A*woL4gn0E5AyMgDoi`}~=pldQ1(Xqs?fU?Zb8#CyT8)Uq% zk;i+3N*@WqPM76-mdtl&uw`pi*dO7>dob zF~Zz1?EC$Wzl#87Q7gRR`|-9@M16A|WeGq#FGEZRqKS&FH233!ceo$y-aMmi|4%jK zZb41^haaS8hF!wZo#PmS!s0#Q;Za?xny_lfcs=1EQvXjf{Qcn{VoqOWNAD@qQ(<-zS2GRyC`|3AmX&B76xq+mGx6UK8FkN>71S>ux0W?!@2@g zzo}aVjfSEE1)dM(^{e$b_t6ck%jGf95w1knczei(P(JHI=R-X=$Tnpw{JA;&`C{=F zc%F-J)&t)h=nOs6HZ=JrOVK+_<{P#^86Kb zp!lc5B8#6KhWi5)#~EkUu>NP9h%cvKsZ=*YAgPYHS>XuiMD;F17SF|PcO~J#aZ0JU zR}hCGY07BTUv4?&+?lh|E3EqD zVTdh!7?K=13~9o^`8Y%vprJc#fmYqvN`o_NcketNjEHF#iXXq_t;;#UXL4wdkyc#d z^t6^~6EBJFEKvlMf!J?|j9o|zWU}riQ3;;HQKC)z*?PX;v?x0VxJl&Th3qB~aSOB2 zh_-f(e)E1eEJfsT&1FA19Asar#i58Ld8x`McnkMTddyHUyqprXqC40meJsD6b!QJp z($?jG+yT)eXAm2tj3dtN3Ch@pnXI3C?Yr!%EInl7NenScU${UT*z^glcbw9!<$^UaFEL&Hh&^GOi zh(azZw1H3RuTx4&%Z&)9;*__6Pr|I&DSaxld0m}<7L3>c+n!MajM$LOH%Y{gmWig` z`vqG-TB24LgIsfLQK?3mnjDoH7b7Z_!S%vD<7_cL#JiezimPM54nPy}iXZI_-YJ4f zh0ilWSV;3GCd;Vsz+wpYa|Zo#gGYp&Ft{Cq=84>+N}*0$)OkGE!3FT2dL$IiM~Bbu zerd)SX>n4$_0e#gvw`7$aXyoM`C#^gf@(r0ghz#;lWszIueo=i2_f!3UkFC(KW~b< z|7=cqWDtMlsdD`*|Nc0xL)~%#9QGYvS5(La^yEpr*55BPA3k|}u=P~0?~Vklr+Rfu zusDZmt1@AAy(DQ84ex^rfTzkhIg6Ggqh(=j0$7C56Sx*IMcGy+O})riyW%DpRmxZ1uYV)~UNd zUac9GVvCaFDmPAt1Cn0h24f10ymx!9Ggh3#rII^5aL>gHp$Fb&(4Gu+jQE}$>bLs6 zW5___PVJpIA6V!zRh)$zq+;LC=4t<0W@E!Y&!#*Wq$EnmIO>gVB@ZI}OvRb1cEZ8^!OQ<2lV0aYMG77+m>P2L5k$e@o{&kzG6UG6L)*JKC) z!99j>%V7)ThGI)5>+uz}|5+LZy%HDvvlmPkNTXdk!vGJ!DSBOb+5<~tK$|%pOi+s= zOi)FR2>g0>-O_BEr`O_HU^mdj9<$cP8^yIPI}s;(JCE7_onq03ZqUSLDBEKf1hwd4 zf9VbChK3?43q)mYIVC%pd-V*{7V%4_8v=>;4lw`}z$Ld}|LTQamiOp~RqrnvPl6v!YWDvyq_0Yixd$t~ zOcw+$kYUK950mDbwA3~M8grX#-hXplpN{DlpjhgE>2+D_%uvM26fLhG6LXF~zLZbb zE&D{|WhN!I{0hzWLpjRmu7L8W#IxN*0jX-j0z%RP-605#^RMCVi5bPxaDwnKe(8{%s%-!x?ID`e?Xj-q_DUK8N&luO!YT#n#f>%^D*euz}=38X6pE! znvadNdeCyyrjK_-atz^Jw>TH$gL{0eK-#(RILkfvViErtjw@5*wECBA7y>;Tv}@KM z7g7j2)Yr!Rg{2T%g!Gmi4eycx?bq~{hCI4Z|WY4!y+J;LHo3-}0D7i&# z4Lr5QsJl~3+gHo#3Th3~0$V@Cx+BhcthJM!AM85sQGvf&*KE7}iP$F$?KoPZ! zZKKvw2&GfdqIzTHfvpFQ@pfAa&|gvQ;?+Q^n!R$B4?J0Ikkwn2?Mc*$CtuJL9vT^V zW=-g2h=)9r#hA5jOkkM({fK;aVE*X zJPY9U>MA4Z6th=DC7J6|Eto7%o6#}bT)B=4JYe#CAFN2Er8TX`@|)v4m2Yq*Lp*&$ zt&83!A`IH=zI2>r^R%$b-t z&bCW0q3Y~(=@e9*ok6`)4^qy(+;S>Ev*X5TY5Xi5L+%yzVWm@rgJ^my6>s~++g!X& zv(Qr+KiQ;us802SCwqTcKr@u<$u z)gSVd8!ftinNyiE7Yc(M17Zh~MvDM~%wVnkCos|{g@^*PwQ(m52s%C6 zf)V$7jKcA)6IX6B!pyI|dYuuQ&pH%i`GF+|r@8VU-Ai#!@ZOiA@<(xC<6B}+XI}t2 zw5eYTARlOqy)p}Ltdz1PgnUOu!uAmIeJK)l3)I;X zLcZ-GR!CiTNSqYY@msPfPeInVy#ED7RcV znX43-SAgc&e}h0i_$b!i|3nLhrkW>NnVwqgWYboit2tq1ze5}QRBy1`ZK0WJ;jJ0? z$FD3{!UH_90!0^EfHSmDE1~VljL=brQ+X7=UzHt{DiG)R#Cr*G>=yR@4SfW`@|I_F zxQY(XH*jwuFtmYVV~Ad!?VPR@rc(a|9<>{#1UlbbxuN_0Vd&k5=$*B$W0*KUXs`c} z`*)*7UBvOZ-{701(bOe?Qye>}3Cs75x9P4Y_@lVJgD4@7t-cv|IN1GP+=QwFpp0I# z_TgZZ)j#0hiZ*^Qnd=vddliFHd)~}C5|bR;+H37@N-^qXr2+`zuuQQkftpW*0>~|% zZAYh$Z9@bg_k%FvTbNv+txg?vp7s{eS;K>UQ-0 zf)zQkwbtvAfB7K!KY2>#->>I?jPiS~HQztc7P8>6HgXgj+!Bb%I=rqo$R;o;*4nR_ z+3_XhB9`$G;`|PnTZ%7SsV#~6~qqf67_p_ zTgYdgLKwRz@XX54%P+J1^6)!F_DI?69*!uF%VeMSNJwhsMs_8lF5*Z@Msb}v1G9$< zI0iJI@&)(EDf#sLp~4Y&J;=uxfi>)fY@b=iC>Lq*+B*q)m~xp-#GN`egjkaH^x2G< z-JcABiU*b$7HHmtAj2MZ>-Y2gIkm!6DbLi}u?2G~JA6%WU95e27z@ibZO?&IEG$#? zg(aSskE!~?g40QLDi)ThVqux8FDx+2SEpiOnW`@=Q}u<##Fi?=$oq7uW^HI;IVpJi zy3oR65aL9$SQf-$pCZ-)vD&AK7qQ&4W9F$?VM^_#)Ej=F{-^N+T(_4>rcaM^>G3)7 zSVoU8a9Inj6{|&?Yhac`!&A1QM`A$e3ZCNWVLc~7nDO%y))UNuR^O!s7eDb2dTdzn zD*att2j<&Q|0bS&`|#ZO77*zPc(V1bfGZZ3Z|3RpO)AcRy*zt?Co1(V!$X;YC#pj) z7Y3fL4ZYB=KwZJ2*9sQVRsd0Z(z^m`9j|;%{H=~x&?e4N$LnVki*TA0jeJeqVn{%8 zaTdsSG4=_{Fy1NDb;5d;b_vOHm(Vm1qE0IDkLe2v3Ev>O(^~5EH3B8I-+b1sFSbU- zX)X-z@zw)s-Ans|!La;8FF@A(C}h31M@+TmoLg z;p=yRjY;guF-gqHF?YYp%a}lZM*H;=?HT{effULP!tp5NFSRcK(r*R&Zx3UBsl8HQ zJmWrYPj5+Mq^}mz6aoYQx0XnZ&!uWsaj*w*by^9DMd139NL*hQiR;TlxDNI~Jt|NS zGLo{iJ5>l(Yxr3RRcl3!FNnouf-2&-vAf5cyHlmYqp?vN9M%TqC)JdlKz``TUt6}F&a#-*$x~V*2uYCf zCI}*^6{D_O>)+Gb)h(e4P9!9R@Dthg>D)ykD%F!8SP?cvL1(}BJLlf{g-7?X%kzY} z_uO;OJ@?%2ob&y?-{%Y#fK4*>+P>=pw(7tL5CiJ;jm^Z0*27Fp6T(izl7^72APQ)t zh$zH0eYA`>cz9I8_;Sba)wXdI-vD0N!>hFs=0RwegD{zEbID#=5|(RDu^ilXc{bf1 z<78F%>IROPWIXJdDbMocGEpke^5t2XJafvLK6&Pn*DBouy?-u;m#?nv)$2c`>nZ|0= zrtOcIk?VPH-o^lap6J-gu84(p`74+DAMt|OA0J!b=_eME7x zmMOKW2W(F5<$ASio?pu>Qfk-e^Q?C5+ha40hK%-6H%&UnnkEnOl{{m!#m7(De7sOqK>7HH)V$@4;=NL-6i z;c>E`l!JpovM-kpH5Jcw*r(Uql)7}lanv{(VWc(dK)}z;cEaH~r^!n@I0-4sGm;WT z`!iUpw{g^zTmtX_cu2wC3nf?1JNZ)6$-OjL%8-<);q)1<<_jUI^5gF#dNs9^nHElxEnRe#Ep)`4HM2gjN{-TAVzN5Tzp;|I|cd!mYel};sU`7^9k{tO#` zod(bMT3-y$cjxwtdN26WUv4%2?!tIJe9zMUIQy;l*U9+R`duI>TkwYa>O4+c(5(ic z_8S2k#}7qCsQ~{D-D?gJaOdCoP+pB2kJgtjWDmCTSDzn>Hg6ctkoMcoacQaknWk7E_E9^~ek3Qaxv>&fh z#njV;v2-9>)YUnnjMkOr`$6_0PH)_KKc)z~8QqT_g~~hy>z&Qp(G`i@3wqq470%FH zS7=OVu0u+}i#7h$yE|y^W{7Q{Xf~>&uW+$1bU~(!MWv5Pi+`By2nHI%tH)Q0e;aMW z^=7+5*`e8vfpTvf{oI=o%1)Jg9~?t+?`)S8d$U(vs#ol>>C`QE8yz`96>Ti=X4ie# zcp^BmQp_10gXihpp%n$4bcAH(d^t)q zKs3qdwevH@`{y)U^IQ!l8KPsD9yifDh}Dk+KNti_r#gIIH{~SJ%)SZuaR}{ z2fc-U3p#7wjt+_~bSK?6O(#Kg3W8|NRv|17ivBIs-&-B}T#lbHRU2no;d-^mhJ|EOSR|eug7GH9t5k2~HC)eyZ7I4rk~kmNR&DI$bHIp(}xd zP5}ao)1MoXFH*>Y%xsnZnSX}MaEDY6+r_O=$uMN3A7A|%{a#!194JAin<uBN`%rNx^VQ|^#PAr7IpXPX6l1`U z_6CR}2EK|Zs5={93-n4n2mGpje9c%}m3m|G{Sh+tcuwPlDCa}uJ8k0BW{o}SfuLei zI<}dKK9~^ayC%`&M#V?C==wj^T>qa=v;N<6E^ht*-No1c^p{(WTQ9!;`{UymdY?{M z|Ecqhc|b2j)1uBbjnn<)G|wEthRp>Ero2!EpRY(z z`uRgId0wVw#qZA0_|}7PeJTm%OC_MZX`nnk9xYJWDX(R)$iz|JXRe8ZQY{c$rqVs* z7)8~sYP>lkE)M(xaW`skwyH|S?_Jk181?R{y=U5sD6Mt-|7cCC8=vB7HQ=cqEYQ#U z!*rqf6-?wESV+0f>0gPLpF}NW0q{`=pDR%sn{S?Fx>?#G%Q665K%>8flU|hQbD^pP zTJrAcb7eET%sH-QHix=LXo~L@A+MF{TIs!G4)+nHhm(T?F2@=m7GxI!Rj08Ssd8z; z-KLy){Q3j%STC+LkS%4syihWa>g{OS@h=*Sb)2=UOlj?AM%t7}P|8PzbX3Mi*K(@i>gcqsw#tTZj4pU>ifDQhJ#jC{+tc}n5pvF zZ)P+RmB+x(YXPZWwJJR*E^j~^7^SR7DjwKZijVt^#6P?LKk(1lF{oD_u{UzSM#iJP znmd!wUb2n_=l$#8_x?vj?KWtSU7ag`G z7seNUg?PD1nI<0EorIYy`>OHKZVu^XfuRx6tLa~9SEF^#aj+_$SFLgbOQ#`;@gpT!PI(oIW8p1=!{#GQh<+%t z%%ho>vSsX}3bh+CyOZfO&u6-vstq?!ZMU2@NSwHqq!$b0$7-f4*dDi;-6=RU$3ZdY zcz+x&;DEU5jF!$R?|T^*E+D`H^^=RO)>Tj({Yjk398vV5rHL<^e}NZu^NZ#p9*X`D z@+m->wb0{{ipBzE9Ms#b2r1H|b~2DE;5b5THPp*^0P;hn4t5hR^Av_kop?oX;o{9H zjD?Vl5&j9^`-!hzcslKCSJ@Nb5XB;LwN0x2ip9<=;k3$!w_HV3RxF9;gO6h^EufZO zi^E9x3+|ex(bt=(z%46~_W>T_n;%Apah(>o@4YfQkA}Q;&2}G$~ zdZ8MLfdYtR^Vzibg{qtAh@U3entVd@6j7iyw$Blwvp4^aE08y0Sh*1=O1|3?hH`O^y8BV@Q>f&78BTCJww6 zOpSL|h7=jq2vD^WT|H#MH?I3AE%{e}?zS;%s&Ob3gx$ZNo?b~qxSVen{{Y>JK+tox z$joxo1DXU=C$(Z})Ezq{8ak)pq4dT-l~?rh`cLimj(|nfHZZF3u>NMwh9B5@i_Vkz zZE2F1i)VkDZDWDOv!hNL-=veUCxDGu!UO_#77@ZMGlXRrCw3%g?>%6^8q%gvjI_Gw zQfw)Bdo&VP>OF1!xx_&MCoCdxEd#yJ)jX8P;+vXYJ z>PQ#AXk_2ftUT;j;okR*E_y;+WM6N0?|Z#wG$jd_WbgtVb+a|5n6FM3@4P2{Rg?X+_?libl;1K z_-LuWczpcD^cSq$Ty$RaY)rx96;khZ+&&QP%$}Kzc@%2F#D}utnWrwQ%V623s#wEXD70fru590(ZD| zFhlB)KkXb+QS{OJU?(b{iB-NFl`Zx2fler}VWz}>duLaQ!nr71zXRQMBLm&XERHgJ8uMI|1@L!7GQy4L~Y$lVNNx{{%PKx2` z7|l_@K=8zPpw86^eBA=wS$JcleLVx&`U$Ud45ftGfGT-821Z>z*4% zUC@HQ4qp+_uwCcnt@K4ZnGS5W>u5OrVcF6P>09cdmLz_n;We-be(dcc`z$Ta5lkTt zLFSEw8a$I}q-Y{p>^O4Dg9UQyLY#O-vsLmwz1g7MgV;uRKXjfy8wG=n?Tx*U^joko zG@%GRK8?jfEWJk@v=bCD!iy2(goaU#yQPZ5}f5F)XHO)mX*Bn+g zyKt=qM1lM+-E@sf0isZgNJ|UtLy8t;LKlB;Kg02q{Y_EYMi{!Kt!xAPIYbZV{GNwN zhCj&ooQZZE`&W)`jLUvPV(*VJ;W3@|lpt4v9ZdxxN9f9Vo#4 zFW{U%ZJ+@6{gUrHlXBl$zHivN?=HR%A0gnteLtjgHskgk?xzJfjC80LvpPUi&Kg* zZk#_ju^7W93R}>7{ZMEfMoUbxPBB>GYbiHj%*44TsFnk@FjQjj2_BV0G8d>g`ULu_ zAI#aZTeQ8^AH8XJu%_V%&836E87A}WcZ`(O) z(3~#46dCWS&=o3^o}(m5JE=&bPJB3Zz_evLlxFP$L&{*zWdL%%szsK)>QGl(+1j1K zc+0%x{j>aOcwy+{E9n3Ez~ySXp^x^_S#>BoG}i$>Jbxq@=}?tZ{N{Mt`TidtyQL2= z?Kc`%77fRfz9sTM`@{<$n=6CE5ChpVQjqB$D-Qj)+h+RK>D>y>=2prBw+O?Dl}?rz z`RkBt6^Vk7esHR!AGs8>sF0n$pHu1S`ULDoOyM$cF>?VHuslA#d&>GhY_lSoC#SfZ zIg-UHzEB{^pvMf29RGyY{1WKXsU!1sgVs%oF8IpwyjMrPcZ zwHniZpBE3jWqM@*8zCr213gvHATmAWfsjZX09lblK=gy?9|%JwMt^s#?d{Q}^@SQ+ zR^VR@RRwGxc>xUe^(=3O%X|B!DWyv}IC=5}l<(h#bE}o($ElUs z-Yh}Ct8b>c{b<&JtoK?%y*felCVI2(qu?q{j zU2YaSec?y#laIR4gBZ1)xUD6I z9Z2<8dto~i1`#D%Y}*cf!m$iXpU}NMNj>q0?Mdp1=eb}oR`BTdn3^HxZ-gS-M1yQffNn#QZQ*s{;iP zZo$KIKA~n3A*6^?fb%PG&Rq>S@z9)RFsX`?ei1N)maLxY5bwgDLI& zw#j6I6We*DDM>t0?W}G}i9iRxHhLTV6m!*|ZyIaP_Xxi*-qsrlY6-@+`HBbcq1pi_ zWCNC2+BTW;1&a#*zA3E;^LR=zi!co-#Vo@7b4oFbGF6+t9QS|arZ30+!#VV>kJ54c zfvEp)r>DeAm@fk+IQ4bVk^tXl<|l@gwDmYM$)Ofp47J?H|1*YhcAamgZ7 zKNwnE7g}5sS{$^p7=K$o&smJ)Tmns}g|;F_J?wOuis0I5vpt5bm-1FtEFDW?jU%&7uYfX+hZT+PTlh;&oTUgvep z%2%;|s7zVk_t@*(>|P{zb=QUNsORYv)%j;hTjXbJxxioB9#%O8nP} z{^-D5Y}9kq+|NQ=O3AO<;|;AZsjZW;EqiU2n01=l+JLr*mAG#gtFwZ4D*lmMe0wwq zD%Pe*YLjc8G`SpmZI0Ynlxpk(91816)#GX}H93(CWUF`7j%4=YlL2Bc*0g)rfk65P z(8Zs*qn<|h@?1?Aee6*n-a5s0ro{j{->4c{2R~2{2L!! ze^Hx2P5R5NJC5tA@eU;hjMVo4f4qnxn;Zzm z1~;Hud`w0|dAH&N=4|+p{Jf?)S#OeH9J}6QWSDE~-xxC-#w`x>beu86Y24yer#X57 znrL9;jsCyvU4L{_Ri01arA=u|9vC4w9qf2AW7T>nHaf+k(?BigR%mHxpe-pQUC2~- z*0Yl;jFq;gEs1GymZ=WAcmO@K4zsH)?)sx3Vt-&u1rePYCU0p`IgDP~>Y@Y4$dBy( ze($|6FUd>CJ)Zqyxkj$|5{@1tk0lE&Clc+2?dAgQX3e8aq2M0+pnv(_10 zFsJhw95(k4#e~PtiR5um)pQ6cCf{$OuEpz3XF8Tj?+QF3bM73E@aGud3y&P0I}U3+ zm#;+RhpFpi(6fWKM1|RDISBl=PmUAd=$EzY_^rwCjSkEii!fzu6yKUo>aN?=fVUY3$j zA%Ze9TJ9DEno)3LykSkHMeC0xr$zIDZ~zzrr$r7*i{|_O25AxalWOs66Ufn1f18XP z(V^Ozw3mlODT@=OT1)F94OO~`RO$9_PnEtj{y$av_Ec%dFo?7n%e^2%35o>nER-xc z;FZ^vGm4aQAci6}JR+#klP|tAVNRBPfV<}^|U znIg*ly^b@x9*9z5#AkD+s=Fw5ZjL*j5_di|dhRsGDCC}WrpBG8#hp)yJ5P^0pBj6P z{bg$U%Zw>!`mN9sbErgG8>-B3gFOen-X}|@0+Mxodih$g+)y%i9eHXC-99(kv;7C@ zb)luGf8j!;vxMmjenStJWWiS44u_RFP+=B?RhAS$g#)y1Xo(9dEPO#112Vc_Pw(_d zV89)H=|ZGFy@E`qokM69>&mrT?dsm$YFBr|VTl7Otm&tGX21n*9^Efp(Ask?ua!us zgJE{t`x>ViEUzsw*Bha+`Hg51Es=5mC8H*S+(<7fCDwWbQVYF@Y6vnNFWO=?5LX_H z(?BqMS~?uCgf_JZODH`Hlt|>K{6cSl_75tDRV}CwK>CRK0FO-WO}J(yw`;D|Dju8K zWyo0&>yPMk>A3lsLlV0A_2>7*8QAiB<~bdf9qMJgpWexu$p3)1VCm|6vJdLO_u}t~ zfR1nzU{29ZO!tPFa`Ohs;JYpal-4OLHpOXGR!EA|rmQgUG4`1a&vp9?WWdz4wC*#2 zukut8>NEBrhWgyrLu)l}NKsqKfmU*{l|0c(C=;96U^n<4^`6TVP3+P%5xN$u{G3*C zd4I1~X^ZK_r1p{py<}rA_Jn8kP4_;lwy;psLg&4P!SyrcOgiwN(f?%|qWd%M51JBm zx^$dD%U$srIpuTEwd9`&dR$7SO{uiHl}fpDqeH38Qz~7QDVT;E@8Jlz_(+@T3JiQv#m!fF~p1nHun94wR%C2TIIFPqii< zbYTK<7xG^I&|pdpeKVaUMZqPG!IJqX^pq5UawA|4E^%RSsB9!9l$+>^lh8i7Ia&Li zOB|{&c}_X#r&?H3^Sd(!Z?rqHP;$Fa?*o#->96h4$UMUH**@!^R9Cjh#y!WEq_RN{3Pd zOH(bcRhk2CGbL1MfkkQb;*`MBDfD7`z@1JnW&{>x(2JRYrJ13@IDO}tv8y`%gVhu9 z@A?(MynpG{VcsV-$!z~&{e;ZBLch5Cc;kc)gWqEPLFg4-gJrk~tw!?26`e&G|9y3z zO_UcZ%jhPh|Gu$^_eGhRc^)|^8$40}pO}2*Pwu#&$yeS8XKKFkpTb#t7tc(-dIx$s zZsk>9-jNvIu9MGv3UE%f{mZ~z)%HJ!GqwGXUI1sttvuKH4ZqQ8UYn2n9(BX2O-5e$ z>G{}XU@jBX#DeY*C%|2!s~1E20`?FqEQ9Q$TsA~4VohK?!( zA^*v$yT&8lhyE}Z+H&^Axsb57*3q_@%9XX))z@<><+f+~aNFB_swfW+(e&UoqzOk* zn04>sd)Q7E;%k?jj|6dzsHO5Ewe$yA+QCZ8)~h!L@ny1WqPmw*ah)fw58_%Po4l}e z)cn%T2e#sZY4m0-WylqjE?k%8WucU1YzFw?mFMXS2C!Q29C3jMCZOGb zUVK>@-5Qafybk7E5g(~M1~08;xS!X z*9jJ_AcDSd?}iRxTc(U7g!nIxBaru>-_o7(uiv$*pt3v%X)LPB0@1*K=r^!8p#hc? zW)G!{se&= zmyrm%=%Pda2VX=Y-eE=t+7CqMDw>03cBq0ysFd1s%JqVa{IL556rp3@L#&~@u%T7e zzMI6ATMp>OyFu2sk4sZHJVG=EC9@(f24(+h9)l9)X)!Sc>1SUluMIsu_gY ze(0+DNcDH%F6AV~RFZ=0M;kIY&<-6L#ivqem;xnqZG;WBP<~8@e&tsT^BLms)L|_r)N>U`)Rx8vun1B5J*M z==?A_`9@sl%uDf4Lr8~nxq*>KCep`YX&t$4uav9ZitRb*nh82SD_6mx$_*84Sv+qC zrUx4_oqc<*P{Of<8>o&8mtqtlChJgUqax13)NdX$B+97vAGEQV^^fONEO%pGG%Bc1 zg?$2ooacYmc7lp+7K3H;0@{Cr0{g8*BJe88wwVs`_Ejv)Wa3(sL3V zJg9u;KYfvYOoIs`TFZDH(sUM*QdZ;6KT!dJqa!BB*I6+%8caIm!<%G-0+Z~b9k^JM<`oP%qXu{kAd41(g|`l3#%t_e;4A>vR|Fx64@0zg zjon9fU9!MXK#5*X8*3z+uw8+ML7U~M*K|-F0_qy!JZK*r)3Hbvh>k^86))AM0;OgN zgjCukgFhs<(O&83SJQd=HJb<*ZJn^ghxUQz(g@Rh*Nw0bbj1k!EFT`>aPY-;?+AzV zF1FXu!yb0%8xc_8m;F5%O3}E#H^V<_REo|iz2PHDfA}b6lD)%p7e)WB9LK|KrJw%V zu%H8*hqANE&@fXzEAP+)=;~x2JvhY6&oyid%@|Qa3u;Ef7-h?c(AkA>h;P9t6z65y zzv3MC_htD1+o(9t(Rm9WRv1QN5ku>&8i&>M4lsEQf3G?V?ENsXcP0uHF^Jl#a}B?J zlcU{bQE41XhocHBiv_c1v&vS&q;Eb=lU7|$l6W* z*lPs@=>p7|USrQ2tHNC7xd>yOlL+V4(}#ZH1uU02eKdfv9t6t|!dMSVc&Nfy=QMs? zc*&RMCV$^+<$25t=mRb$wJ#xbbU`y4X%Pq_gKPxAkjc_LvY~zwopSi7Nbfui1Wz8B z{wH1twlq@wwIzzalSj^7L}cvUtO_FGzm1_RHESJ>}sDzNz;L&=9noSy14 z(^?JY;CA4kEa9A1pYaSb-gf*=REgw;Uc-`s?_a7zve1kb z9nIJLJ4!|c{0&Yf3tmLP21j8(uO_l8-_*k#jXE%yff~HK3U`D4aXUhZDfypa{A@-d;m1N% zRW%a!zb*N%NR|$AH4>pr$=cX9p@d2~Af16W!Curss`@91ai z;8NziEWLG5>sbX$0rp~)t=S_V(N^YSD-Cm!_NgE1?p;hJU^Trsr=y5NAKBh#8<7VJ2eBZuQ2*mcfY0EN{F|B@CNP(q1;g zWf(UZTZa)}I9SWm{dx->7<|&%&@MW%dE02@&am(2@iWwV!gq(5rCEHIw8V~Cr(y{p zyQY6vw$5GJYf<@6^{*Jy_*WnWY_w-X+Qe$`|3avx{1fvq;mYIR=NL-;Kuu5V=JvFq0rm$ zuuBiVVMRC?DM+8ZEh17_aoDBh^^%)>*od%8ks#uB3GgM6x@zMJp!ffGcuOzuWqb#c zW3gdGkwR2ECw&vw%uMQE+quLl9Bav>g#eT&gR?EV&|V)}eA(T({<1gkyiG2z&!&v- zO{>98?quE8f@7^5^hiOES#_-4Sau$=Vk{brcCwTj^_{Y^GW0pJpVO6pG?-``7l41NF|y>-$tz6EOAJ%f%uV5Jibsnv?ctRQ(-wJPIKQIFwMVv zolSG+=2G2;U+}BDO-cR3lD8Mz6p^pQ=C8dP`3C-Bg-hb` zFLx!rKQ&+O{%h&xI0sz_#2RM0etgtIiv-K8{nU2Bwy)`M3o=NV(oYyygz0< z%wBRhVMtng$svi^OAPnx;(6Red*>rR%7k$+i~O!;LaWIu`o-N1TPL=foY_`=S*u^{ z4#(;fn}euK9Bj!=W78D3{z6#5@y}ZI^l?zm^l@@W5`7%z_nkBoxG>aa!6;j*5`sNq z>VNqIrWzq>t7*j`-kBHxbLKl%gZ5IWC6Ctg?s_69m(@qe{WcwZry$m0O~J@OGLa=v z`X|QMKxXpmlYW2OBUd8%J%cG(Ho6nr{EC^ydoVi>DStkRkEZtD9%F2uY<-PQxV{`| zY<=y#PuCuIZv;Qvl)CGfJ#ItQge$8`zqtFofBq)+xbB@TG3yCXoIt5qBvmSg0}PDF z>{fc;g2R*VpaB5|Bha&!+N~od@IDL=h2D?C4Sm`B$&8jy0BflCjD>#m zw2^d2C*Tc%wFm&fnbTl+C;@25DJe$qA?!`sD_EOs9OCH9-p^;Yd;)!rxa{UzuJ(R9 zqj%KO@+ovpp9T7z|1|VVr577Bc7%tpWq>j=gwtThhv6kob8UFB*@Hc?(4XG$JHr|F z@R;f@S&SOk4AqgcKYRq8GEXaqc^$mYohsQe%T1ZV)oGAqSqNw}Jb_fypj_HqILQ3_`&{KwtRTKJE%%@83&qX9zq2x1Mi;ng*V9{8Ko4M&cmz3Y@i@ImrBBteL@UzS)G? z&ShS8YBPcnStkoHqLHSg`OXErN!2DTyh-UMExgIyo3!KGi8cB7$OBmO9It7`niqM^ z6s&of*Sy#cH4Xe3N8zkp;=>t+v(08coTFGXgV*fAnli6BX%l!0SUGn8>2^JD0T>?@ zi`p)@Lx1tr#Hc#zb_V6fdkRfn`nTyWN+wP{LFjheH=Q27P7iStvY;SxY;00!psmL@ z8}vh3ffnlx>3Hy0YUPCI`@(+!^rwc6F1L668_Z*S z;CUU7Z9vWuU~+W1mX*6J$#)$o27!e@si|tt+%ucG>L+v=?@!|&cW{0AYB!p5OYKFW ziwFn6P#QsRv#*iusEZ(-%Q_YaY(Q-fcYLP5MfeNhzu-^}1m?)eK1|9s#9fh#LQsqT z=OpG(@0ppDgaKK5zn55gPt8F5w3bsqW74e*MGbsnT`t z?Sc0qETs2>_wK-9SGa<7vdE(!!2Jbk(9X@t)b_c&b_OZTtN$>WB zaQiU79g%Kc-l6-X-MY6+^!85B{#1C6@J0|Hr{kn-KqiD?tls^!3}Y5~04o}GSj_4I z{inSby2H3^)d?nu;7q~oK(&GxH5=FtRB908GQhMT02_>eTY#ov@3Y5mgQvpvL5^?N z9opp=1zO8gHR#*OKI>{OJHfk;0~JsAKxegyB|JO z>CXQQD8}*_bF+ipLFrKe=C7R3?9#N-Uf3*z&UQB1i~jsCZ{cGI`|l4y(LQ8Q!&Ikf zQe65G@&JML6+bM$Fq-^w6+eOHccYchY|}+d=4P2obqol!Ay3rbyG;+4%A0e& za9t*MBfcdafy=#&-29_2B4Fb*?Bmil9f#6eKasFqEQIaQq41EFE1g1Y&r?xsPsWuv z77Gt93K^9i237o5Sd*2zB3L=%6s)RZx|ZS4AM^w5jMmV{xQ^!l z$OKS?@o(85@XZr-$M|VBAOym;j&bH=|4;8WynDe2Q=XO1|??f%3Gk(ZAafDj;#XjwGnosOOhU7%r zp0#6z-D*KjJi7H8WlRj_77&IZY_&fQ@LC^>kbO=bnT(D7^mfSeW{iW=+Yws5~IN@D@nqv%*4+ChK|o0VC*l|Eay z(la!dH8LdY!g0;a#Cbq04yy}Xj=0G6guT3$YG4C=^<4Z$u4DT|NaReaW|gygn+?Kf zrjt8>8b3agf_%R340;1Wpot)bxBJ!a+K$Arkqh zE~Tu5&8U=k4zvudg!YqQ(Qob({&?3)=$*2f!n zumBQqckYlZGw=m-(Eb>>+wxlPZQRb(PO%DngP>qNp5Kxuc$~+qK>tnDO~O3RlJ5kp2WNb+U_iDJfBiCRevj!<=;+;T0Hl&Rg|)#my4ZS^0kKHBqA1j?_|AVED1BSQ2rHwi>b{C z!?HEgWe!}1mk@?GW>a!wgD1Z2>9@`(I9cQpg|Y9a=uhbH6u($CK#ZqA7r%HL-aAm- z_MczkfJHeSzLBZo#9n&y;ZLy#JkM)f@@A3<^n3bK-9IcJ+bi<4%|1936%TqyqA6NG ziS1xSi6sB!I8PibWeV=C(?56cS2+Hp-e2LZtzgMuZpUV;9bRfQF9j6{GF)zYpVICs5yyhz9VYhK{YptnvUmC zWAVQ5Z`l1*ERBUO-mL0CvDd42p!m?Mcc7TQT<<{fe^gVQ?kTFN&h{|gX_NhgYO0fc zlxnJzeSm7JlbuF2)ye*nYO0eRLp9aO-a<9i$-ekoj9QD6MW@I9&x92WMf-VWhaf#< zESg&AVprtJs{j!zan`61KOir;x37U!t|Y7Z(o9bu!5|L4`>f&!^&%Vd;(O88aRts1 z-5n3e)9FD?4L!h(U=lTgLo796qoeSC#hY}GFJ#=y~<9tA!7V@YbXLEG8e$%vpTvI$LY)Ej( zRkkqtE|{GRxDmqRfD)wgMJN$YS;$jYTt?{-`*4ar#=}tfl6q_}+O)8iZEwHz)`;)h z_}J>M_F}v1$BI1tSv8iAq~0++hHU$nuu{A`wUhpi_N9Qr&U;gWWz z3#-Y~g$NQL#|T~O-+7+}&AvhQ`1$brd_?|UipBsq-1|-bI%7$&8u28C{t*403)vO4 zoqdq++2;ab&ofb~PKqxvlgF2M{?`oX92EP2gFo0s3l3<%{j@SI;l8ty*uTQq88}J$MtC=yVa+S66ZH3$Hs8kV}FUb z?CrXzTOHc^*u~p|?{}N8FhcLP;m6M+O!o|;Gc)&kfJ&)?o<;anPWw@bj)5PcUzte1 zhPY*X>rHx#(@3(FI{Jg6i2gj*q(Yk#2zyi5l9d|1ROH_llPyjbJOGGm&Gye&g#)U2 zN5pO>bX54p_j$*<~>r?1rtuF*#=Dxi@LM&CoHp+gKuQnmj&f z0h?z{<*o*>-hp}Z4qycaw)8SC<^9T`^4bRSeT~N-0}HGl2&~!`{2R+YX$2~?cU;UM zK5r@91Qh2Bz#mKHHz9+%3(1{9m@yrYe|r>TexSur8S)*60+mR}qySofmhDKiY#-^c<&Ddp$%>FWh;Nsz9kWz7Ruk?@Au(PAKjCP zcLuFzH}zakk9V#DTraTk&Em!{lmy z_e~@9Uv?zAe>`g~^I0y-TjeF6?pRRYfV=iHxiP5V^98r6Y+Rg4HXD!NJs!bk)0&TxE&D`BjI5 zOKhu=MnJ{DHjO2|v7rLLgt3v_gGE{oT{!g;4p8?+4d4=uIZ1$l;H%}+3#aPy=_Qd{ zj;+;xq0pOvxky6&*bEcnv#2|nmV*8VX7)?ut?k#?=o9cO1!fq@_;QF%fq79`Fb(1G{D4)P#>%!9(aS8?Z;YFxu219KT~gj~Uc>ySGPzKgYO&=ULH_v{&^%nrCxwv+fVPhtZN*`QB?hmu58=5vd%&-`$7XM0yat|AX0JJ+%Q}5U&nQaGx}qqd0k*wle}mV(8|gLtM>9>Fh&KKpxOYPkRtPds zsgi+GBfPnP0{1W2jd#mx5X2T64zuYp^3E^8}EM!)c1 z@8uchzaHng$$#DC?Ts36Bp{)I{_93y{NWarhM^?9B8>zfNRZy8F-)cY*g?!mGx~9I zFh~cccOW|mC`!%(p#&|p42(fe$KGYw=7w#+CC*dX_G5gntKEptHqs7vTt^TN7*vA5 zhMv43vT&SB7Q{jnLWL;Kr!+TR>7@bO$d)blR4 zri)!}4eo6Hxq65j;tzj;PcJb;KaQ+Cs4&meY)wFoZi zTADqBb5U_Y(`HO%e#y5F9kfeRVP)$mSg_x@Vkk?@PIe6Vl%&QuG_57YBH|u7R%{;*?Orx{ z`Uv(CWb8yPYJ&_nO~+)}np5gH-b&S&f3rY4i(skTR_$_HAt$3K!3J}!86guTgomzR z`lGgp>9;deP0!MRIfBR^v>_)GXqJ{^5XdT14IZ}rGYa$QH}09p#gA!o<^9ja5I-DF zclys%D6YNftRgsokIh|9Zy%z;IamZ=i$$Q?V`OD)5o7&{vq_80=EG2FpxO5bsfg`| z7R~X?M*E?Sc(*7Ib^y+YWY?JfdPXPG*JjK{j^2a*F0j(75tqRL%tV8ypVKXCIQPU* z!Z})|P4bGb&L~w330ebgZt{vzKPaq8-NfYi%|FuekF5QSdSu;EWCE6tW1v+2E)wi6 z1QIE*vYXNZ7eIBK3e_+_x52SN7frzz@lJlbT~XM*F(ej^CsIiv$|iz&I24h~SSIWx z&j4D`cV@I-qJYr={AjyWjUL}(kG{O*8;<)s1nvXUW6D+wA$zD?-CJI`LzGkV+rg+TN$vwlKIYijV51PA+C zKOxjLDTj*ZloWxKrpB)zeQ`hmHa^J<E~V@Jh#J^;%?EZS6^xKML>yigcdf6$W^n2Y9u?11`Aem(x~LjK)$# zV}WLb*H!_qDIMUo75f-3;N`QL=M+WwDdbTIi4KzTS~=NSzwXokmMQ>tf0X3Ph6-_~ zRjO|82E91Eay!CnM+9Cy;@5tTUxzjPIt=g%!Gqw?e-wTpyad-8L+_MLtGz4Qj61xo zH7kSql*$KoQ#qP8N3x4?mCWach)U)&L$|F&wii50oBq9`;IFrxTR8lef#e)6bbxf? zBGd!GUc+h$V+8}$%~(8j18167{%sWVkG`$^XrU+c;*~c4F1hEb zL33{mO$Y3WeCL7~ya`*4ZMhsX-AA-9xUE01D1id zooyYv^KE|KoIEivZ+O^)75*Y)ddA ziWiY(+94*bBXoE|n7WyLCf56vw5-$^oA+T)3T~{>v~RoEzTtY&Y|7pOEYUeq>HTrh zG|V6Vw{J-K(9ah3dpfFWmjZ{cD~e5l-32zN#q$`ZS2TT~hj+a()~3MTMc8t{V*az= z!~kl6!{v0BYd~%52Me;%2!OOhU%r=+Il$xMY^xR7rec&v13uMPI(?3weM0;E0v7>* z?+y*P_Bpdl*<)L;qgoU&g5&J)q=uqS8ytd8l_DMZeP|7RHG^v%Ip6L#nv0B z%H3~I4Gt^8omjoCkqAcY(+(-{dpx<^L8^a`#l`l^&t6ne;hKu*M&Y+Bx;PGqHi9P$ zEzd(YR2%KKP*(#4;)bd(K{X|w^itq)pryi8q5#MH%_&6*C_aEV?l^1AUJP{-#i@|#*I%9s^X!v)ekqb?%UMILO1;Ks31wPO^TvnTmkwNVH ziz1#9wT?D#h}fsa;(rzxyVG107^5^7zv`xPcbW@nd059Co#{sZQ3%aUQky}*%Ef;s zkjIr2*W+!;mkJsLC6{d@?W}jIs;|9=RU(;N(%1!cMYrVdw$RCOXbqm2d-`BJU;ZSx zG11_bmyo9C2?>{sYBDhfO(w>m9LEipU{-pw8}+4au+H%CsUDMPVXC)BVLi|T_o!j& zF75B59xB$mFKc`E6OpGv-!kobM}5o0Pdes@@t>XRblF-9cM&(`v*rQ;cEJB^_@5IM zl52MGRcje4VX`sxgvaWzUjVjdp%Gt_%GYuQU3M4oxN_ydk`U5~-JS!Qf-Yf6wB6a1;)1V%Ei9+GQeRkeVZYAgWk0jtt?AL-HQP)zr*T~Hz z*B*yGubd)+Iq<&e8gcTpdN>muC5{zvi9e+d7?hAyeYAVBX4b@gGl~0&f(yDKQ9lLf zjI9-f{aVro%>`lqLA|i=3?%`*{QJ@~V9q`w*lB{xT3As4$Q=AS6j-eH!xUI-7T}>T zPCi>gQN{8pB8U$O$QQrQ5cxlYO$Sc3Fi3 zO~NYjlMRvZ&{iu3s{({dzIajL;SaPji`AwYHHI!==~#ULM?kp0sVP#rpGwE+OD_)7 zO8Bo`M0xHecmu`!cPN32ped#-q(m_xvV%!ER za+9MiFtvOf)+z!C8sfe=narLMhIoXOJOvGu5teAPyLNJv*>SA*bEcvpulo8cQ_*S} zU&rM)X==39GUXqk&!tO2(l`*)lHx0+nb^cuxJUXT9Z@=(JV)gd88b90`W3b+4;f1D za%7O(zfwB+uOrOnlKmcc`^fYpqxtJ_HL2qd>H*^cd4}%{1%H-tTOgx$%Gdqkw|-a_-o-Err6{!DQPTvcTPT+|YT%z4+~Tn$R+kS4jB01w@8^EjLpk z=2NvC(MS+32)q24H3&>;iSGtzNs>idNqA{@|cX#WE0TeE|hd^ zFaE;cy;Mdc`n-iQI1dosAQoZdONY-|gL6(_M4R4(MVA!#5Ce2|j9Q&6Ugxy$66Fex zs?|%Y)$jjV`xf}7%d7vSO-sN+!iZ3@Kz#iTD^w_ijh0j`P@u1NlHRLaTDNZh*tfp2 zE!C>AfN7xN*L0m@i2ni8SN6_LUSNW=C_*aq0_CFOC9$CFotFo~O{H7}%zMsxo?q?- z*~aHXll*?q zl0Uu9@kw$dj!)vd@v2s?ug8Dg_Q)sS37>CmEP7ZC{PK}8 z5&S|7iRbP_AAptQ1JJ|FcR^w@Vn9dGJK>!v;5^wdM!BC{)`mxPN*fk62Hqmf*cCg& zD@N?Ve(_{j(Kfn`&~_zn7;Dm-Ji=;%%5#y^0XHn~T80-I0xc^BSO&^(Du^8%TXCtW zw~WkcK=c67y9)R&)U#B;|M?Lt-Xa&r>SYgB5bA0*p{}CA9ipyQ2~Q+AaQor%=Gz|Q zsH+dw5|?Mi8sLAGe?D<}^NGu=h~@I`d6>97tHR|~G~5FG1mUh$hj3S`L;PKZg1dVD zKNSAX3MAIEFUpC(vl4$-3&-^Ae2ca58RE#Qx*Dl@WTw3FS9&sbIr|Dar?b1OqAXju z=dI^io|7$xB% zh3b2;GERNS_bB+T|MrMhMR}8nvGpOI+#!vy7h3*y#lX0|%bBbMjIurM} zuY#Ug#S~9uFP9R}R}Bl6kZExaA?G&_S-+Nt6xPoc47Y~~BJpzyq!jMeA$&T>WV{@G zI=*Q?JtpxRd3bqbT$aa{QUr_?r>AaUmZ~lZPJECKh8IVsQXfC7M0y!sUMPEoM@evp z|MlH{0cWr6mS|s-=Z?|N|MZ7v1M{fld6g#5LLt{H{D`cR+rJ;wemc3tj`k%AZe^1K zkeWlWugRSRk#L1qy{-|pI6kuL@=ezSpFT@ZU!tel;M1k_bR|7a;ZKvEe8flQU%u&j z<;emc8EW~a8OJCXUD}jK;x}=d50IwyW?+y1iC@zzI?kz|{8JjEoR=-unH{6a=Sz%)_ z6kxohrKlXqP!gZ?IVjM~Q+SRaNt3JvFJ|hOcGV+R$KFqMDtUxscZ!9=$zT-MMo;Ae zoBcadz>6LabRaDsRRgE@I}z0;HhZebj`z77C?79$=iR?cd4N`8;t`D(=SNXnekt@UHsxI{x&tj1?@}r&dzTGQ8U3{Z96cTW%911vrbL30*{G~v20V-?GPF{e#16c&eX?4Ao^B`_R8%+GzF3t-YP)v*aJ|2?d zSQT1!4(4nS&ljycLz6WH`BEbRhX_B|9@aiBXu$zv?sDxQev&4OYsY4;)YdN^Cm%E)v<)s~8z3=V zXu&FpNqP)#P0Y(U^LtwOIVN-F6qfsi=o`}dpFm+=ENZ@#>g>fEBy6s@BrUJe6tfc& zU6S^?))C3Q+3UzYSll%=l*+4UE^E*ggz$dT|6#n}U1~EGZhzT%(l)S^wLi%F1(64! z#iqalGxC~x*D6faFoxgH@U_JG`H9(_pNA9E{yDeb4QYQ;6};0AVm6~f@&<6(q!iBF zbBAfSHDKyB`xPK$`y=I{`f~6zg(aL7cZuFDObl#4yygn#dNcOZ%Y9;2BzCz{9VgX~ zud3v~ImGj;RlF~DnRoG#a8Xxri|-~FJxtUp5*>(5qF^+$wErO~Mjv5O8MdQA|DdNt zRR5rTNU7an&EFR&a=iId1wUK5j zVfxdg)in~5b=Wk9DZ4{V*{9oBqe;h-uZ8Qx|#yn8^uKB2C>v54TY`B zd{ywNuj(nWAGnHf{X$dYRNX97Cv%@&vsdTLL>uw45){Z#6CGfl> z`s<%q-@#53>-%pDxEb+Dc@^(NzLHyEjY~0p5|#5po)R4}EVn}qQH~Nr9kguKgxI{G zAA4cnDVV}uI+BoMRo5jiJeCD4Mvca*x!t5^hrdQXPn-$|FZ2bXG-jv?XuA}qfAOE~ z3)jE2HAU((Fn$&~u`9m?e(Y8GA%b55ex?eFj)xz+={V_U=HJfsGufJ}az!KKzi!ig z`Cargq2oXN^#AMmzCZ*tn-!;d;BBr03n~T&;2|qi#iAdDO)%~w=~00Kv4m)K<^WPH zhS5~$Te@0J`VCS@8d0BbVV|pqs_qZQ6UDIFM)r#V6pw&(m+}GNE6}3+$j^=kB7};5 z;ty8Pi9jyrEW3$rx$EIDK;NuIx<+!p40qHck9M2zXs;DPr{qs;_r$pK*{2G-?um|_ z$bDDa$6_V0@)q&^c@w&=FThKXir^HkMsafhh)D7#%!37**;8H4lMTB_LBZqYIXtN- zP8(Ro(|9>eU-!dT`7XJf$Jv8Jz!ERc-g4Z5tyNl%(dj!CJBT2+-YuHdhEj$tsjXzZ zNpC@$l|S7V&kkuA2*)U&aG|7!yY6{{Vs8kIPaTQI7Y+Eg;Lt>GK``U2t#X%U&H5Bj@K718C}&h+7tXpQhP zs21`!j~3mu4$mv=5@@oUj8I+cy5Vorx_$H=jd;nD0On;G?buH7- zUoHK`I2$#&u4Q_(K3|6M2#PK2FY|CeOmVF%A)Ug)dB=YB!kIwiaty=Yer5X^U@?&Z zyy9rgJO_K2yQN#jZ%GXJTkT$jbmKLPCKDObw?*n?{6!7Hw5w-9TD^PKS&`N|k$w}! zn7SK(=ek=AG+>2H%L!fDSt9O4Uz-@bgyH5BCJ%bF+QwN||20!z&PE*4VQ6`$2lnt~ z-LwaBxQ3K+6woiEoC#D4g-2PrwRJp6QwgWOBu)kPKXbP&m1B3<^m1NHiqm7)K6jqy zsdRJ)7KV%<#QKM+uBBjr`%orCkniDgkpY1-U^@w4|H#^;K&IZjWhod&P0OYweT9NU zQ5sZ?p-%Ob3AylI_`fg+7B$K;ThWFkV+2lktdYF75N`ARNL6UO-W_-b;eC_=OS~;| zz#L;E2ke()QBF_>tPX#*ctzR|$3_m=oUxGuHkDGBsCnw_v0+zasNa4q*1iCPHknRh zV5M@|2TT=)$w;I45U^)-=1E4CJX2$_uK9nNXcv~pO5f9=c|ie;s>NE7K`IjU6R_tw zusu8>%Y2W99gb6;;OOHRfm_BNDC8-IC@C^|iXD$*5pRczIC)4U*bWm360D^JUP>VG zJS*rCHdf@>rsTQsChjThAxPkUGKJeCl(GjBzrq9CiQyUZY@_4>BHPoD4R#J{62jk1 zhHQ=wwWCYpxT3drnF1tlV5r4ZYbsQ2Va6|X(4J;9p_bN4l4w*m)o-wrLLn#X^^m0m zvS=Yo5j*)gW@$9zNMROHcXC*k*C7j<_XYI!XH1@KM=KwNJe+|%n4^D?xXeKQOvZS5 zI$J#E;s|a#&7spl}mFu9}kE$>t$GBh3r9~1#w|=QEi_Nj1o25 zj1()yOc2G?y=k$Y#bvpl-7Fs2^11vKx9H@!MIU_(ix%Y^vM5CZ)r>#eDW2J8#xHi$ zXYq=4QL&;3d$3ps^qHDOWslmd^rBwZycdH@z&p|P@;hR-WDP_`4KqV&@=UFG`>^u% z@OYYRqwuX`eQ1IfC<*rQ1eYJxV_=*&FiAtT=_Ca)wr(Zz&!DF6HyDRU&KsvejUAe9 z;ttsPC{7bhxrS19hNqOmQ?7ZG&yJ2`wmdZB7Qp{%W z7aO{57@FKtVfNR^dIRwXo~1ddc_lhn(!Q{GNm)0A>sNBggSGf+={)*0J3nJZI0aKwy0@-uF%5pPwS~WKmIM5xo4>z z7Yq$(6+p42D|>0l8{~s4)J+MFcD3Uz^|W1eU!mzMkW*DmmPVy?e}ngNY{Kvp0ngFo z$;)t;MkYea&7|)@LIn-LhL3bA>w7>nf>-=%R(!fkqUbIWbmtS@S0x$C=Opv}tUtmr z40EaCmsd@U&(Ci>5lHZa(Vh@R`xzh5?h&*f%8O5XK@{x;g7%q2d+Sw+Kkd6#f5Xe7 z5ZQDG___hfjv!nUMfg-C{3Sv7$bJa#Z?eq1cBfAb(%pLPiGqY94k1$u!Xrr6Mv-p! z0_iIS>6J78fkyj>9gUo)<6!pyif@mT)#%$Fo2 zd&T!o1Xi>los+wl{7s$NqL3dXTZ?2@3$p#w`|I-@3`L6JK}DZW0kU&!dDkL4i^|6c zr1C*I5JPyT(%)0U`kQRq-WiJhofPcvOvxD4-*Z2Jf=g9$0%BMNMaoLMPRNu4-~cpx z)p!CFgdYF}pm1wQ#{?vf48SE6hM!<(dpO;zuDDoILX-TXxqRE!nVErnttqDmC1uVXmG&fz8ZK zVDeN1bvg(?auV6-d-Z4ln&(H1mOS|qKbiL9K(NWV@hzb zm*9a!)>vr!;G(Clb;J~`Lu&*7kIl>Q)AoRQ89k+6s5{4BrajThwYv%s3zp7JvWB3d z8b-+h*?=D3D!GMfFf2vpgUYafChuq^DzIep1)Z}~h4dj!s4B!yQWb8*Z|;avytAl; zlUipZy;cX_mA6xbX)f&?WLxfp>DL)H{r3G8Z@hf>@;DVF&>VEl(y$fL3HjC7=6e;C z=ihp`baSxDiJ}p^KYoVAqGXAe>?Ebb9noo*kXFWxqCKmEuH!-w$p7&R9VZIS6@{W0 z#k@Ij(|#-FofAbo(>eEo6yUGQ*r0P?a(T)oq3rp)oVcLY?ILp`@QaRJ8ppSK`)<=Y zQ*Rl@AtrgMkiE^1*(t#9qEuU$6BozkF(|3biJ@0Aun+9r)Tkv*_tKW8AqIQ^RlTGa*wOQ z$esj3yApB#cRi^kYZJ8Pq%O0-`p>s&?f}6YM-rNW3ta7*vXsrjBRo$kk(;I6)b}Yh zDhnGkL&&EM@)+6Vo{dIs84dUfXCu=e)Dgf1$fjk5MXd1O>8f^0rzVriLfszhK$tdN zKEuA+ty9tLXWovWSPMTb7_2XqZL#WcD(3?QNL6_edttf|ZJ)*_a1%Rpg*E7!Gf<}8 z%J-+X^`!L}mK{$k3(UiI8bMUMTI3lilpJJ2U?>zB*OwJ{1qWt}t>OVJ-!)6iHb*BE zRiHwb%EqLkY#|;$6~F8kXNwhykO#)f%Va@(LLii@4cbj!hS!KZ$rRyn|F!j zIlwm3nwy~2TlMg#%w4ak$t`zfGCgdD=zf(=G>EiGt~WRH4MVSM!KmVI;I`zhmVk89 zN<4YlJ@azn8TTBudA7D;5RUf&rr!sBa)Vr^DTmo#3_~i<)^utHs9bl+Po_CKjgT6; zD=fn&J-A?uBF8PE9H{Mp364W~mt$A1*|}b&sBfM`pTjrtw@iqu$Vu(ye*J4Va9+x} zLE)v`_{}{sQd@cgfDpe77DY=U;X&x=ejiSd(jE(_rr?WA{!MJ9x~%EDRf% zBK&?{{w*ack&M?jJgOPN;cyjcny<;YJ`UiJ&K9YJ5ZJOIZPfjGNBWKvgJ#@ZXq0aIm z3p)*&<@=y|9nP`sw6x>Rv&S&oN?MB|=d0m49YibUfd$(b>g5#naW~x+RV!Q|+uu#g zK63ZH|5AXim^z&=gpHAkJFAd3ejffJOAGvL+Q?XbR>CWy zQrTznlx;Kc{jLC^5K>7qT)_F(rabhnlWJYfWGf447D0nr`R<1nr;wQMi+QjwHu8Ou zZ&6K+d|%uu;jNtcOefEvw%sZZ2XXU2I5L%9NSj*uTf|=UYslYYgM_w~blX?T#%>e( zL0#h|5X!9uaK@H0edy_D@U$k$nh%^-DXXC8De&Bhw4mMoPyc>8AP+ybCs}UH>>luAeburZP1##8bI#clG5pTo`x5FZgq{v zksiF3DrHOB6ndQRCDHmZbS~`}0cnrzP7)_+DSIH4iU314P_2lQ@Ox}TMk#yA!YwVd z2#+TBjNk9rrS=b#JA(SK#yzAc=$1R=FO}2HU9ZOdbOoxX8{dWZUFL2Wv2+FI4Na_s z;=xGB6b%XT8K;Mg^9|$SP<$MC8DK%eG!GJM>BKuo=edTPurZ1I<-iJq8Y9s+83y2# z?X;i9g@_ypay+3`4ARAB5Xphmq=y){Hxl7u=~3 zXG?IIi0BL=`j5(UB-1vaa|!OI>VfrIm>9!QCy^GM$d}*J;YD07LU{b-3eEFs@lwTf zr%rR*0seqC@E*vp0*X1-trp6FQZ_ZzN&6umM%cp*n2+ai7-Ms1-6+S7^{}<5Y;|%A zenGPrqt!ldl0NXR|3PKIA6Fom=OUHtzW4PiHy63&?wVxS+@+cXoV~D8B$)9TOZd#s zrSm4!3j#u!rbbnSrW0;>ZK=5dj=$BvJRY1;sc`tmM#TMq58rukQ3=C9?vL)@P zh9udqp;3$d_K2=uc_Oy!E$uqhWRJfn*o0cBPQ83PG5s1|OhdAZK0BB19>Mibp)zyh zlzG{sQ*Bm5iBr^3Co)>{y_Fg#-gq4Q$YO4GU^ID5y;*8q<8$bxPWpD(MY7Vgbr;OL zbS;hqRQP%Dh%48T{zj%yyXH$E2+(uax~=pV1(Rc&@YRks;x}KU@AQ(MpyA(El`#CX z^v`LonyPS`dh;%XPO_zixXg05Ra0Xrb!D*|?lJ%ajgAJcjYx;-WcJ3bs*piogJcQ_ zfBqJHU!Hi7{kY*wfbPdFW=d=S)YL1t&{SPU7MXr!F6?o{KH^ZI6XmY$dU27<8lr$-TigOI8z{a zUbs`n6!JcSE}8pORG0h>cCb-$F>j~)$;(AGmSXm97B=j)g-y-F-X7Z){0+e>dV;V&4NoRtGiJiY=Nx>m!O?ovdgaM$?)SpcM^M=ZgI~w zvIpUZ@6?I{DZ^Kp@2bpoRpvF#EvQ^T_L1Rus^LLo+L$vYK35zwyM36?dOc!H)U-lhuC^wC?ssnP zw`VNEKiitO{TxHkW5CHw5jqpS_R@VG@BgBIofji1QcO$nB#UAz=CQvFJs*%Sn9ex^ z4Ud(=iJJ>80eNq04o8#9uV%Vv;yIcWTmC=FX8uEK1+)j+`RUjw2adx-CqX;?Q>dL7 z3NjU1=SA0x`H~(l{mpzYozo23pO^kY7Ye?5pzLTVwMhOoJ zIM$5toZO+dlfDgv6I=yNYv_F&3c0XpmnsAhWTr}7lg_rki0_o z7`8cbC-BT9PlWN!7f=umvAh}Vk=IFlPL~gat|ft``U(n@Xh3Q*P{mb5tOGRS3_Kdh zw(Us5jR)C|PCf`)*0BxE9Ee)>C+RvUolTTpmC8Na6e`hvqhCT-(zry>I*1L9L+edQ zYd5F$%B|Ny>pj<|bySE}AwOKDW33Uy-oUEfj3u@fq)lrZC)RR3#NJKBwqAYw+xj1W zqOd-K!W6b+dn|=}Un7n=FS{0|J-T0f&$?)$f^T%P0|gqb*}qsHT8_95{kKO#oVzyA zW3J;rGl3t?Tn}sZ!%JMkiwB97Tj5VFg{tPWt_jK+UM2b)64y#4R9kCkCC90)DOlUn z!P@B3lh2D0U;vMd?JM@Up$Hb2(G)H{st4;e77RBs0YE?XX@8Kj1#!6)$|IJ&# zPXS*tmLpt$pBMy0RCQ`@Qu)Wj3q}!?xv$=E->Hv|VWPYzw7#Tz2b3}z`vp2g?bxMu zbQm3-8Z#V`y5{}gLK99QdLjipH<&!;_Zw~nE(kjjdw5AsvdVUYPqWmU$75HcnEsZX zg++ap`BYGXzbXM=jix>3_j}0m?6ihEu$I;(IYU&oo5`DABlGa&z|X%3 zlPOb4sRyw~o90mm3#?%64>*&5e`uGv~#=AAT-*y-kuE-ts>NQ<>IAjtM+_N4;b z3q#g90eQg>$0;6wi_9G`73oyr%d1}GO@_RdVBR9kyMprmPUPj&2eQ|~Ayo^9R4qBp zW8S3LdhesWl{~Nc2-HbO-Ufc;ZJ@1H|OILZ9fbUen zOqSpab!8YCQ@i}IMD23(E0;dxl-Tn)(0++HwpV;p^b!+Aafo1(q_9~sU! zDDS@Xe`GipQ{L8K-d3LXYNM)aoCs^pa_p8^{JEQBnfKaoe3FYmOoc!VK_;vu0-wYR zdoZ$CtbG_k(9H+dp6i+spQIv)Ph#l@pQME2la#Uc+X$Z|$hrx9k`hvZT4HaC|C~wOwtQ~{?(XHMd?y3)_f)1Mo0%6 zc37Ij>qVMYCCv&*BP!atUR0#bH1U@YQ$?9QnGqE|s}yo!qDVIj(s2Zqxz)@&GA?wA z0BEUTsgRiKwuX58s8Pwp!ztlxARsfjPQ~xVQ^lZb8g3}&)_h-XLhCic8ZRrdu*tAH z;JMY5Hi@$snHc9pdBJAr^o4leDms_Ep%$Uz*t zD-^?2>UHVS?k>HT?CuUQ$6T!{g0lbG7(p3MR`%_%AY}sxLFR~_&`jnNlnFjoK-6{7Q=(za?z0{%C&N+H%bok+=g3Ki7 zL3}8a$JvYih!2!o2)o6-1B1vTyR_d-zFY>GpjHcu5Pjt!8}ZHAfP!RKEKuxjT~DYh zDbFaNEs5QgJS;9Dt?~jA-*&AEK2r9_pT+Eth1`2w!zMg(C1CP&C1=3;Lq$^;K>(0R z4PmD!m{I6{O1ro9XxRXLO(G9?#iIhd2&1LO=q6Ma3vl<6n$_^dqK$!+LOiKB`*j|S zTR8}%ARp+hOD2;zp&p@EnR;Uc3R3_?3SflrE-&2c2X-~1ZAM5064tI!LPZ)Ixq%jH zDu_TRe@5E7;>{pTpQWAH7JiqD2S>xeV!Ho-nQZ$~g#mzZEZrtIr48U4g28nc5Hz3k zv?Y+#TFV=fH9U@7M9nCOYDPgsGlKGQe@&VDveK0Pn$aC>22zI1fM|+A$;$7SOTSO| z%S~yz*rvGt3{9!vO?mjn{+d!E`k%f@bq%vR#GbZ1nfya4z!j8g+4TDM<20f$EIcfZ~3}x}I{gPlv+hc+%Vv zqpQ;zxs2p}YPP(A2rIU(hCHn3cbs_0{KuhjqUe@d*qGG{(~?9tk7yUrf&rAwM|6$F z2`av0jDw?Ti5_o^xSpVpR7IuaJCEV0bdArTh5_NzIF^_%H}yzTMJcxI@SYl#Z;yNc z5#xNhX6ZFGlv-Uaa9gHLx$SNwXr@f+Xvo8H*oh#($~_~qTo&Y zreAinr}(CyaJ-cQJA`lgS?B?8k5A?8`M4cA&bt>r9ysC4)p++}AMC@YY5eIKU#|9J zcxw077KPI zj9CKzjmNNS!pa)RS|ZERvWJzjI>O5;7iHB#S+!7Btz`7i2`eax&5A5Y!z87kUEu`{ z5e0?qs4%H<4;AWrIyahL49W2^QoN+P?(@T-VBmGLD>JpKh!f&q)Cs}wr^d$|5#je! zT7i#xbaNa$vP*p-JhGjhtKLjKAOCfmYfAq&Q^EVTu()E%&6}ET-VU&r5EAVGQ=bb2 z6mx+ga7e1|&qfp5kz4!vhcS!6|Alh@Go~{*9uoRX=s7Qo3>ESS?j1oc-qg$ef8*f& zWn`z(c-U@@cbpe1r}@vf^aWgFgbeVSOsuin8Zi6qHosOJcdat<&Hr+|rM_1XZ)tTn z-V*=G&G=`Jpm|)3K!H5gf@>X*kxMb*KC~SV5c^(}2z+`A^0tMr!Dj%Y01H4Vh`ijO zu#3ZkuuRb<1w-AT?MZB;%U%1f2ajyhslFh}*!NEWsE4SbCmcI4%fQ)5;I;M< zx5VQ>Pgy<^{W>3} z?d;GsuEm2tSCehk=o){G=3xR;U^%^yzR2qX4mSrqLW4f0=qv)yRMGNk{QJBt_WL!$ z(KPlxFS8~C8EWJuvx+*=1)P2PymFbB2@fh%Oa<9tt?@P)bS(|Mm3o-{8Hl`6>^NbR z&$@HearEdD-LpuTuaRCHYu%#fu@9f4d7LXBXpNc3IidUWOuU`Wvk2>o0nE5gnb0{wS~H## zh6xzGS9d*h&b&6A6N7a2Pv%PM+z3jw?9sKcls>&$m?a>kKTW*u9u@xlSEG0F>i+2^ zQddTkI*|3Oi6!;JXLYFl$S_}#KS92Jv0j;qd>Z=ihy)-ML38-=KTK!9%$fYk9dntF zKq274CRs(yA5-u6PI9eL;v0bs+o>6+l2bB6 zR@w&Vukm8v_9Kw>aN)^$!!+{6(53$JS55~I(IpjsJn?EEK-cA?eW$#mgV*HM4^P5t zau9PdVW;MHHCh(#qN{R{(d^3{AoUThCz7FSIYNfsc;QXa{pLeHyJUT658jBw$h+tj zFa(P^1cS_dM674wgzZJ+uJ-ESy?847KoxaJpMYctId3^KUy|7SL%3BKwgc1?uWP1o(?IW_6Gvi!_ZIe`>NS*1G>l#thS&sY0Y5j zI0@*g^=V#$k-|1fFj{sza4z`LUJ_%7LxMepb4}O$1N86Y)@jJki-nmKbf*el1V1_i z53uEfl#5{UGwg!(xP~7*t^8vpKX|YZP3$Q+i3*u9ND&yURKfx1->5x}K^3N5tsm3L}<_sKvdQ&#YWDz;HC3`Tlh8K@Wt&>?Dv;}H#@V8wUwoFqtP z{DyQ3=>J|R&TT|f4BCDEllZpC_lEFo@p6ab+mej5kz}NeBqJd!U?y=%!M6_+vYF}pA>xC)!(V7-q2nX-*)2OU~lb+(_0+hHnP9|NqpOI zc~U(fh;PgLkV}sL48ARvb9H=Mx(|g}Y$mmUt{I(o*@&+xtRh}h=k#${bRHJT`rh3F zcC(w!_&?UJ1vsiIO?N{#wAk2dkq-xg;TsnR22KpDSCKLbgSn>TwO&WHut>Tj~0lt z`mCQq5S|#2Q^6q_Qh82=n0#CEdA0cKhB(R`68TBXGH6w_y<@YAi0szA`Eqb<|Q9U2~7I9kEKuyI^O~U8*~$;DWYF_EeaA z=X!NP{@qxj9oNWF2&%Ju!aDnEYz8ENj9@v+n^$~576BMhee_|HpN3mh)R69M8IoK? zwTO|=HP|;r>Ej!AZjQFkcgvULclW|s>VoGkd7A_c|0vlx-z6;*3$AP`5QP~C=qfoy zIY~yd8|}mx>~{l+Vh_k7TIsjY9P?3Sw>wRyd|4 zAdjcRCuijpauGSWZML+g^2K3XuI%>N*#CCD}2L?2+sXXa~MNBg>H&4&R%a=!JUJutqrWsfLz0CXS33w#a4Xcg+{E6TI25H z0HW`bKd~hWy_Qd)!mn9{D4Dm5ohNli;x+A`WHkeOE?Y$@QLXZ}GdN1nih(F%Q%C{y zo}2j#6eMe1WKdLoNKE;COnDm_`o@`#t?~u_=DN9#?ZT{R&Uv4`L`pZOvA?DHJTPH`}XI?K)=qyXg4(;zXF!*GEMCClY) z^|ZQq9*1lMkox&9qEoU`85+5cfpqvg|8!wax zjWis6ioQk~j$LbXmoz9}0W#~#ID@)?FsxirP@v<7Px`g$8cE=X%OA6++9H7;&ftk~ zA8Ph~3~Zsp1bAk0cqrbXd~_gC>R-`>LB^)QHmGz4M<`saaPI1OV#!FY5ZkhELprUZ z=gD+h)1~rT-q6xQzktGlAv4p^7Vg9^Q;50pc8R0TD{TxWNysP5{1Cjt? zKU+gz4Yj``)V>yNt4;4?Ieycp%ljQA|6p{0b{W|}qtZ0yx}^kmL$mX8^MCY1OZVbM zo)m~OPZ_ztTnHOJvVCZDIBPh#BhvCxSt9d6{5#dp*MtI;yu`y_Sfy?@50|>sqcZ>#_-xC+GQz@jGGnKL z^YmUi=Ym<79#NODYz;#y{eD6cCzkLD;c=53YvcGKP7dd z?oS_@&AM+7HA!GKw*J7DlDAL|BfrvuP?Qtk2MF-pYr?=VrY>@T&S05dcQ4gl9jcqc z$*6Agp1G9dv54r3BHd-^-pDI?8oQEzLQ~pFQ@S(MOP1(m3VP{lpliE`(Pa>R5izVb z_LC*U-0x!esmIC&DQw8qv-ncPV{SKM^GQZK&~9`k4k`De#V-M z82J|Yyt9zZ^Gbe20VrRYPRUCxN*y|qmZgzE8OcuTwzC2f=GHyWeyI%3iQ3bgkg?LE~? zEC_?uqz~1@vrDMg&W$(d`MA4FLa8PadoT_5{;iRY(0jD+E+T=sg+5Q4Z+yLpCex;fp~{<+4l_I< zezE;p^H&8tN?@9uB%U6~)18lgpZNfN5yqh7BhZdZ7<4j0IV)lb*aI0Re#$M~=4ldF@YpJ5;~uIRoN>L`I6THx9~L7OBHRJl zqT)*;nD8wT7wHJ(n-~p8Z>^-u$+rjx(S$&fUN@Nz;R;G`3_xcp=A1OTflM4lQTBEC zZy*hadixb}cy;i1?9x%bfYl(k=1?T`2C9H&T!^qG|Jj8!rbn zef^dXxqdlf{k&Ci*YCcKU(@xxC@5ALX}%CC}lClM9{)qXG#gkE^to+>~yf#{QO$ z@--%#cq|!pepisR53m`g+S`haINinUUDBl>EQr!+{E|wB2@fA@_$GS)<+AP@*=k?Z zJhNb2Uw&Ehn9@I3dk0VutopKG(Fd9d;(Z669L_BDldP*6kM+b)4(98wroWz4HQ{&= z>XSR=D%D7R%VRe)?#sQF)lk)PLRhCHvQZ{+8-vwXmLO~LnmLr`lO_0fC?)O>XS6vy(F=Y~AB74cs;{CU;T zr?zlR3+*t7ezJIODZ_}wEhjO26ife6v}eYg@VsP{&x=BJCAzZ6m+=a|o+@+^=lEuX z^nRXubJZ_9a11;%2CW`>HM-S}tH0Ky#DC-V0F|E;O*=7^N@pxm=|EVjUY_2kal9m- zh{je7Gx4m2Q!fOu=?(*@uh}PSzNTpinCBx{`fiC;R@39KH~uQ`d(B6Od{~_``pT3gt=^tum!$1{7}sPwj{A1R zZ)f+LiR8?&JOF)vgY^Gkcze@_1%z%g`))5UPyS$k3E>JE9i>>0XIF#vz;NSB)yD_}c^(?H`j13)^~|v#Th?M)=e*$EjFn7nQ$LL=oCN-|7C7BktCyKs z8OYTAliHRSyq*1xlTX$1@eoTioc`TKy#IeM z8qhyOZaW7cH{D;CV=X<3vf6o-2p9H9B88V5R1)|{N!0=G9xYP-w;?+JYr~dkeQkz3 zAf6hj=0o?v3;S>%@HFBb>ehYzp)}$h>izFR8o%4ZZ-Nx|WZTEx4cKGTK0MaGChES; z9$gBhz~y{=**-A#do3ZxeloLp^XX z65iVyBjL5bRKY)q*Z}-7yFeL+9U-i}X zI_78adMPiu0f-tfb5Di!lX=aO^;m86?VzMEWf1cnR*ZmRLiKv40|iJ^=4$6 z--`&rN0d6h8HyeNSU{)0#0Mrei++V90<{P5RM)`%pZ9RcpQmb%Iby%pHPgDkVdj(l zp`Lc}O*5bEnR?nq8jUt>e?4}w-W=M+*QuZ(T(FA@4ulIfQ-!_&b%hYD*H$r>VJ~kZ8Z+wD5Tfrx+d<0~pvBb7 zxV7Mdb$9gy7>UDpK*(blQ)yJc&Fa%6FOi)`XzdK@-@XoNK^Rbndh`cg4fW<%s&E(( z+DJKjp?UE$5@Q_ce1vtd3)b0t0(`fWIvkb02Lx0~#Onj$*FWJ;fmf*sRqegp+ZKrI zVnmMu*oi5o<45q%&LU65?FB|5&t$(g*|xlcHT$5{6H5l9Sy_v$B!j;sf2;!1{sR!0Atuyrdv}o8J~8PMTB7nESEm<}~)V)I8hI z4*6bVGzu<1^(&_9m=A`<%iQ4l?p-W zB0K#^QWiPr2ZXDMkmJl;>f%2Vxv>UhFRpCv&%N*aKFxdRO+1wH_9^JPS|cDl7L?;*ZH)1o%0Lu^p7NuBXbzqZ%N_r!sG z!IVde|7Po}6Sz6amdGANd#6e5Ti34<f7(d4$xF+hYNjGM4P8<#YpS z;zr{4k!Py7k#5&7Aoa=6y{s24Wuvxi+9PfmxnpHs@Qx{c8rLt#jetS&KYkDK7SsPH z4!O-%ZX%WoemqVdoisPsl}jH-r-(G}yg1qrd*!q9rkg~yF5)Ipx*w|Q?}{0S3+Dgv zg>DjHcd-TG@_z0PsJ=Olr2ck=+#Mk#1tXrN)WKe~5U-M?12m38HkoQkr~#-5seTcu z2@>!g!BI#aUxxrG>g@Q?EaT81xl_|-B0i)yXaLpN6?6W1gQ?*Le(4WXb#oQS6t(li zxQd8RKc~;Ys+c9ltRX6DCP@^@BIV?YWD5?HkLVYLEMyh3o+VMcLPnr*Gz#3DgFRX2uW7n9b=5hl>t;g>#I#F_Ta zj<`@<=n+tO?h)M{siLve+t+}&B%+fDCKl71V72_zGOD##Bm358FCKWQcUpP)T{zP+MU91`#)@!_-;6_zM7>55 zATazFK~R4z4g_=sh|49ld$gW)bumn~)DbNd4-LsV7LQaw!J_Rg*RhqiRq-oVp6iBCm%8#?Ii|O(jc`U9+&X~^# zncE|gxmAx`%85MG#&PX2Pyj@~>h`>|MhnJtP`Ao*+QA zuD*G|(B^J%Sv=Ol3Dpc#5Q{5;W6;+7DyMkm*dhWi6Zt#*oI z_2+g1{2>lFWSjTvueP*@=M+k?3ybW0(VNEqqC2Ns{aG&j50fIo-IV(icmP4~)oWUz zk$(2GpYr{(dj-smp!0L-(O0-8R77H-gn0QDZu3n969ec{8v!&GVqxqc(S5#xXUw$S&mY?U4AmksU=|VprOH(DEX1Xb$7%7$bs6Qh_gV zhDuy8K?)Qlr?J1QVP?={wljdbgTx?u&?80SFJ^VnM+qzO2*q@a^vtaTQL0#CYCyxwMHm?-+YZ6T8^n#n8=+{;*&y#sdvrv#7F!qPN zjeCgt5b{Dsm*-~z!Kha=%-j?Q&=Id}=bCW#GO$Faas@drYeXvY{e=tL5T5(su#okgr@CqG#XVIY~9 zaGCH>=z{IkXM7gfZQTYRVZsEG@JfiUxezG0wB8Vc&>tf8HTylzy?5@+1Y>{u$MOfc zbMAM~<2&bk=X;;Lct+yNUScGQ__k62skwp~2&c|KICTahulg8YTl!It%0OVace%3b zU1A`L1Owq(IKsa=3x9NQUSQ%MuDLzx{4P*do+a@)!;kW@$eXt1gHd#4=v>d^NW$@) znuVm$--IYG(G<6hB(W-g9*jiX=YLKM_x<*SXV<}AabAiv;#M`R;O44J$Q5k~($8+? zIM62a)Mp7e={qFgzdjfz0ndDhNN;Wjrzs#7VtXqQR_87*OI8B!5EoJ43konbeX6gU z5L!7$1a{9eu790=&BV}@8|)ZzE7vTYwN}b~PZ+H4H<(h7!?3FFa-=BmcX{FT0OG6a zMt7Ez;`cl1RZ(4eCX^6wZ3omde>9#>O70gCawVsxs~hK4N_*z?-*o9f#7JI29?wwa z@t$xt$>Um6*|AMwU{aY&DLbHxNx@?yrxB0gJywuS54nArQ)mtrt)QzZ>B7V#LA5PLQJlBTx&yAb+ z6XJL`dXM}NB?aqN`hy7pwWMJ^ZZ8io7)+=S`Rjgdd^_BW2()1%YUPwQ!NmOU{%A2C zwSewUH%^jZVU^jzgqoUV>UULhH7)SkXK4UCt*?a6Elsop%4e~2|%_=k|S#LjmRDpQ^}!uLP( z(uruA(lTkavHy#Wzsdgfc##UMdcjjvVAl&) zZXzq#6oICv=x5W_0VzSAbex({WhBjw62Z8^M=jo+G1`U76OE zD%|S289U19XT_`W_=+4tyBU4ThvF4^BNC3VuI*|mX}-khlvklt1S=@N6%dR`KaECa zA9y){gH0|FnItXwi^V9|^9B%m zy$P#FyHJTVwg@n!ocPOW^wzsbpQhSY^v;Xe>Le4#f^$81Pg{WVAh6$Z0Pkj(3oLSg zE-yt51Ou^~Cz=!k#VUx49snPu^Z+}woE|`Esi22+IWV6dGUUJlddQRmm>L$%vHhhz zK>qJ#f0C9L$-nRxckp7i=*-@h@Z4P1p`L%^Rn?8q*^I>6gw{AyFaE5Xi{tj|oMG+P zqjM79&$od0YhNMn*KH47aKCO&{JLdL<8b!tOs2oM$g_3Y-ux62`&_+X0~O@y1$9)A zuNVA^3JUasUr>QlFDRjcBE6u13X1iDsZXKPic3)M=P>-RsX|cR_tztEnhrdIzK%N7 z&a96))Q+ylGbmFK>-F#pGhgEGnk&n421UL}q|M1Y$^Qpxr2CSr4uY!wWNutYKKhzl zpG?uz5piA!gOUGVD2+yk@D6?|7XhlQ+R>Cgpv zq|(`=$E%>?9^Gx_338!jp8Ny0kE8Y7K1-SSUaIC~0VA-IE$DLO%9T6zMJYdb(TjOz zl1k=TfQH$P2U5bA1&d@R^Lr>K80#M@{9iA)AM(V0=qXQNPuX)$x2L@6zTi$+oA`Ch zrqx651n7r)WGNRDyMFy!35mn}P&8AGi^i?+88`Z^gxvJhP?X*C=pAhLL_R_Z|1suN zGaQN-r@8=+#4vr*NZV3*X*i#FTd`X}q#9}KO8bGF(C5b8qIi>n>l$z)WSXMa*+fI89@7A9W zr!+j6ZSU)nLw#d=o`JrEEIVZWcuBw(zmO6qN7!#+KtQ>w=xtb|iGqXP2C1ykLdQ+;>PDI$v zD#=ziHEBqEnTLljODw+32R&pn7c__*n8Jn*z#=c_s#L%;FKAxI!VQ@*Te8YY`C1x?jt_xF>+rB>j>V!CuxS4Ov+4vw zG|AA4sIt}hOwsBIL{_#DSuv@iOzzd&r)k+oqBpzP;JoR1ckdsK`ViRi+y&nF@!Or4 zUDcH>L5rvF$Vu4ZDbZrU*rG;aRLUc7q!#tI)cxe$MmVuOje2Br|De&@gZTo=gh-$6 zC%n*o66;Z>6YPz$34d+O6KsNnzlgCx$Ylg6xEkdVf^-VV`w6m0K+Ysc!OSSP5~NE& zK1h%i0{(QROQzQ#_rR?rhA1(yOgnStBUM&l#2uSu91+}7(jd} ztML;!yd5FRIQ~Ccg zlv^CM_^LmK5f5Mrk;}GeDMWlhw@ z`%@}XJ197~fP_}-GMJnJi=#vOc=-sahgXF^?H+&HI@Cuqm+W7X;jGzemkjg9$1kWK6bue3}yG z+uO5akQ?9EQnC^j0bB&&z5=+f{AtS($|4|&_0D298yO4OvMXT^_Z;aG{fX~kvDQPE z=wT_*rv*k98r|1fwIMPn$ z7`2vSy7>qDTcNE94DQaSSUii|#asyBqZ}}U0wkcLg6Dg_97IYQ{zS5f4N{J9B>TD0 zC<8x^>@E>k9^u%~%0EBc1)t*pp{WI!;RR82q@S1;%;iu)exuR5Y`(V37RdS)MozIr z5n(t`yuPShEaL?p?oPK(YK!f22~{KL?n^^8P(1 zBSOhgd(s+i#^c&wnzcaM4{|mg-opDJi?~?QSA?7Kz{mFm#`k8{c!Znt1aFoX{XQe> z9>sqeGm{L1wkx4_y8dCJWS2TgW5%j5WEn^iR+=mnVdcrBk}ij-DGrlzq=Y8NCD$WN z*h!OLD%Y3uFT%HC{seF^hwLlm{T%}scy&0eY+1*O!ymE!O;>U0{z%DWk+R;&E}B<2 zCse@B{G*UsVm|?D?FpYdDYSJ5Ptc^8x`IJ1%g30FOS!VfisW6?>yxqE0 zb;7f1pp}WT+G+aZB;*I@P+Fx8!93!k8i3k($TW;`u7CSmBtlR!K&>L<0>OHZC0*a< z$*2pWwBWJnk^7>_mJs#XVbSzxe96A)Rhd0*#q4t+esJpQx(#v+PZdYYW!XIvO>4iAC}<^ zD9y4DOwhAr%2Y5?oV8tSa7$ylmKL2%6Two$z0BwS5(=m)^^0`=B3=Dr=HW0@4ON!O z?6^sWKQT|!8AF)f-S?j)cuXR6E}s&QJG$5nD;n5%SXSBW7Y~OAo@ar`^D7pHzDo>h z_&tk3k?C8a#Gvc8p=(a8{Npbe)Gvn`p*QNBy33j*;Qm(NtxYm~ zjeZ9BsRcf*C8#7p*%JibUoj$qYz6aiA?a!mo%+&YYq00e?o9(ge{6Lh;n~QC^Xy2^ z^j3X2t=E@R$MUpz!ZhF!I9)WnWmCc^nBE$N&ueJWNRjWcUIJ0;C2aG3itA-?`j%m= zmz~;rA!_r=tGr&~_sLUHI)W$hIW#=|z$33ES~HIAs(#=bO+PT~#X~9JzkTKVFCI+d zh=3w_@P+2FTFj$oc=Oo!YWzG-r+JLNZ|@(S&x8N)e1=~hYCaA7f0%Rm!Ydcp|2UW5 zZwehR4?Trbw@;B?8L8I{@y&a zeW^rCm?YFgc!uYloJJ9OWZK!iIibDj0H9e8pb`ns<0X7@2R0{U$6q4YJOR6iVISL^ z&@A=u1e-5l=QC_!;y%oHMUBM641XdL&s{o$;|Y=fQLKr3l$!9%V(}%iJWB7Tg#D9W z#{LzF{yDjS&6^VT@Am{-EMU<)4Y;eT&P5_j$$M@b5J_C{KuD0ph0Y11D3Yvs%<`pYkuj!hi)b3g>^vnU=%s?f zD&=M>!7Qtzy3mh-^D~KHku$mq-`zg(8hfOdFCSnK+OuJM^+E32Rk0aq zF3y6toywONpN+WOOZ?=8ybreIyiiV1D|q+D&kZR?h=gnWE zgfK6((4_oOeKy*&4-HLk+(+?p*;`RKH7Vat*F8LZ=l+lcRK#-)`eUE%Cl`;su)G@r zxE6pKnUwC^honFB{>{YiTb8~)Bmt`3hU?Dcxxu@wj~;ThQ0zt*y31QTOdjsP)UHKk zOO5!I+j}qUqWf<7Jff#0P~}k$v=xpPk3Wi@7r)U99-x9}^@6fT(US3^NJlbC`$c}S zlAy7+So220|ePAAlELY zow`?m9;2Td_X*fu`dPnU0EejB4+Lx(RT~zd^U^j zNuwFk#vSMumCol5^oUwZ7E@}HqgsNQOzM4~XlBIY*o-2+ZGR+TIvJR64uDOb77R%% zuv38Ie94P(0aAe#eoS?=c`os`Fe#C}Ewl%!dZe`>`g=*c@vqh+vd-3xHQ0*qRiR ze@T!b&vjz|R9)?D-AfNyKrHfrUhb;L7fH%z*K1L6B7vwS_KM`0vm{yi1I}~psiY+( znC8MhTl%)`(-aXOT8mK&$sM(Mc=@Yom zU25k*j3zxBBql=G;h-6b@t5JYFefS|JoVlr()>M_=eypHd9c06fu-caFV7NbvDJ6Z z^Qa*%VhS7w)ftYIZ1}r8jThZ)mojmJdX3KQ25{#S3|{mEgPTVa__=fy{0CAYwCk}t z3av21*kIL~%A6psq)JsM68#U;Ej%ffQ3WrfS=`SVe1G5=|r8A8fbk#iv4;Bx^&m7dhAkNu*#Z8YtfTm7xd(*3wElyV1FVOEJkkbV|9Q9Z=CCNeUq_H`dbe?#XO;V*0D3X7YXvnGb(5001rERdwx4|- zf%Crr&W-xtrJCS4p=7r(0U}K<N!cc zh|BSL&E0t5kC$#)JPU#9V2R&AS%eM{TBaGk#>_Le)jy=KG5d^d&BgdN(ECR|MLl)h z2>Ns%A@B-g!_!yUtF6!|{IxZqY~;>k4&uvc>S5G3wx>4RuJmtCHM!Kc6|lp_dbRmp zz%41(#6ozv{Y$#Xt5O~7Eal;EA|lj4u0IZNM`UJ1@_}t!kL(d;YZ9EJ>F?}TK?xj) zF-yj4?X2iY5+=GX2aZz=AMckd)wVB(wtr4-ds1OV;%ZRW;~Uq;@`t!S9{6plsZ8pZ zd|_IbRToVQ&J`2uZJ~31W|cYT(7@G_mzIv5RubJG;7&=&G@(`_KWY)}T7rvc*6bX- z4Wa2(lvvgHe9yb~bM--ZGHaeDw=6D0mJWLRxc5DtoXZT^M`>~_JUKNpz3=hd|HJ@8 zS%-cmvJOoJ1l;p5&bcS6_B}>^j zqX}QpX1@}S13IQLk&~#T>eByZ?rXrKDvyPC$tG-)gspLzf0%*;FQyz|a9cJ$GOMW&MneXI$jK}BTz zeBqCa6VsD_`HYUA&lLfuK+WFG@)ALb4tV;}((j&JZDhCKVl!0R+X{=xX?;E1$_zUqbou>q z0J^e=btMep9GnQf`n7pTb-v@COsL{(gJOa?|G77K!1p`3e!?LyV|z*B)|oK^<3h}dA%wtRZWS4KDV{iO*CzDlmo0hr9$S zLkB+7?%3H==s?d&4ey^lrRsgk^J&4tUoS<1>*0Y+aCOnhTQN~?DU;5(ZoF`{G6h?C zdPFNX!+lgS^w!C!V(9u=AU1gzgKjU*oyLk~6tziyk^&E{rTI4xdf^i)g30%=Nv_4! zz3*YOFWc-KEprAGOIL_H7JPfV=t%`bNxEa<*m#sR8bx-Cf^OKd> zZMr88Wqyhh`G9e$he)xet4GCB+2q>bJ!Jas%zzw66!)Wanz^}18@8z>(zJl_RW%n~q_ z+gk)qXXoEa#Iy6f8f!CcRpmMAqaL5OWFA+wNvyF9T>dN*(EONIDLKO zar(ehldxM@opz$WzTtX2QNM}TCJxn$yWqL{`ucu;uD*dc@8`*`VGwv?mr1Lwc#s|+ zHS)(iZXiadf#;ox;(-0WcsyWF0eE;21$OQ4tQ;s}i9OFkrc1$sMQ46>Pn zs?Uq6kf!cbtEl0mio_*yE57sn2ii2mJiNj^L8cPjjifFK1|xzxI3fMOESo=PYh|@| zb~#h{r=V|&B$%{D;XWy~3ioTl;1=l9vP!;;h$UxhEwO2BAXQ10pJ*X+i^8azqA;p; zeFHuH|A1Mq;9ci|?J%jq6G3L(hkPpK6SFpdF*>tW>X_BWSxpKtYYnk5XE8mqCP!h` znh>+rhM3ho8nZ^}6Hki~>Qf3iC03e5!w#N4L*F(&v`a|TqUtmbj-g7@Ie0qrZk%M0 zhtc3it14jc?Lh4@?wf#!DQrf49)(y)&W8st;)AOnh6kJIB^rV9V5X+DWxQ(`!rROb zVq-Tbh)Fdj21ijc4$~))c}ThdFH0oS<(IDjU7jBI%y%OLFjjObIQiejbL6p{LDC&8 zxTA#Dmxfz2RPv|whU_h=A$!a3>dD?Rh3qXBy}bpGZ#KPSZ0fN)siOL~VR);0_`vD3 z`F-_Plz~V^0v{%}=*J)gdF&ET=gJBI-zI|2-+?<;-|+KIAwXK9F18B7M4mU5$!uHV zVn@hWYZfftLyz&F`!de!Fo5x#-;IqOX|%{#!i{s6a1zRw8Z%MLXEzOiO5VpxKUdXm zvM?CAf@IL=BPf*#*J$&3)4*2>I;CyT07pD2ZI&oXn%wSX%?yhg13@X6yUpGPlen-|Rz;nMT)|}*8#FW3Qp(QBjXC)E(868S} z>M!S19>uyR?!fmmY#F`2TnlJ@*(TKfC?vFx47$EnjEU1j@oQ)bZ9Pi&L+~4u%K5#+ z2t}r@7Bja#w2;xXrF9?u#&72%66Y%|T+3bD-{`5x{uWZKS9F^08IOIH*P#|GZ$vYX z|MxfsR8~t&@?ua^&AvxXzDLczIr4MZWX3V)Axv~0gI{0@tZ{sIol*njN3 z--tu%2B>lDt4ToW>u~tackqePeT|%Z&R=#;N!hmX2YY&hBcWT3}2-y3(LDP36S*-Lo08D~2> z|BiPQ`==1@z7KazPJ{beZfV-fZn={JYuV&U`2}1M94#J%ul%B@`nU(H)MRp6Uz;$k{(a z$fE+%@hA=qEeSZVDfrH76J~fbwU8GmR#8rjkMsJon(%ff-uPh`wp7+bt(C}HCl>HEKR*=?a5F0a4%X7^z6zeQ;r6Kafpn3lB;#a*A-GDrj9Z;xs&&Z= zl5yH~v6^wQ1~~x4nsE$GO)sOyP`}mYyuf2Ab3yGKwl@1{YH&ZU91paIHUMiHzvG5( z8tUdmq!+8dPFQZPMxDikd8Ehy=}tstg~e<$8V0|8Y8~H8b@2Twgrv~NBeoMuO1QQo zFR;@UOmNOY>xKg0y>R2O5skxQBWpsnQQ*D@Lxt1cQAs|#Z4>aS7b%}C@VaTqAy1}I z1O~!(flITo1o6H1XK|Xs8aam<#2jXA{u?1~O!HV9pU3X`2<9;_H|q<;^Btlx3#LOw zw-Dj50yKUXdd!fHkezXi>j(1fxpGmeDdwI00+wLV_7-fIss0_ilYs00Jb0N$t)I~K zX>3f#YcwC$B{#jv&0$vMIO!03s^i#Ke*>g81r6*?!oZF_saqRH-W2(Jg#EfO%qWmr z%IRG9O*DSU$mikP!;E+PuAjv%wqwKI(P0Yoo%$hQuY;2PsQj!HtYK~PbNB?;usopE z$3T(~Ib#|8U2`w)FK!u739da1?!YmCo8pWOz~40!MFtcAe|73f4z2;#GXT8S8QTbd z*Gy4jcRe_Z5-my3-%JJe+fS;Ve)+7`^B|DvML2!Jma%dAqUA$kixrS~jVsLhx0@|7 z(N2E)mnTS9bxFfqaZs;4sUxeO2g#~fmMj=IeGL-S@i2nw7XW)pcoOahd6ZPxPtE)GR9{)G`>+QGh89hI8>S>)n^PVfi{Fw_wRx%O$w6uo=2maG$ z0;9U$PYYg`^szFFT#|Ar01I3F8tCe+C~@*7oT;R&e0m-WYj1x$;<}JOH=x-Y^aq50 z8FhvN%~Y}{BG z3eJY}ecgx|8N1z-S&p0L}Q#XAtBhhoc zfSqxVaKxtI!G6+mSz01!gTi&7j~)I{MZ5Xs&Tb#E4bNEhBF^tqINwSKzAN?@b~A$! z*lvoC*iyW${As%O_9Fv-+ZlSByD0c}mw3A?^wzc{`1aTE)<=F4jo*2c=GO_u()JC+ z)Hhxo+!38jjv7Y!D%pj*NBg6ynT2o9N}xBT-$?W1i5? zlk~aAvt*#}!AaqwpZ(iuRI5ybEg@3JDrI@2~}^lP&t~88zyvW7PV>t745sXY~1yK799FWIm+ru5f>_ z?a6In_gr2Ljw-LRCJ6t>$x-Ch*$X=V$LmM;e~kWm` zrPSZScMovR3r3(rBY`}XPwXyvp_^2*phc%8^mdAExf9TI zP7&#+iIkin1U4N_ruZK@jwIeVs{N;zpHV4wjhUZS_hJ>&Hc$)?PH23Y;(=Q7@z6)X z!JBOo$R(SrdV#!`Oi!5I21yA_ll2TI0c!d z@^IIurkG*b+lVi6wwqiME6Ttu+rgdbQp=NlqFFc1fuER4<&HFv3oNQQ(saR}U%FAt0Q(jI9rFtO;harG1HzUAOk~^CXw@n%zXIl z2~Cv(HA@bJzkT7UA+_-jhyvU(=U~D)6D>3Og+Zsu&z_7k3|e&$dRjg3FQ=> z9kvBukFH2lK>dXHH~bc?hTH;m2G z+i)i*$fi!?GpEnUtw?@7UP{7@L*LPL#1yl`x0z_RXsro1&HFHeTqGOFR7%cUpVoCG zRxUCU>^LBSikmyUlK6x$HQR2^RE0r7U9<^b*ytYNdfa-gaDqWn`^%2xL&Q!p@i^vPFGFpAIyUp%7 zRqD@bYq(CPO8hxfV2Bq)jY~o`9y>RpMvN?ePvqp`0=#A5X<%C^M(x2l3Qcq9&C-Hr zt?C^$B7`EI24b?b- z`xM6M&!c84X)5f7HRZ}}#tJCm3eJY*JIn@jSe3G%iUKx0SOuCvli+I)zDaGT!)s6} z!d}n+p)2X)|Mt!s4_c>Qa5J%EmYRDTlHzat27b>r(O<%~kwAzl0m@HlWQ7Qhw_ z)Z&xD0;P?wZ{X*YA}L1@mg(#m2rI`(uyX-+;NS((yP+cNog>&-eBi@oRj|{70O@U0eH+d~ zZ4JqCD|O$M*7YIuK7o4w*jIYCp!O((dYxS=D%K*yPM(17fo5&_+s9tBaVHF*&q?+O zeM;`i*zeD^_-sAdW}?enQ_E#gLAZiY#A6y00P=nAo|qrQDGW1zf=b z1y~@H3M_JKw%&>~UxnRQ;lL*7W4(KxK9&vClqempR(V&YQyECrgEOHAFSD-6C{pqov*EY6*g!)pBz_VT$$#IUe~ z&{MhC?klm&iyXei4*7ej^04!)Z)vKv`5glt6tWw1yUQ7!O18k3HcjJK8g%S_ewLf>5W*TlA^`i z1~D3pO}-;dZb*~UVqyX@!~R7^=Mr;6LN)1cpPqvu-)@piO~v^I>;@Q%yu{R$<4Cc0 zn^I9g+3~daOu$$sj(@Qo`&Iw{GWy|jN5e+rqlk?s9~RiaFASerE>8EAq|1vkeTy^Y z@6F}dm}_m`dr53mKBB`04w7gqDn|bFV?9RFLm0^-jNBtIVhQ@|5oWUFhODTVdGD+q zGuB5hg_(4Y8TotJD}76|Ne$}O?>QumR5*a2=0x%nt^zI}KBxA?H3Q3MbaGa0t)cT{ zwe-7+eoOefwLXUL>G(*z_`GiVy$YX3S#L#YFT}r8LsS;t7QnBY<6^&X((*8hakr9fH72J)566*j_rUEow7_)GLVxH7JXvC0rqtFz7M_o^C)4GB;@|EGf{vdEdrnmesbU^7w}OeKVZWD zM%k5FSX>+=?aVK7lU4-Vkaoidy$ zI{{kW4Z!8>&mY6(=blqqT8bQp+rakiR||5l1mvCqdl^l_VG&U5YD3IP7%0wiB-{Gf7pGvD z-@|R&Qjl#KKLqMn3=`dWoapi#*)YyLj1IQTf_oH7{o6QX5Oi~$?^!*|X~1>y+@}j( z__JonJeVs;oxe5JXnbXj8;@9*9I$Y;;t;{748@;fT^KO%25$M+CiP%@~Qd zH%_9$F+)S#R}TddhmkRK$m`UQhdEko^{;vQ-Pg;v3#>eXSSe)14>Nx4 z!}-k(8evSIJPhhzCpB(HKiOMO3d&q;DlKraHQ4l0XqqyuNYEOP-z_jbWg3gwFBIes z_R_DAGn^;=n;f0^t+qN|GmvOQ12FN%CI>EW0S}$T9*Zu*9#&+J4OEQ1J_P;X<>bB* zEbE5a_2juweitE{i{C=e)=4k_`^svmjVf7hUT3X|9 zTW3=#9kqV83KI{~+vZc4_@u~DCXWNHz+W2+^AXRF1njoSAQp=}`x9{<&k@wkVGQnE zxqEcpBX80F^97BM@<#QwMKIqc&V1ohRJ_%*sCb&9Zqwq6NAlmTpX>QguKqgn)JXmt!#uU~KIFdxcSoP6#)ZvOI{q8uJoWTF zk@!lCj<2`}qRdnAVe?dU{!9LQ82>H1@1L2VUXzgj-l+nLTCTre`kg`@q%41j=pQo z#lP#-cWK{M3aUvH;k)9xUUI=NGNR<55oLo!-?g7F?lb!5QC~ECG+djZ^4*8h7?}Ui z$rgyaC&x)ffclSR`YTiCod0e0%ro*pJoO?FPrVk1hwK(HXOP;@%=ik1ehW80INymh zI((}NY5tZ<5t%0fLkDyi+Kty|We)VUaCEvdh@&9wTchJhx_1PQ+QV@4jSQ9MGrwhK zK1UYd>Y>3Axcbm@42D|F$LU{vuAn-nm@W7L4z+M4UrCd-8@F3sGTC*6;oEOn&+J$o zJv0KjR{=%U;xKe!MW(FB+>}9vm3incvB9y=KwJ7jXK}pWDfY(`r;WBh z-X`J$xlRp5?vKwn7TO=588_?8wBKZ#1 zPjEh1wc0F~)cUhN10q4kv}{v#ymYV>H$H{~xEEpZbXq+FAPMGCwet5(Aik5Wo?#LO zXzFvG1z`};=D=Tw)FmSIJ#FQ>;r8XUZ~-rYfG-9jZ>l09KQ@or z2ce!u#?+UoPnef{soX)GpAmC5+;F<-6TRW|{Yo;N3Y%#P7wf*6=&QyJnIzmZ`sJ6a z$byJN!5Dsz8xcJ?#1ii^Qj?)rHB6;TDXkxO- zIg?C4KgF(mRzEITuc)Jr#(nz;dox^vl#c zv>(bhkQ*WPLv$g&5PR_NA5#yKQIX>v7=$WB<75|4#R;66HsUsXjo_3G@1MgMyDSZb zk;sF92W`~-g;HQ6IZ}NHoLwmWl;0dUNuik>VIKVUoqybJe{w|pCk%_)$1-oC)A7^c zG9E5@V8Xe$GjGc1h4q}J!n&zzeHU(#E*|+rZ=a|Aw+-YoIX&3VX+0F+0za?1U#e`Q z9!~#iXk>D$Khk+;OzLgI16X&Oc$}0Zan8!57y{FR_bCpxcb|#kgf{Im)6p!Daa!!o z`G*G-E#U>l2s;$Jy#ceE20q{blMYTuErS&)aMIxqmE_r-J8ZNqb||1+xy=YS@N`TS z06lFp0x%{-!rGt$UK@?4XDyCA#-O{s z%4huRT7;9}8#7O;ema_hd$2FTU5S4qo>yVtA2;6M|H(Nzy3og-M7VW^cwVXr@18T! z0f;^}c@f;*ZpR9$@kOBnOYDWqrk&{s;7CXCG6DWc4%{uYV?9m;zukc)pAUhbq}sO% z@ZTeN069R$zo8wgwBU<{4lMb@5O_C%8xlBpOHQyehi(dXW2qW4eG$ zq+X{9$hYy`kE*0qZ<61B`)yIuDF$8Kw@t!BdcJjde5x*8D^q=2l+;l!DRru*#Xob5 zw0Jxxb5r(!+OsZK9L0-Be#Yg89OrM%q{B0FgP?00)@E2N$_Cp|(R@^ly7_uGY5-)nN_YlYt%-BOmL61|UaSUevu7aVR zV?orrS0*_+!J`eVy)}{2$JT|2N&tvAMOn6 z8+SH5SVMHD$SH{aBf%<^Ed>TfOMFLlo|>G{@(=PC3YB~kcu6Fo;(?#^)}gI6E-kKw7Rt$Z`RBE!DY7Kt7M(U}rB6Z~eLKFc3cZe{4ojl)-`lbB8d3Rg?bwZ4QS{kNgv?9i zkbZ)6C346$1X+~GA^$`jt`v|v2?;d}7dWG))w}Fm+@UKj4?ck^Cb3$2oIwqKWj}L>mR1>|Uyq zMTj4EEmefvG_s->pdz`?=SALP>xNZTDl_x_nP~M@^T+@$G1anP|1A#j#Dm`&;-dz? zCBoq35halNuMNSsBwa2+LIox4iI+Z8zeb5_1C;68I-tt0Q+r5koh|{!EeR8A)edLly*0YH^ti)Pw^|-SHZ2+0(7T%Hz5oj zL$0SHKgF;`?TvK3g|3_F`gyu;q3ajudIMd*NY`z2jl=|cjkgj1t_01ZnJ&z1&KfJ_ zZYWVm&QeUdK(B~S;m@OZDarYY7kgP6==B#lY6>XQ93GDdv|?{5a>n=oPpp#-YKX|K zlmlvr$ncZPT4$b8DSui~Fn`)(34uIPN|J;ImZWUnmcSE3DDAKDs}!YuJBA3tJ}j~=e__}E3TDKjT8T0|2`@kAS|_*IsmA&hfF6tt3GHuHx$f_yOS4JRH< zPTzB6E4W1B0tj zEj}+!@xeJuo3Y%swcR+v5}3MD)O{7pcFG0GLOD?P`v{!ke24h&Md)53EWnj@sE$+{ z4(>*;JDhoRrVbmMR62m+_Sg*k3q0v?yIfvFanFkWAy@O@ zn*}2qrBn7P;*>p|hr#exJQSqWbDaqMSt&vVO_84}!+jwJ$jPKUq>3SMHxHhZZ3^7Y zW9MX>19wyC91N(Vpa{TlYF&rlRdyWSqx2r$ad0m{AD|T2$ ztoJV6zsI_@{#6?;*~9nB<0xKbu$iW#7LX@W^Hb@RpUgimn!d-)A;wHC{6FfaQ8XZY zEYr(q8qhBd!MB%~%R>=FhyDOW@qjaS82+xgd#t6AJn?8_A6x21&0-9ts>xO~^OpcV z1<2S0;;lS*fQ&snkqk?n$0=`_MXzAsQi5B1$AvV!(c4lOp27^Be2$Xd_xa^Ky|1)s zInNt+^4lZ6OZ{zlAn4p8UB3E03SQ9Wv#~3V45>m2)4m+8sd0719e-{4oGJb^9hkz4 zOUO&1lmkV4Qvk|*S^%A51^HZ3R>NO361&hiTp*&(bQi@@;*8Bo`wm!f%!qbZN zJ6Gt^dc%6Hc5dY6Vl9k;e3r`}x}b7{dn`Kd;#+Goo?R_0F2&=h?4`T`RWrGN_k$so zFap;ZczuLlfAa(4Qy2fl`<3iu|Ei?Zs>F)^`Ze*nbP ze*y7J$x{h^9hr!}m3ORg&*2yZT&9uyM86l2VgARr4~&RH1IQ4;S>g2u%TOo;^En3d zJBBfA7S8~VFL>Zc`KeB$NEgn<16mT`o1^Costg7;yO$gaQ}6*lul_!5VYx#K^Lh#i z8}^Uyzn>j-|2@;j_uu!uA7%gj&B4(A`zxOuA8r4=E&A*2&rG~>)cyCCkZ)!NN85)W6U1P_&_5GodC&o$liH;?!=h?BUY+-1Z{&h2J zm|A!oi|EwCeHvm{{b^Lh8b?9wHUFg{cG8J|0m$7L1i1zu87|@?fXdKtG0k&vTe!pjWs_(&NS555t&M z`iO5gCdInx4Kf$3N$4SHYA&Q{#Ns!s(CNl}yk#Y&p#jSssifCSN0(DXlHF_utT$a4 zux0ues@UWCfIcRh_3cRpG=byz*lXy9M}a|IEcuD?JjgDFU;8U`Li$*I!G;-8ve6Z~ zSo1vkHa2}aItxy_1^GCE`Dt!Pl^H|xVx<3kHt9tZI<-L$=WUs2P$)*Qznv_iKo)yR zx28Dr4jyMdmB*P+!&vbYj2_kQOw`UPB0cHmsF`PD>S~O+R#*a>JX$B(&KW5=ImVk2 zTD?g*js!%R%?iJIEQT*dy*hng{(|+Y4qlAJn#a*^O{1q>#U&N=jV;n)V)6 zbc5C4?9nY$coeY5facJ7uOEJpOeP@jY33(jI?A>!B~u+fFIsw7!pNGqIJoTFB1bZ@ zFDJRMnlLMjWTu*wb|ib&nHsgGPv(&OnsjbCZxweveI3Z}tTlRxr=f(>j^<0s9f^J* zdwd|*6N^Ay;)YSw>#GW)_u zBX=|k-(4#0=HnroG`pTRovgIm&3s$uu+WQRfs4a zzLcW16*um~h!5v*on9iDWX!dE?c@HYp3a^c4XKSfGx zOItc3y$tRs{{{Qa^(fZw7(p($(7?4*Rsme);Z=Eot3>!(y!PL5lnK0E8WOfCpBK02 z#j_3(_p<|^`DnXdHz$j@c{n|7_zy8XT{lRMotj7#cB1nMp96j*!!n>-+^t1{aApta zm@i=5#62NRlKs^&!ZN}IPCS0?De$#ndC0?{@!)*Bc&||>ivJHy9Z&~UT(y+zLkN_I z>yPL!MSJ2Mhz5#$`{ToLmwaxlKz34Wk)$_zybqTi2I)GVribjD> z(V+WfCwwPMf!w#Y2d6!@1s8Cj)>-8LVeZ<)qo}fc2f9P6O}d3FM2s4=(I{~?3hp)r z*PvlUH;f$!1P3ADX2%+2cG>+BS4YHPLP9JOqZ2bA7;$~9`;9yJ?fA$zt_Gq&!b8mZ zxz&=ih@)HKHK>dtD81)*&aLX|PAA6s=8w%Eow~Pf-8yw2=Y7xF-Eb}#uR?DFNsQx> zBW4%Pz+Uk*>VNMu2{~MIBm|D`k0*t`Egv~t`j(XD$t#-sRTCgq_j%UdY1{t_2~i`T zLy<${rh7?{4R z+JSq`miO4^0GYeUUeF5@Xey+(x0B5$;W1gSjbI zTqf9KS%t}Z2v%b96|6)##&gV#33d(|&s9qa$UE0~u2xE%`<9dRWb$t|{lc>JR&-CK zJNgz|ns%{;sTeUNV8YaQMt5)RA5SAeTVV3LTMv%gYWi0nBv~dlDMpZ=les3h_@MwT z1#TYcFuIIAeV!~>Bs<_kNL_9W z-S5GDj&R!53Y1-K%5a}%OC&2TCl zrBuvnI&quNhF7=cO=0=$Z#)%?S$kOX(&0kr&f zvNJ0JX!9!)qOp|93FDuCW~yZb@`ecPIj2Cl2}@8JJXH?*f& zMpWc$yY7tN6JmY|hEH*$DJ7o3XGE%Q_Puhx6h3hH)ES5>Cg zvC!gyPrL~p*t@tBwz89HEXk{OmE$Y}!-t(PFrXHH+VhXFy5!~#hZaari*1H!&S|eR zzq;`#@mo96eYYccQbXN34;ZT!{%yu@+hz~gm}flf&4W|>+gXZ)vbJ>-w^;9CPY0s3 z^H|5ntGdkpWJkw=^sCt)pz$9nf`$2fsZ}n9Jt7Y4j=JUQ%AAm$G`GjMV{Rv2A*?f2 zK?Of;9zQ>6O-jqJE|0I5*K6|8a{Kv7D@$7LxqPhcbRkDgyp3;Qet^_j26r=tq=k|> z|5Oao{5J#TyaS!KJE90pp;tR_7l{G`oeG%GkKniu{THI6?=-rGf&BG7g#P7mp#ScH z=s&tEdN68v5oNIx^??Z~v)KP{uKktJJ(Q^b=2K5X13}cff`(pOpk_p~=Hbx^DRdoS zvprV@InH@JFWmb#n*1JiP z*FxOs+PWnKu=&Y(DZ4&$nfml&khV&L0dZQtqWX;`?8!QjJ+ExeWemr=R}lgaZaonL z4`W5FzDk=ez}+kFnkc~0)%fa=t8$D!PRr_RAO`rs=anr@{ExJJe#~ADvZIbg+IWAN zk=FIR&v+Mg^-c=^5RONic~4=dnP;c|v~w?0m=~W01Scn-W>hU>|1woLyU(EY$LJVG z1o}QeWpShntGzUst-l4YkZ3<)Mi6hwKaC~d5O!lIatRrlXVF051@S<2CHMzQiT zEQH3&8*$OE8@!|!yaS`n%!p@t{nP-oesUz{ZvEy3MCbnTs~O8dZmWI*SsKQKGRa#@ zV$I38&q$ee@=lSjNx-=_;;CH>y}so4=8hF!4`M=#H?kVm+MQ)3tXen!c=sXZ3w+5L zbZd?Tb?A9GsEILAC_r{ljw^~$(`zjCDm}WJqycx31q1J(fQ*HlxTE5@$^afvT$@{62NcK$bok4$`-f;4#hDXg0*eJm zUK;|BC!Z1#&)TVIy^H2{kId|@gu8is04mQqsyU|5= zR0TJ1T(_FLOeNTOZDGe}3p;^m+|8D~e>w9XiM4J^{-bNJlKez|1Cbv{z6IV=nV|1Q zz7wa%I6@H770>ub3d%ehv^?u2^3@7oRKJ03?IHprDWuPH=oWm-B~5jJ{;$erH#aMe zePMVG%JLK2w^&Y~)*J~iJI0Jw)$N+ETr3$cjbT)Mk?jWZ7xpo~X%~+|)JzXl0&xz_ z>f$_Y=VFT~Eq_^<(weux;Le>_COIA9^YfBWgxO<~h}VmYaiYI8N9ZzCh(Rc)V&JKI zM?a6JRtAHOZNPz8Z`yi`dS*sOMb5|hvxV@Ejx6+=$Inek8OP;V9LnADLB`)5iXk!P zYq41D0b>%Z9*~YFmeT6*7{nnGXAo3Qp6jvfX1I+>VMi;+{d@Sn*O*l5ynnlWAA$Z- zrl`Ek+C+DXLV|Y1owh}*$R^qw(qwpO8eGJEX!1yhzuq3GZS5Cy#BCk1;Q>K4i^QLTu z@!G6%YctIGPu>jWIA%Xzf0dU&x*iVE+Gd|RubvKs<)~pigwwZE%c1`O>N2za$t+Yk zHq0d#D9N^nrE-u;EoeREGe6D1Q;Qv2A%`D3lC2)&QD8D;kAq(~!R&R>;&m&}`3NdZ zJ~PTzV9AOT=`hnLjCJWdS*x(&tFLL%c=v-mi1SWm&?0wUWAt01?%|So>m-NC`r#F_ zrQO7sWX)ly>eZ6+sU-!nQ?C{j&57|aFDLAldd3G<0@wO`q$D1Q^(&>seu(B>H}}V^ znS00F^Aa&Bm=VwxzSzAxiJpgOK3V!{ytTr7VsuIlw)1?8gRNc3-t`FWW$*G9waXz8 zc%fh}Wzv!pVds48($ao&+_TvfL@ph4>OQ6`h{u(ubh2`NrOo?U#8^|xv|vh0b;R`+w1I?B@XDnZ^>LuXSBz9K`ddPv$%i%s^2OayXM7@W=0H0|*#nNv! zSkBex1*gU%$AFgaq#==xO3&Av2ueV=zcxGfZpp!Ei>h`Ai?MtO(JjIW2)95Lh z9cLFLu!^e3P2(o8EK4(Au00fECmQi5Ex33nf70hY;!j%rLJEJf7!8p~3x03AhCVfQz5sg$7UL)70m8(K^w8Id$8V{>#-{pk7?37(&Mp+E??B4t9jr z_ODo5?DVh5i|19eJc}iF+?4%uV?Cj^Qjf|>{LSS4*t9krM5;`0;Qt6%2Vt*6Wy9$q zc2?U=+t)lW3ZX^)wNIc`p?jXLxeE^aYa6K;TIBNA*3w1Un;bWAWiYFO_7}%2BCMc6 zz0W_uPlzgLcAm-LX2gzW7o6Tss4mT4`?!nWA84H6hn31t(BJF1rKbD%4U@MO5L&2Q zS3R)TTRSRJiMeGgyM^Q+icrTi1d6)U zil^+`0ToSyZ<}7c!_Ak)+uRakHL&udg42~}m6>H@m^|^P!4gJr=bJas2Oyk(wR~@` zUI)#lq`PWt!u7HdbE!e`2+J>Q^FtY~u$EiQSUSu6>>5AVp+I<^uw4_!jY98OM@8@@ zGmpDC4@eFMXQbKlEysV&=1}-W9Nz<}wtsi71bMohN8E=IU&P-0kV1Ztjv1?*AZ!a= z1wvVsKKAGTxP=Atxt7Lh{e>XDT%YOE=dnxNY{Ux_ zQ$UA>*om+aHLzsO%t1wTNV&qr>=evDfVXC@S{Q7_TPqzSm?e>=^>m|4-#a9ity(Jb z;Im1G5@{<#N#|mU$6pnI0>m1m=&y)TeKBnjqFdX;REeNX%>kSV(Km5Q^s;%h$@IUi zz&3K*K~qjDs}U4D{-%#v<~xar_Se4el3b*FUAA;HjyCKTTs5?agMYJPgL=YFHwKKq zx})!*m$U@IWrrJY428vo6e^s3qWx1R?S5V%6wBs$#HDGIyoOWS>XPnV|MfQn-^h0b zq=wezB2$F3*z#@ag+4gL#S`$(V>5AB^1r606KtweFAszr6jf~s~{gu2q za^oz%UdPwPN=q`+ZLE{3nJz=1q7^d}%B%ogsOGi&&%uOMX#mA+D^Q1Jf;%KxJ!{^L zzdD0p5FQ@oM{yQL9gxW_VIR4g&d^*wNx|@X%#mu>GvL$_);;6phwnHP8w|sjFZ8d$ zR1Av(Bs%X7^1z=;Rxbbd1hP^bT(S7-jztN}H_lJk^aQ&;zTcNy`+dz(+Ux#W+K8^| z`!TO@8Y|<$zUtaD@l#HY@y5YXKJS!c%{0lBN=NDC)9`%cDO1g&rBI4EPC=Q^PCXNw zk9jmFPopVy@nzr|dLf^0;&O?#C7j!%9xWbAc*Ko0E zwi@S39N}vXtJpv3hhe5DRZUMGijR5;8(adQ>-uN7dHN%Osm-daa*DV0*7~d)neKAZOX;+Kj*-E6<^41amu;Q{MTn- zCdc_-Iq>?Kwy7jPpqZ+(r>Cih+ zsmCEt5>!)S)2<7wxi+9j{7sL6WK8`pl~D3N8ZYszgtEMnP|HfKz;h4%e_)A93%9CF zuj5 zk-76!66SL{LoZ7U>o-)y_Gtz`(ol!ZWFsjk_R?gG;as5#QnyLF581&u1%C~7MPBig#n}-)@zHa5HgSsRkJ!Xx!|6^s ziFXQjGrPNe@RG(BiFj823oPxtF(iNL*2&yuK@*-wT1tOy2Uv=Z2CjMF96{mz&oUQ` z?n$aE4*FJ}BdEL|>;V*`A5=QLprB^5ui1d5u5GW-&fd|`eMLGWZ$lCwso-rP1tf=T zH9qR~Kj#A##7<&>E{DQrA30>iW+%MAF}P4NXpW)u*Vd{%%DO|aT234y;XISX-gR=i zoSmB;7$IUNMi+8yU*W8tURp8hTEByZGr9G62&R zIQjNCB*Z5YeqiH}`~V(e{pT-J^8=3`Wq#nBrNR%~_HRS-1J|a$-TsH!!{G-6-ro%g z?*awyzSdOwT1S5REzb>o{y+8Y_6%42bP`_(HQ^0pX3Ri61h^gADR%s?hb1YYoY5=XOq8Ev13C5Jn) z4pX%Xs)+3)F(H%WStirY4A9#V&s{6UL(nl#Wu?eS>9i7BUA@XB$s04RMXDIlmQF{)d*7^Lty(H$1^zTRo=ij z-4Vkv?OuEaGso2@3g?lYb?v?vIJSN^}i`8ev zfIg?HKDobU?r(X@{x&-LYnJ{7_V)h1FK8b4c2XSB-k)Vqq=+MRs>s|5)5% zT-NvbmkhPOH!1wJ;*kOCJ0J1KXL4d*-p&nem>w+S8=o3-l?PMbZugW9y~=^9E*jiE z#!%D&hT+=BQnyX1$XwC_Mdo@-?1gs8TmK zy??dbe1dLj>sN6{+2)p`87^%>y(&G0QnPjM7bv-Y!y@viBN;Fg4LF{eRia$h>Q1Lq zgDS5qFHxUniu&~6=yUcpwDK$tHkVfpZMn-|XSho3D*u$WC?M?c}9MY%NS#Ussk z4zdX>SiU%J1X~Cr*ehR-;p9Zc!H~TKig*cF@oD~#xOV}Js=C^SXTl5&I$;I~5H)Dj zQKO_b7;J-yHei6LQ70rMu|}Xat<#iR)QO@-NSw@&;V=O$;teCTR%uHuUrQAgAps=e z9#QKYNGMv=69Xw0#Hi@`*R%FMbIAqi_rCxC+mbnFpMAfqz4qE`J*)S!-a0J0tQQZo zto2=3){FaD*477jS$9`pS*I=IW!;SBIY3Jt{=b?|(?h-P0-zG{IE+x-f>*|dA&yID zkY4Q$KYb`cpmk1q=(;m2v-U(nEC`2(TBLop6b?jsAwtyz8Nx=XC)U-1&9fi6DB7F0gwH38Ptz4BPs~ zm>Z9+I<8AQVB@QETI8$l#n;8LUz_Ani@tuqUzf&yT|Xz&__~F^E@R1yK>`iaiSui& z_2NO<;~gdiu3F}j6MC0bJN1>W8~cuv=%*kRw^V2&wO&l5uxFkqR+7JI{Zo_t&MNEr zF#W)w^_}1FrnNq6W&C|dP5gVOvgkx?|J63>>UDND?!ZtC1UPFLm5*e8P;l1cvGji6vnNXnJ>u0sX~$85}s`ln*6r# zYy-E;#yV9(#n3+BHF~NdtV2@=R8j4FjgA`zL!r*CHia5mM?pXEwAbjY1{=9Xw?(ee z?fliLeKC8O=`{8SLU}KM4mv0`jqGgUCwnQgNj}K_boJYxt^xWpeW3n~>BWdyyN2`+Iray5AkSBep)!bz@(Vjo1SJi`CjACjwhP7?27RJY|e{C z`~$ps6O2XeO{*O0*_xl>6nw>=_XnG>GYdOqwi=!Ix0w6j&&Sr^`|!5FKQ2YgzF5uGNEiq~!C?&pL(&{7 zM2L8Di|ei=2;Z9i6O_Yo>yg2xUISd>=~+&j$7kA4{U?Bw!EbxC_v+;lhUI0Y%}j;w z&kEQtr-Rfz#DX*?U2d=-Q`Nw+D{yCwziI#t-Uq_|*xy8r_D+UX;TAv(hSxZQ-x@FH%yGm>}-*Qof$NP6;aun+8d~69dXfw+rF& zvWWOqTr<$7Ws8*`)NFih6sk0}X5)H(xVC2F5tQSdz;<26(gFVBbDMbN#`M~HhY0Gi zg$Re(RwMuUj^v0M*32;rjR^p^Byml!1|4;aq`3JAi<{ZfqAcInlnj8@Am%Hzid z@x88#l3Ua=0gO<_@RVv9BU7qn3`(h%K@%iPErTLRj#`$=>T-pK%=(B)R2Wmm*Oa3s z&t&ReW?@oaX7g>$*%H&2J#lG<$XZRaRGLt!JurHqx=gd9D$|^($+RR?WLh%nF)anv zn3hVlUg zcd>Ba?jl;xpLA%92;=*Duy?S4@2(sSdrZZFq3ux~O&ugP%arA1mqDi^qjnoV@HFtA z3x$p>p>^X;6dpi4P)5UaU2BfP1EK*n2{;Eh-N^2ZTA%hN~zjis6M7$irWYVEqdB?8S5l_<~>PA zoS8(_df_*Eo_YoT$+X|iKc*{nf#T7u#t_Lr^4*8;bd(P+<2Xu#a}3AKetZ6q;9?F# zPuDi1TRQ!}k)0k%e?6r?Ao7RyNyR_E)?>?w)Th))FoF`O0@h6F(cb-Jgeh3JF7_7M!XGwQ>qh%;RNN5K)TrA6y;|4JNUf!=2? z0q7dd-(_mQS{bj?v~K>y60G~7-6ggWWC7%Y^zR7_8&7U}rpTgD?N{OKflv4Z8_k6X zShU9_|7DY#pu@k7+ME@5_flN6yl#rTBZXv9vfBiyUZ6~BTV*dQvC zZ@&E7t1OxOy3QNO4a(DBe5qV#aHrEf^^{t>{LWz}xukmd2_b}a)N%oSz+BHXL1 zJY43IkGY+LT4n{gwzu;bwA8K~BUp--+G8n#ZI9QdV}6JZ$ljadlzEQK7pPjt=y(2k zai7v3q)!)QTQlf)!I%hZ2~MPNI<5lVU;7EJ0x&C!X7B`<@+$ZWuPd|>f|}ViOul=Ec@CpHC}Aomoa0K# zet84f&F?91`81O^84x{o`cswSc^66To~`(?de+4zjM(K!Q>E~WFI+BBsG8Kky33=E z7Aw_8ys|NM6vTp!VghUMp>h4?Y%>#BeR;giOki#F^QGb~{30jesBAM6n*|&p1@JTr z;^0UPg);qeuAlYrU$1hijY!UBtjpJ2E_Ldcb74CSWQ%o?Z0j4*b3yJTwQ=R;>|VXM z!w)`SFhg4^#-aK~D8@qFYrFSHF_zKASNomSS7uww=y$=ToJ?Zx)U5vmQ_eeFsc*wZ zbc>~Gu(F;{CA#+~N&wSP8KX?kaeGql25o^h9<~6CNlu|BNe0%?#$)nkD7?wgo=|;B zbTjbt&hJ>(1Ud`Dw^WZsBx{SmgTlSZ3}e&$QtP-MyZA!vib3kOMGL_h(5eCZyJ!jW zJo)y?X0z9pWj!_6@AM}6Ha#^K)6a6%fv$wVnJM1+M$jnW(|~ZG)Vii^d9INruN6OL zT2v{@mC9bps?%877EPm;IVSa);2Tr6M6k04N^R_C0mKX1;9%<7Ll zxe)tRLKT*TtQ8-8ENjhO30E~i(mgrLlPeOpsS6KZ!i0EoF5{C-kl_dN_;5KncmJ#4 zJ|w^i3w9`03ODV*!VM;wcK@pdqx;0NaI7}~20ih4e4lSqt>Il6&aT0#HKUdWaI{nc zPce!=aknTw8vhuDbS0?Wed8XAk84r2{^PaDc&Zj{=7uv&yZQ{{EtBySqf4+F>h6|f zxde$ZaI_lc8iCZonMbqVdSnRiYhA-=I^oXe>)Tm?$-^|kGh5~xp&7!>Y*Y9pN!5Ut zw=kZnJs+WJDe+WoA=8xSB~@G4o2o5b8~uE#cniP8Nft(^T5J}eY76lc?bc#h-I`5X znPiN^r&Q01+a((Q7$E_gF=IyM5YJ+S(o);DkR#V@dMh@~+aP&mVr!4b2x6Su|Y*;T?S6tcYVZLyiqbUZ)4ko9qCmGdi2(R^3L zrA148O>W;?=4)}{gNF>qVmoW!-T^NSmIeplu;XACWm6T_VYdcR(o-~thToX=l+_BRJZ$RbYADXk3H{v=zmgF zcqW{^-V)hN7xHFWmk$>>PqN?><_goDq37}92F|Cysj3R0^rE%aevA8=pOtvFq7NS| zjnr$;?tWSD69;6ys>Z|jcjrI9<>I+osFXNP-hq3U;6ij_a3Pq4SvZ7dzluli_p`Zo zy_YFtg8U?uGZM3k#ybbKisc$yaw^4Gw0F$Bos-M?HKRt%uj&2Fuj^vw*Htm|>(ZF{ zb#Bc38WuCZ5)xwO*XMob*PFZg9jJfpkIb*8nEACXW`3=XnO`en=9lkVT}-hm>qZBq zn&Im!+OoM@Qm1*cEzsmoM|u3h>Yl|o&7f8qW%A$%(R&c;%1s7TCa+o z{m!&S>i=&uE$%!Sz3=d>{k1S?`Rk5;domp68c(DXebvzQhAbtsNpi7}hrst(zV7>S z*aKI(#fT0r79D|ne=PDnOPD7h8LMl=1QBIoT_A1fU%P-cP6D}j{`)Sfz_>j>38UOpICRR?78IbWS>VE~eu7ImtP!AEA=_*T$vx^^@5nj`c) z)$?^cj%%Yy?cyrKu*j&gcKRx3_N}thsB)5Um?X>)V=K(iSh7_DOv26;wip$*MJjBk z3M<=Wm1D;vpReFDMnm7c)H}FEC^H4pIUxETmD+_i^0VO59jX>?OgqQ83dJ4xZR;Ve zb<~X=j9A)PXPv~Wf7oC_`fv|51F88=F@p}y<>Fp(g7ANl#R7NSy5zIW{g4FA$l@m; z9v6gfw!r+hz?IKekM8izHokW(ro zRHPNc6lE_Rm}Z+Y4`AWXAHNhIc8L!-Xu3}3&T)#97NXOUXp32I%MMq+--VObY;%Yk zUj$3zd|VnWeu++7d$bv^YPvuWt7{ThuR`w~5Gt=p^YdJQi}VBRBv@vkH8Sh0w1syz ze>$f2L%#Xl(fk2JJADv^cCrfyvVsV&JrD#L;6+lp&Sx*jd0_zy4RrDe+>7}@XF1rc z2w)wjuP32u1V?qYC%UC!cGqw^2UneC;Ot6|$xbzJFR>dIAu5aY<~qy*X~W^m zg=Z4>Kzu};2rOLd6qh!sfWc;5fajx1v?tDw~eE{068(HXt zCb8DV_51)T7D+glsS$)4rR~h7iXm(|y=H)nWq^uDGy~G5-hdW9Pa;WupKsK)nd|tX z=1!cbVKbLT2kfSUpQ4o#Ib5KRrMQZ;J3BwtJx)0mqqg=M#uJx}r?E)cH^O8jV#zh7 zEYY6YHP}=n1lfsmKh>2UKo$HA_X!VI0E*dB_o7&8Ze<9qpQWsQaWe*tfp-ekCOTP! zZ#ETGno16CR`yb01@#3H5utc7f$5IX?D9ynXLGYF^nIRFa!;MPg{yrHrcl0xxDfC6 zdmUt}QsN;A3fX&c32XmEmyZ@b+QJnXh;c_)P+#oG9!-|ly&2^R>mX5P*BxS7$;Bt= zcGYDuY2P*Wf0>reZ-{HT!P0eMOtdB$P2p(XpI{_z3L(;V*$(5wADwsgmOk$DR=I}1{t?*u2 z_stR!w3@=JsolHPU7XjJgL~B7jCUe>B8i(wMba-bOZ1y06^hch&u}Lnvt=^Gn%p-< z=EbgRG*|+viLmAfw1|8s8n-|tYi;_9G zEGM?3-6RCOYBzW)xRCU1tfTW@f}`#bu8PLue$H#pY7_@W@Ge=6SQu3lhIQengZZ-> zKldm$)7|TS;8=QkGMll-6~D8d%M<{tyDRRY{oqA7J|D zrb$xHec7HkcTR?Le_$;|*7j~6VY&o+Pmtf0R^Ooov^Q4MI;?K~a)|E(bFkrs6fnd2 z4_xI8C%PZL+NquU9@F^h+6n+kK(@a|x>&#?)QBagc6W_Xm*Xke_yYf3%N>Odq|-pL z=)~zl$xUrUUuxyKQdupd7gNbj)pCSWwOlnfX;nV8NEO(2QBYWTA%&NW3GDp>C~`4v z4dK-{zTFn(hhd2jU1InqJ3Ug4d;sh`IW63Po)Gg@^B3`e4HHAaMx2rOv2|RP{0GV^S0-*3 zhni|_YQM=(2GGl9?^!$qhd4)WopK!=AgWmbzfd#+F56U1d0%;aX@byS0Hmjo-#vMX zPN;C|mLyDL<)og)(49QVX-FCm$ISB>`aeTC;yc{rJK~j!M@yk>RI2ZjxS?LIjpAx#jMN|VKxw6Fs9Z5sSAZ*xXfHXHn{6>Zg5V*jS(u#= zbw$kFK@kO#%h|nTp-vGOe@8V-v z%y4VP&J5u>SMEP%@7Zd%!ARWFu^-5H+iCvfGsY+Q{j3h+HeY&&mmI$XeNxnp4)pEY zJ1~VmR3mz!qWUVxb1_QvP>IH-X$yaVXp)V|@KJ8G|Jj)z4;Gx+U4pfZg%5s<-BZuRCB9q28%FW;B4jBn2AH{$(Gt5ba?|=mVJ3RcTp)&o6~8Z&u(We%V}wE5!Uz zp@g4E*`f?C4K=)rpwRM_BcPJD^eG7;S7>a|NDHkHm&5gT8p&HC|wT)u9izhY7p}E`= zmChsW9MHm?c>NW73?s${ElH}mAKReUU;`GOZ3_inpjkgtm7XSqh~1{<_V}7+`#R0x zEo#|QYVL~gX0>dEn!7UGtd^}*b6-IC3u^9ags)a}*C2e2n!6U^Yt`I!2w$h>u1ENK zHFpETH>kNy2yaqzw<3J2n%j!-RyB7Q!gr~;Z3u5ubKgbyyJ~J2;bAqm9pUY2?jeL9 zQgaU@{IHst2tVQQPXIr&UpTISIy3c8lI)+^Fy-6_2}>$> zruO#IkJ${Qs#?2L`B<;1cBKgZ)h_r~r{G_cLJc^s)8UX-Ldo@m%dJERVJ4+VC0h?a}>!1@QV`sP)^i8 z%pVN@becI@$=LP8{y6`>?-Sj(Q5f>nJuZ)Nw)O%KPc36Yj6MyOVI&X!{QjLM(UuvXsX|w*)FEZE9MJi|CibPPN zG}pFh&Yl`0OS@+8zilIBma6n4T_9Y07gm})bHPjFB4w}k@1w(byxj?M@C3({ri^a4 z!Z?cuNX!)8U-KgFQgXu(un_%Z3yCDgdUrDXc?w8}ot?WthLf8tPsHroVh4Oi;-s9L zB7@=Rf@pDWl911Y^HY_{v&FRrXYC9NEudUW1kXR$W`OzgQM}Q9sY!Y{c8VvL+L%O7 zp@4P?4iA{eZrWC@E8u&8HdgCOexM!X1%9xr*46yrRIO|HAxX8aw11j z73ODZU@0c8+PLCUoa}`enle2_-v^SIwIw<~UA?YY9sY75{Qh9E*rRX8{d_=(VsQsLsd5$&op6=}2H7-3ST!pF6*sR9$;d5^$otGN^#(#0^v(mq}D zT~KYa^)`=5rmUkh+N-^jh)K2ab(&O-^_L2T=3`=UDQo(j(~oo9#WL<85qH*_ekbeC zIqp*QD*R5Xjms`&dLuPSF=^X41F_{ZgKOGk9!vHcKn#L5Ls4hD@O*5RGCaZ>xqws3#l+I2q!r5txbatAAiMu15 zkL$!-VOJxCUCG$S`5KkyxFzD7f@g13Tx!hT6ehP~3M)IKlUO^inkclGUqC!968@~< ztr;^TVlX(w(==Bo-m#NZSH#{U)?PC6|H58!X6M9Mj49a5MF{O5*oK=G{vG}kxE091 zN8Acj{jt|b=iY<6vg-AcoLc7yr}RP`2&2$&0E(Jw#A_tKX~rGlqUDmeii6!szsJyg zls03;oKdo>IfmufPo-hqwzuf0+8ySvf$96Z*p8A@k~^7r+$TKy-z_mwU+c6w#wdTB(O{Z>_RoO*iRBSB~Nq-<+G{VteD zgduIWrVB$qbJfLB@a_xUCv?eSd+o?KMKEFelsCCNUHPJF>8zRYh(6-JzLd7x7G>$y zLGkQdh7`ErSfQR%$-Mk+%*)?q!{WHj4*BP8H2IU%+mf&lW)! zbNv+DEHoHq%~a*p1?G{scz|nV)^3n~zf^H|!H29QwzMXRzN?p&t+*hJ%=D*O zJ`Tnvgj@M?m`<~JFC^47CPYMh>DsBwAzxM&GpxCK&VSXzFwUl4#=#LLaK>OsJn?g8 z#%5N#74@b@tU4jb1UkI-QhHj|B!-1g6lr3&$2YwVM2U~3gSaVR(w$J4Ktteog?)hv zA42K!awmhUuqPaVh}PKF1BX)NjSzKp9XFMhbBa{pMHdU#mSy7py37_mAE+#R4g)VJ z)X+4-6xrbblf=Jr& z7h?D2nOis%^ltwPv7~wC7NLC=@o$`wOPfyMZzD`>hG25?%*b;qD)q%|`sOj2a!rM= zEq?7Dr?zR)=enY0p7bA`pJVu=$Xl07xnHWxv-oxI1g!KL=$6EM8rRVWO;JQW>RIR#LHwtafHuJkA? z*cKJO3R_XX-P)>Ey$aZR7Ybg#>TUl#?x94Kr2kSI@9n`ndq^ewVSimDZL7#_zt@ zqV2uugx<@78S~wfdasyb&p+R*YfaVY+86&EGsT2m5o@%t<4P#;!2UH^+<5XPZlF-? zog9lvF=;BMWUn2AJ;4{rAy)Q!FZax$%4nlRO@_%W!jp1qhO_HRCgt+wLQgRw9%=07 z^ye1lWwZ|G$&8 zMYVd|JB_17X=W+mTuW*5$JW|yiyx*@_=@F$$A7P6gz8!LJRQ_eX;lJ{Jx^!wh!oW{ zs~6kT#{;kLNFK4^>{SLp5hbO>@8TsN@~Pc09rxqDHRwMK_RSCZHkYg%3st)^sQfBR zq*CHT&+*?c3L%Z-mA5Z^;t(ZU#D7ojyXfuuC-5Otmlw+3#F}gG5Io{az+i=vX|-qX z^;#xQtU6byBTft*rk`eGZqGs-NYL&+*mdcJ(=2PD=CA=e#lBaTke8qD4F<7PTRJi?Ci;O~1dz z==))g+#enuxVwo02h#iSK+)nr-L#e4tc=!;*72k~#TNy;rMMs>0gSe+Af{#Rv3zcF zs!tS)r=(6ugJ>Ul>gjGhD#%o$UJU&O#_e#D>MxJp4nOQspDQ`6RGm1fY60UOcXy8#Ejy8*Scox1+DPQ&+$f^I###p}%Wzd(z_%jP0a z+)6yiKyA9on0J#;Gw&uC^X{bL7rK7OAUh1f4&*kt#J*?(UA$du!d6TWGt1gB;u(;*l z&HUa7%IHNO>-`R)$6vN-$ohq&I$Iq-S|?-jOTF>3TN~N%v2Nt~J)@#7gTkH1(Y1s~ z^y1}Qv9SZO8J(M*(6J?N>vhz z|3m}+psQ+>aVNC2PAwBNkqsCXe=@bUDDwcW*uH_4 znLo)64UW%H#ymTez(2<0t@%o+rEt1u*}Vn`%w1WZ>MF}M ziFq8YqzPByU=GkjxW~;Y6vA9j&Uq7^Y?Vum^KCH`-!?LPp~cRcq})pXxw@_wdFmp~ ztN$Tm+}uv52?J&}R$d4QlrNQqf@rWe$XD@P8NHj%uaviYu&)uf)60a`OtFlS6OEB4 zV;mDP@*{>kd!rhcu%cNrNa)?@`wIEgqkVAYm-?y+zhh~Z3_3_bHJg~ic-eeE&2YL^ zY3YP2Yt1IHmV13n===+RJaK71A5EQ)n}mThuXF}S@Lpm-l8UIx9$j@ZKnmKY=e{jOYg-|Q3N*7j|UMKQ4MSvF~<$Fx>7QNEcHKI@0AfnLUeh=*x zO&Urchf*e8KkQXi@xg5BIzPUgO5HoeGt``5>ax<0nVXWq`(NUbrCCnjgDxt_@x5uS_r1d9a;jG7#dB}bHCK-EO1kxJ5mLU+TC?;uhMNwqVuIJXICjDx!`iC7Cb8V zMH;rrXdV4@9>J&tJvb@wt!N;PM54TP?|!Z;VgCU~orYEpqOsm7?>p>s{7%Q> zw<$Q=>csPsx6I-uOZoxDl?L=4l7-%b0HVkh;HSwjwTgAkZS;5hSUB{SnxIVLl^0ka zR2kk8Rwn0E>NIh3?O|F5!qARr_;%@(JPj0_$Q2V=)|gI*^fz3=gxU#71kozgh=S5y zV>j(mCIK!`*3qeqY6G?ERXIpC-tHOK8uczKCQHP5sqhc(jIl3}@eFMxnBlQloOK%I zN5dJo@v-at4(n65*tUz6GM`o2_wrNbI$a_k!cW1BnyvVyb z_InOZ74Hv~B{=R$Ep;3L>CA^y;F9PmFjSW%j(`wJcGKYz!p_^$Q{1m<7f3{4HYtv_qZYH+G`>i7zpiQ{_=wOvgb9S zV1&EAN^$mX)o$Y$%S4Pdh+%l4e6*hT$0CllZ%-mcs}s@uTm)5n1BcENp}(e3HP8f< zi6gd-6qjc%g=8BZ7~@apO@xd!dacb1SW#~?sA}mt; zg;SVL(~jLnidK>05l&$_O*@`7Qmhv#ZsHWS)0FWGBgJZw;v7z4KTR1wG*UbzQhf6F zenv2^468GK1pm${QcqLHI}VwmUZn7GinP;|vC2p>L8K_;6cbKU#;=VOX(Gi1oFeNq zWn62dND(QHu0x8P)0A1*I3mip1Sys?_2Q$2&@txYg-mh1k>Ze`49EY*Egufj@vaCugrH(q*>s!*-+BLQ zxWd8CR=|7Yz2`8w%9|ClN9F;#USGcc9OrZN?*;nzC;At6pT~HAY)j*jDwO6Jp!dJDngu@G>;SA$L z^o7HVqv0~2SsV^$8u9r|qi_l12;OR{Wug^!4&L#;jXezhnYO3_56y?Hw)&?Yp6IN( zb}2a4Pz_$ZXSD=48XxJjub%3LO7@mJSxk750Z?5KYQRIOave0LU4~l!teIORYckjn z74C`k;Osl%W=M6GmwL~q7M4TX%Uu|1fR`67-S&yj?kL0>yZ zjbdw#<-(d{0Yau)vbmL2i8UKd(EV`K-DQTM6x%^mM;sevATpj4;$Aw>p4J(}Qy-d@n_mOJG}2Dl3S zf2pq^;mvEZ%n^9=L;Zcv9pJmvC&PJ^^~o*~n&eeuX*L`>)TtXsOr#e?9j>3E^g-fv z(eBXfos?zsUfWwUoH619^QiJIfu*B_Ua&a2`i2; zJz>O))?XFnYg>aOOrj{H6$K1xmg$y9)A^1#`Qk0brLeV7aaLYnU`LzxG^=F1?CPx$QZxeq zBhFIgOAp_tEws3p5^KLJqnX7)OHR0i1a&C|3H(_pzTQg#p(mlePv>l{cFqZ1S-AC5 zk^VmVrYvc_R3!Zw-k#ibsfbyMw}ovo<_+}bcxB1EGU_w@`s8~uuEMVi{~;q+i^%WG z$iL!z!!oX3iVA9%Mk;tE7j#6XJyeP+9g|7-m2x>JWZH{GIiJh8Nur!DWMr$TRF8~& zlF}Bc3r}6jmSt4XTU;`hlq`82%327=2*~Z{T(V7k*2?hqBy_)!*NyZRS~x+*Ji^`P zmNBp7n8h+?D(BfQYd8Ywp2r?q1oItL8MVnG zCnBRI$(M_mzvMB^yIjP)18)l6%81N{Fb!IDtF94 z?(fRn_s&4;!ZPz(PI@?+^wA9BnWWX#KFa38FeQ6J*hC4Z1{e~C6c zc}&K=qXaEFC8Pd`+rC!D{Gb@MUl*;Albf?X8lZEGrf5Jn2W+5#un{lJ-&J8Ym5fN+ zInvf>s`VVudWG=#O(nln%&X^!tR%iG3lGbAd?SBc(j-UdS$ur*m@)pG)rn}2mvh=R zvVp@y+LLmX&JyJumF2kkl_ACvc@mN?>5=8UQ$#)Wm!!+s6>}1?U#6}>p&Lxfj8jPZ zclxSK;bea-LM}I3m>;2M-&=Oy99NRL>Rp_dN|~$uQDo^U^>MNvlUR(e6vwPiz9tf; zg7|h;G0zz`4Q$LDY?2V;INLxzRV1_|4b3zV>hRw)oUgS*ka_TdUc5$}EfiIM@`OqU zoW4}qTe#D(iOt$l;+aFdN+{o^J@-JAXSjzl*oX}LCq-mdimd~lP0e-E2--9Irp2Od7?DO3zzo+x}EBbs-mETkN`&oUy=lV8rcT}Af?Zlb0wn%;@;=f^h zycJC8L}&H++17meT`<~L?Q)uwy^bfE+$EaF+YR>&Ha7a9NmORMGk=5)v>!*CKa^kk zYC8;<+75-<-g)e_W=rPwLT!tDt~6=u=S7{I(D}1jnP+MK#9G?|oxsrA)#Ief;PKzj zQd(zrp7ozthc1PxRz!_cpT`<(S$IsI>sICauW<@hgm0mWV9ldASo{*FF2nPZXX*K50RB^I9R3Czs13#JT5**8 z^_b7}?3Swgr5L|y?59|KcX@0Fg%8cW-hS=zK7W*WF<9dHzR|rCCFDCZN$wfipVpkx zndxOJ!$7sTy;63+^Py<;+53ImY_l>o6>ciCr>1z%7H#yOPcz|;nIf+uE%(J!x@Rd? z)~yxV^}XxTJ{;QLH$ED*d*$0*&-zB<s)8 za5WV{OS?>OgR7T#1!~_BJ8OVTcJ6twbCsr|V6`hnGf~-+%R9lrZtZ9-RA7@co8XNh zRN*p7fZI#N0(YY&58fw$^KwDh_-uhUAT`%y5m~rAfwK}r@@6(P1{`%sILUf!o)r+s z?ACs8UoWG|zww2oOjIuu?LpP%+B|L~+M}88>xa12wg{l&z1n{+?$$kXIBIZ^_U^s? zns|Oy!z#WW&nAv{y*ISf9Zmo0NF&+LfQW)x(VVw5r`(UYlK$zVK zyvb2egA>Mg7CM({#s_WtBQ)J;y$7$id0J)r@9n24(e_^>+W*g9?eE>BUhTgq+I}BK zRcpJa-=>{h5Z4Dj7^-M5+|%z;*+Fp?1-TUleXxMk4qWj+>y0<6WpDU;OnhWd_BEM( zTb#Zg2j-k(@uF`G_90s>WDk!U@wd|K$?ssV*k)?5Te&_7_mt?a113`+nAH|R zC`#PI6dA~GhWYUtCMXgr{CWN$aK?XQirOnLG#S|9RSP8bslHz*$5q`;hpGORQ<$XfO|{rXZK<^u|qhAj14^Rc_jipug;K>AtUY9a5pzDi+R6xO^FW+a_aj$9E;O-PA{$se!h~4D zB(Pe<0UYsBZUYVqp7kOx?N`!0ys^%OtLT3}EbVM!a&pGzq-reTX~bp{WWhkN5!;Nw zP^?OfhzS-ZornlpRz{@ffX#(i(xRH!p|6^2^Q@*C^#kE7l!=Q4I>`#A6w zzIz2WuW;bYfWl{p&MUEQoZ9M!)PklBBvtMn*SLfr%;us#}3LwnQBoRaioNYfR~dm z7q{6{nuZV~wOf0Hem!$V?w|d#R}I%VXu$m%?+X*^>)i*A~a%sf5UF*K# zW8JexB)^lB-_d8g^~?kM!&V+qi)W>Xe0A@#MBE&X3-e!rgwX(5T2Y}^G7lsNOVLi; zc+>-z)z@R2Z^velWMI`W>GgG*BP5xrqyII*ZZCC845moDcq;0Dj5mt`Xz^VWQ-rGZ z%mJgs8ie4~3$;b6)tN>w=IFsjRIVjpILL$kPNg1fT+1ZN#D5_!sBeT2L~H%!mpZE` z5yL=~{A2(Qv@DLgPq{6-`aLB%>Yz^RIaDPq-+u2o9YcEAyutLf$`<6zx<+Bt?0!Q9 zsWsTZWr&jcp45ImP#xl0U!s{0Bn(*Hcte{{P9)&=2=FH@>{}53iLI_s?S|@+y4(iK z@#PlwEoZ`_dGt-(FXEb&_nm}bvf5qW2yv&nd}Y)fT_Z$o5i1bkm0h^F+paEW_jbay zop5WnJ^^Fi5Q6p}#)Hgy7EWqBXhAD{^(%0|2BE3xl5DzOt-rzJfb#13k4DfP@ z{uE9F7kne5E`r^K5v|@5i3#g3@tmY?5APG|%A*fFYI7OL?dGbX$kg05q$A9^8<>a` ztEX2ir@1SUV>wL%lF;{!xr(tcGLLpS{Rh03?2annbtIvSm137mIe&Vgr|14mr&iD{ zIA7)ZEa}2KT^WV`QO+-%?m3Jom-mU{jET~QD1+mp;7*1Q%i z*bBS=Dwz+AXgzBX>5ceE{-fUW)$VYyD_J@e2~*dK$H8mO0PVY~Njcxm`M5n#A%_P! zhpb33-5A--0~prKqcxi_i!s=^A5Mu5oWp+|JRJQ62O@|6D%^lCl-rDB@x{1Z%kSki z2pqS=Z{wIC@1Z43lRbZ`q23b zF`SkIb=P3Rg%AAmP{;nbDYri|I??1}wMydiSp`Mx$+UBT8gmBjwtAL9g) z-o5f&9BY$-LWeK@1FW!O#HOLPWq0-R^C**wDHZY4H#-`d6b;QcLSgop97PKq+7agc ztZSqc7_p0Ow!cIl`vBQ|ZyccmR2mo`bJCFVXIt`oD|9*fZfSS)C3q32jt?@kcs zqclcb0G%~??^hb|pqQ0Wivekw3y`B_{=@J%QkC8!ROyKelz@VE zk&}QGbV58K+(rSztdhZj>~6*bO{0p4U4DEbi(ULv%uqwAc1?f<)eq@qRsEl`oW6N3 zI&!D6OROQ)oyNcWkCo?y5V%jD*gpI`Z{teVcNy>7xaLCE^+EJxi@`=B z&=@bMXr~2X0iWyA56zOj8+ZQp9m(MFmU5RTAv?A|ExOm60o9jsq4eT}(ua*TYDyS8 zUBEgf7En3bi#K<{zOr2Mm3?`x$wDPCMst544%CpAYkz&p3jKwQEyQ5GXgv3>Z>Bl6 zzE}5O-%3-T3V&YE#R90~rU=fG_hBi!@!qS9Y9;bryZHntFhhYj8n$z#F~*V!O&Ij4 zE*^s%ti7G^sUDdQ+1tG5Dh&uP6w&_{{aW>*z!wQ}G2E^JyW>Y^+;P2G_qv3r9y!ft zJ0@SM$Hh}m^kqceb9MK?@}37}wv@%d0=BraqrNA$!sa&S9*8d-jDFO)U7T41xv~@^_q>)*DzHL7ybxaBcehFwvb2H8Hd*% zbRx_Th3VqSu(I>Krb{N!9CBBhO0`W?`mBM5^>&AU$C=JoalXNQ7R2>90-2m1Mq;tu$#8scQCL+zN4bG&CFb$ zYh&h;Mf!%Mgf{XgfG{Vd-DMv}rQK`F`nidr37-YUUz+gk#Mysm$)XsgF0gDg-}OJ9 z!Ekm$dsyG+abwfnsWts8(PTWoa5&L)d%qda{_f(UoI>5VLH7DyPrnN;{mzuy?o~G2 zhfMvUxDR0oNn-BPhPABsHtqJWbe&G21vp(g#&_wIXFnYMH1It?Qd-0;*Apk^^qu93 za~2qqBr~Y%A??nyKr?B9@1fp)v>bD1u<~fxAl^0lt9UN-DOU27`MB09b<2`SqpDt%4wqecJPg97Te@8PMuQZs);Yn<7-uDx}BjrUiGLM$Lfm1u3>VxI3{NNl{ z0sYIRe|aA4JN$=ujxq|V)+{#5!cm6{b=sD4Lz5in2?y~a4_a^Z z?Kn1h+~({&SDyEg?6(#qm1skLGQvc;xRE#g%kR1~yO~(%yhkkbH#c+p>rfB);zP&M z<}cKm`vg_qO%xIXc5InMx~Q zW21cSnz~`&zf#DhvaL4yT>urM2PuYc{c0Lxv!jlW_j|OTMZzqOx)b73 zweUG!n~OGm!?fg}2+bCu74LncckUEI=ROr7M*p-DgDR(XVg+Y&^bub8zvwR)=EQcb z=&w08(VY1spsyuz`qxHO(|bX`^|{2i2^8E^m0~bABUuyY%0KJ6yHvaElW%a_NlE)5 zgQm>m9d#OUAkY(JgsSo}4vM!BOMOa6@vf#~n3 zJ1i!`RY=6ufcNbR(C4srZK2Q|!3DLrvFXGWJ52}1_oNO}tg zp6PGoE9FGKlPlxBog)6>nSLs=&fQCyq8#BP-C~KeTHYS**G3VKrt#tCj@xI>@@-Au zQmXk|HQjjYb-uOXx;Imw$VWyReLMcxXoa@MFeGNjV%*5|dyDPxH*(AY7V(t>eu>@b zeXn7$4yr1t+4woL-Zy^-d-lCm$&WI+jQ3*gfv36Mk5>ztYbz9lmo`3*q|9^CL?cE* z9R6tzXE~l|N;h8&p^B%ve7rLv7=TCyhR)MH9t5{Vl1$Di^~~k7A;bi>7<--^&`!0R z762_E+I(06g!nU?(kLv*u{_{k*i{%5NHZ?!fq4&BhxUw@CQgypEp5!3mI>J2( z0|18v)=er7{V>x>)Be?#80(${O!yp$kd32)Z%cB~3@Pj{yI?Ti&3TQxxjowF6`E9n z$$=Jsocx5lC8wm6DAZ@7PiDX!Y-dx)ckB4Q@>yMv_-PT< z6zB=qiSuZad~RbR1~HG;h2au~<;mDWQ`BjeLIxvF4vCxh6z%BVGj8>r=l5x~{!^pX z8Ahv#AG)EuH$PN+q?+k#<^iR%D0Fj$X9dk9ZPEy2bl<~j5)gZ@p3W6bim)wzJZXV&hzo|&_p?Bf)K zbO}fp;mQ>nvy8s{nY)bnvs}TSQQdm;XE{;+ zEGNpJWe|UM9Q@fAB?f<%OBHo%pG;={EVo9e%H(q=+bXT0WiB_hX1Z+XAno9bnoi|a z%VrYq@GnS6>NuLGUd5gF>u*PNhqvM5V(#fek5e#eS=uiiqTZU4@AlNwD04_Dg>1se zQGxHG4jWu;_*v%KC597$nN84NTvU%z>K9uOM9sQpXVnoJdeCC4iS^H%iirjr0flbL z&nX+Ql76k)&9gqi&Mza#W(?5+|4c+H>>vd+|Foaq7`@J)Yp6|UAE(NCXne}4k1B|2 z&8{zZdAxbbwvgMr$R;ZD#kkE%kDPaQ%sW^CF8n92y4lK}rG4gGxzX?wqT$Cx!;grD zpWudf4K>iJ^+om7)N9oK6LIaw+2ick_G?D_y)o?{s@*)(XulS1|JhpLAh&;Rr2RMa z-~Pp}bz*U>7L{F#pT({1$YqE$S``Xh#tD@@z4~MW=U!Aq92C+2=>Ss~ za#E`9YO0|<2zq=x=Tl6@az|aKnOE6t2CmC73-mc4GAwQVk4^=qagac7DR!kzr+9^K z5mQF|qVzaKesgJ7az@L7qUv9vHvuh0v)(yHW1VE1*K;mwH#8KssTqo)`6 z=|t6ERDZGPAE-E`(S$&Es7=!&{zF3f6mW2h>Su>X@oq#?Reu?UhIgp`0{*2?KVYDw zKamvGUyjr}!XdOBUp@RTtw_1Q!UCy3RdkpHxla-OBwh5tGA>nGm!`yE&Sem@QG*=$ zfaWUfx{!fjhAWRFFyv=&5CO2~o?QfZJvT*2)_(A)hLK+#JI8-}AQ7WA#~cZ>X$=S~ zaFs_0!6PD)EgH$9{er^S(wes=ERUDye5f3wc<7X%*X5LP2Wi)e3^QcFFl|LHA0=~T zK%(~WwS1b)$=*^g_81%{e_m)1AG%==b&$>Qra=2KsBENcfNF~Hs+QiKJ#O)i=w!}9~{CyIrU)HP2Sl!wHErv;#jvZ=DZrsVK;YVh1b z;rYvOy`BWu>r(8Zp1Hf9_$8StfvvDIsmK}>c}Ol_{F@Lq;LWo534UjCj8;ewTY_y>OLe>?QF3)Ab>nsK-#!Z~9~ zk}|`oEWyp1dwOVL`Zyd2%%LY!$6-mifmXNAjeFg4bnzT?@p@kQfqALY;p$_g4y`I(~CbX?XT+P5Ap;Mi73gl;(Z<|4V_7DY`)Wdt+ z%oEstwi5e5~kCSv-~Ly4@f zQ07(wiw7|WfgvjtZd~@_01Z#6a>%5F2Zv^dzlyrw&^M?e?otRj|IpnDjY^<|H zr@dsrqa`zrf8q2JxAxI9Lui9Z5lW9+`&W8o^D_h5YH|ipI?V~dbAJ&gTiuU+0t48!#fdkL*p#g; zCU$I)Z;5@IYf(mtEMNC%S1odCUwwL9_qCX_x#;R85rXNz%p8!(z9sEp#&!1kM>YlJvtP)muFIY1Q|i6U7vPyl{H3zbNqkpPi{Pv!MFD-&IWsatKMW^2R_dz^d zHBKR#nuleOJe?88pm2%OA+u+nM@#2^qsw!=aDt@&ngVXA@HA0u?XNk}Gs`?Wvut8p z*()o}MS|;pOL3PM@r-x^2NmHSRtQOWP(m;FKhh5W1DGRI&O~Q|eWcP{%Kq9>H%c%A zQFlT-`EHSuZ)%_E=H!UGwHOY3XWwl#aTQ11& zzs5rpq4EuTtAn&aw|GZ*4n?Nu%!8eDJhV|Q@?1WoJvhgu@<;ouK5N+{6UGK7L?;SMm$h99=Tl$bKGssp0$X0>}#xoJJ zb@Ug*Vsv)xl~Psg=bfE2Unh_TzS@quDZM1;uR1#Wj_wZ~91`=u@gEEtAOQ&X0&(ZZT(zwk>w) zLPE%}^|aJiU&xFCZTl@#_<WxK1aK70hYeA&(%$0FFR$ zziZ%J+RH%8ccfa?is-OlnZf8L#lrZhgpMRoOa2Ust2G;wpw=WHYZ#U$0E`rDUcm;2 zq*)F;l+s`TKq$gBj;U(8r)i#0S6QFcOZLBh9xSg8P8BzFJ*9@nnkD}{uGhA-4S}~{ zcDu7jtEQlB0hDjQi0O{6`&i!0YzB zAS-lP*6FMd{xEH@N$K(HRp)wU&oh=*-T_{DLRwnAB_z(9g*0^+19g!Oo&UL?u_*jtuXQy`o=p@Hk(!3a`=I0*q-+r8A=ApTqx# z5uxW@jV-)e1h^yq(?0w`uTed$!ob=#WdtpER1{9ETIVMy&8)WS7Vf=8_eJMlp;VZ$ z8QyH`UdL7?{0A|tlk-8g;JNPy{Tj?ZohN+=c}~p}9%qqIo6@^-8~%jYzZ9`2awKI> z_1Wy))Y3V6IZYePByF)uo!8D;X{I0aYtK6*FX{_f`qM(ni>MzdyLfk6*pFBDvme)) zggm|EO`&4&S#ZwjB-wJ0)*91?!I(EE`|E8&xMW5;VM2RaL3R}c*`-=zZZARhk54bi zUO1_@Ao~Kv-VzaDxAY^x{@OdVRQo}0RDeCl<<>4uj}c(caphxNb9r1_#2Fdt1=`XH z{TgY#GMOt;VN4Uyj6Doabsr{ z*yFIFolzkW@E|Eju1o-Gn;LO$Ei#J83zFoyJx!kDSyBN(fp*TTG5Q=?TIHPp!G+{b zJp?X}+bA&J4skvL=IxMifR=~uq}aSQ>#9UvBSie>s@-ndMLZDB%Xo9>-&}3{-=(5R z)&nt$B3WAJPllO1X=2qsa#cSzg6PXaNvuyz8jJ{uf|bc2+FPQx33QC36h28HV|XVj z^XxP=uUZ<%@ufdV9N*d5)(rYxFv$0oS(%nfiR{_aQoKVrMb+NyIj-4{Bo-MP&ynzP zJ!aMo#;hw#kIXtNro8sU%MBT#jAkgMoPVv@@`M}ESu$B3xNwGi<@LtneG}^2T`hIGye_fux52 zfIG(;`V2fLT+};x)3*zdGz>LZ+m-;p(!gp28+g|*a3uwoW7o%K=d!?*nE~k8^jP&RlCwsIVjWs6IU&_eu#;^CsO_Dh-G2Fg!DWQ>`PRy*{rxz zJ!|-uTi6sZPNQe>gS?ps18f;uoY4!Uqy z-h;-jx$KJQuDPqy)MDoC@*a!aJ~hkx$Mu$gM$GTY2!LI%;RwBZJdsRprA(A#{*!dD z+oL@?g|QSbWsZt_wCpXI2Zb?z+o>X*d^e_qTed(CW$QI=R=HmttPQD1g$oZCOz zFOz@XS0B#}Sy*Nh?!9s0OnZrw;mr$N_IlXlcwKXdtFld3_F$*b{^7@LsJax&6cl$h zbH$UkB${Sj)ie{A?Ga_Lav;W1r97^uvDYN7lb563t+4y|yg;Ks+n^Cz^=;#%5cy=y$;=Woo?8W=UAMo{!ezjm=bxwdTIHz%kek?5kgdn0m}9fmCjeofRhR zi^7yc1?TV}twa6YM0%o>s6zI zjD8_;G4LNCXMMW)k&&gIb&*t`?iHy<3%&6f!sz2a1fb&4;HqT zwfA`Bc?Ho8;D=L6YNgYZ*;HQ(W7}XYOlY?V{qy$zMOxzr{1V$p>EK>Jj!iPA0*xDmtG`SZ2TSk{E;##?C_8HkuN2M;=+q~a9(1=3}?5<1%zUEx$ z&eU#2cg4^zi9f;~FO<)`BO$DZt^kBUExDX^6zSLzW6BITIs-8q;H?pG)wTgU?TRzA zUcX^|0+fxkw>=;0%5t{;Y=Fa2+jOCx|Jq-p>iLP6_oJR4)y}_eQ&gk=&UMk}sl>|p zb?-D>u9b@|mL9{A7h#&UQ|ZwPo?W%ngwCSu|IQq}cJv(#Do_Fq=v z48?AAvEeqZctAs(YX&sLNs4hxmx4LDk8%|zNs-t8t^8Bt^N+j|eau z0v|G!@yA`*M7{_kyeArtyLt-$Cxyc;?(|RtAS<;66?|voj8FXnkH=jqn=$3=15 zS<6vGKvhuctLP~9-(7hOH69X%6!9tWipw_;Jt&+`T&FI$sk1kKsw2+7-ltVPG!$r} z@0G-w|L#43M^B$x>66m13m=Z5lI!VRwLVog-YL{c(PXMaYV%iPXm()t{dE3xBwWG? zVQ5v!`0fkvz8tfqG8OgwHD@$?Ds41uI>;H(fv~w}%q5(UHlKTC8pU(Gvit}Vnvw7a z91!^T7z3I95(>bY-tE$eRW2B9R5g)HgrHoM_{~4iGp+oNbVvVzj2a-euQVe;J4YDE zko*bL8SboF(i|%hj`_ot#A;aBFJn1T;q)%sD!jW{yL)c5tnbKe@Bq18D01r>BR&*z zw&x(*QFx!u??bs5P`FAS&M)5+PGj71BjjE$z^p|F9KhG*(C`&KwiEAx;>(O4djszc zv-utI|HSd@BJrQ4_sk~WM!u7&G&`PXQuewv|9jleP1JwQor8TJm_={j&4~dr1b@s8 zcqj$gpKFB76(LOXoS=EWBXbx}aDxlGlkypB)VAzLHkj}JCS_`l$rwfG+{wE zf&n}M`N{XC*)2#%spCnhkqMRE%L)Y@fe+2BMr^LMg7N(@CN!mG-6!I1bAdsI(q9ybRMtZGF zjRuR1@KJkLja0_1wM1WvwbxZ9VrA1qLW#CaOZ%Lb88HN)X(ei|rIZfSjXg3`%2(4y0>kBxB*-2qakwG%0GY-$QJbrUoV`MpKK?n;XwX{|4Q zs#gxJ917_t$es2QzEYnQPuDiQ&1AYvZ3LkU@n3o`r!7^z-BNiB->9(LZQ8wIgUV5P z5F^3GSo4<>G0fLFzYzI@Dr(axYNeC=KUcekUV@Cj#{)GkY|uE)13Obn9HE2~xn@NU zW@N~w)hFKv+kpVb6M3#N+QO-ZQG*jT|bCkl=Uf(#U*XV_1R@q9<%=&f6=Kcw^#&{j`wHlKN*Ar4v1?o zP{x1D`R{E0Tfu)l{C5ujoy&i{{C6Jzt>(Wq{I`z(*7M)R{C6qrGX-g;u6KekYl4L1 zeiN@oWia&}b@kExrZ3}lX6EVIqMz~JF(kN#8+K-io)C%7#+(l>=7g0ts^cB+NlL@} ziFONbCB`x}oP^>IfI1tq>wE8EDq=UGsn|-55T$;Vn4J_drlQzuaKFHji%%OFUu&ve zN$^G(NVBtfWjUN7miA&HArSY%LhJQYA4S!yaQWpupEYljrF-h^ZB@dD^aR7ZHO;UW z5)2z~uG!DuMa^cPwr2PmsS>;LEaQrL3yqp(gdUgNVftNg z0qQeYnVzNWM&IHZ3>q6XD~+Qo>a_12)T!>Yb>eLqJLK=*!J-nH6^K$VXr~D^7Df6Ze24BTHeQC`Hxf%Q< zw)W`G?u_j0XjnE+NpC^Ft@we`Dg+&UU4TcC8qUU<0^?Vr?+d<+BOP>#{M!W+>}Y^W zF;TT8FVJs|U>)G_*9?CSPJltA#$irWT0A{O_t_!e!(%?y>HP@a5&xI;?qS);9_`;J zKGH)C?NqKd!FpV;X=tXzw#rn`yysbPySEu)X#{?O^X82o^GS}6E_HwJ(T$o^jiawW z{pKj6e%Cd{I)JS-dxvlJl!Ay$O zL=wW~#QdW1>?tIPs%>=wlq`@;kDcw@r}K+3TiWErM1TH=`h=~5;09u>gZS~0%A-)O zgHaoIVf~4Z^$2e46J}GK%()Yaq1nxjP_-Dd25;<`4L2mqYbbT+ZAc@~<$0rrDOg@5 z7y+SMH5OWt>`zBo4u4jx3EyNB-gn%%?C~A3i~Ad8D{J<#+bijd2kL#oVGp$U#ElQ` ziG<4@sQU@OK5(%u?uVdedc8#*q+SnYy$p6plr9|-xuioPw=&1&hOS>QjlXBf?>X{& zF?}xvCmHluSm!-2;LiP^84 z@Ey#f8VsQSx>Ce(kTJa#+jLJ7ZNYA3QU!p#?GnFtex~8v>I&!+9E%;@A0_rPx_|u7 z#}ESM2%KT>C6TckvMpe`2)N~=g$z-7G;I)*CmQud1+I}<4LIO=hY2hnNPai99o1_V z(2NGfx${cHIBQOve96h@fohivD7T5%MxB@?MVy!ov>q51uIm0Xg_tpDbq-)&aLLE|d4>`bN01h)UJw=xx`8y)lS^bBkD|n;3rf=?1Ar zW0`m&mcKwe5zh}_ZM^H0?`F1~)rx2J!W5*zsT$JO=?!X9m5_ec7ouo9@V z_uFyf2K;h%rtM$mmmP`o}<=Nli!x=*0>QX%BAUga}^ zy&n)BmiB5FesNrSSZWX+miB7HC@91bnBZ64cnbg$7STtHIYDp2z0zLo#7X+rAlxhM z)jr1Ca^YTSuNKA|o{RO-=3U5k#wHGNJ2=2F^ zeT3VuB&5E`eu9B%iTxQ|q`k0bkSVl$wan}1oYxwoerM50HS99#hc{-}5VL?=g6n6* zypHP!)(kN-xXmkM%sV;Hl`>}D=X~#PN4zY0h8Y1a?9|2=Wc-VaP{#OPEz{YJP@sx8 zu8}33JRw7uZ(zXKwK9*!6DZRz(;mWGgHxv2jJM@UGUhstxl6`;kz=;Wm}MOET^aL6 zjv1CQ*Ko|kGUnYJQk9UaVCovv@vna#>NtQ1)72f~0~D{AkK3U(wp}3& z3>)9Y)ABI>HUs%xZEP3cXyZJDC+mr6Kp{SS1z*CUEf=A{rbvOQDDXvU-+um|N)-yv z2rWM%2iW=_4C!IH_+R=AL)s)KW#?y1&g7(2K4WqwC*_+QbB!GQUvtcplAQe?j(Jp0 zlizX7ZW;3lj@cvg{2|^Nf%f|*3{XPXpW^-CGZ3A<+Bx`olLg6Ueg@@SyYHyEYJX-^ zFaTiZ(9DWp065PAh{prb&*vF%bD7XQlej<@ywopM(sPsGdtJKN>qodp;-!afviH71PT>XD5O%xmu}J} z`@~3?bR3!oH;!GCDT4yH^t}Gql0+i0!m}A)Op2qNJL+0X+~7s;?)Glfb>s5q;_X%l zjJ>Gl7^)g^XkL@|T;t?z?5mTLanX*0Hp{##dgE6TKeH_WvS8N6|3x2|x%mEniIC1K z*A_*bO=2CZtY?^wDP4D5pUF(z%mM2&uwdzdGpdRQmXDsMpG$~0Lq7hW^jEqqy~7~rP00Cz-vrV66Zb}boz4YP2RL#YU$F@rqble zdNwGqWm~lijXdMpUnCWnrrJj^u8R{v2`ZKGiMDZgPCJ+8r@2y|7UEDlTWWs1KRo8w8Cd+W`XpvzRA%XOP zk0|v)xDK6Pe;dbN4uBt)-WxwGIXK6aLjRofFDXvBHx0;Q+Ex=oRHQI|SRU}hk|X#O z&H>bVlMMW@mL$dxOGV!FFInPKB(Ykrv#(n3+EJ`gn`+>PIeX(%q(t#4Qljd;P6I!z zG|5o!O@+R)HqFH9y{S0&!uVgAW^psy#^Psq>}LJm*?T&5$19_t)uG&KnK7M}DI|Pq z(uPib29b*NOaTx>iSk&b_v-|{cuGaL4AMGuw)IY#H8cCIJ19;TG@@>YYUP%Z2HsbC zL?SJ4T50xpAei5$%(Orf)GCaTa|)aPWmWc2iQhNX zZh@CJeV=NbOZzW-lZL&qG74lYI7o_TZnA8C^ji8g27C|YgeIBB4IhjW*bsCmp zzyve(dlE_`ZhhYj7zfNjS9uCwvp;;FQ`>#KTc;th+U}nSk$9@3VHeHn7<>sOM*tY> zj=Cp>B*`Ga${0lxFKj^_{+aX=gUvnH68@Qm1vk02@Xu^4;K`#mZ1yl@?dw4X>cQzM zF--uO79M26+m+Tn9%R^ejBq0(J;(%$W#aGhACn#~VAI=l)w<4dmDAv)7zf+2=v5Ol z90daZdy@BTU$raWW2DXT$Dh+#qs|3nt?X%*stgGMH~%u75!VqvY;IEhK4qz5c4aU63 z;-WPZ9mmDj6Wsv3661{Ixyq-z#fYDMq+9nN_0EYM$r&c4N4)K(p`^jPi?RA(R5yW@ z$Y~Vsva#Z|DSR1^tI#{oVkWiRm7^W@CYsocK1UeQ=L#eGyv~cDTO+P5ovSSfviVV0f+v-H8lJ0K@^J|whn!!&y;bv9v76*}fQvO0NO#bQ~ZGFhQ= z09;R?g{I*l3rXpvhqD%JNocV^Sb=osvOS5WQVM{_GHu84L|D?}qx-csj856;&mpHU zhR=gQYR&9Im@_}c@pu$d>71f!;T0^86O8fsw5Z3KHAl(_)6=g3qvDG%X}+c$5W4ZX zQUdan5(R|>+M~ZnxERgRhI7>N2AXu&yQTLHJyj-RyvT~ zAP%hA*b1}lAjfko+g!gK@5BR*GkFJZyWX*&%dsecpp)Tqn_RW^*E`sfyqG%8TCtPX z8qN9+Yt73UPMZ5__rpUCrwy|@!nAj*+dGO~X$LVU<06e&DyMj>;ay z+{)>0aSFAfSNN@DgvfKX$TLag`3&A(Q9c=Cs1yY$V3wiOE%-%;jm{{#8r=U-m}7$b2eTML18_Bd#~5Pv;~ue51a>27FC+@e`(cQG0{Si zA=wjW(rtB|-W+U%d{3KlbP#=pcrFxJkGxo0zY!9;V8c4TRD(10`s7d`%zl4%@t1e` zOPlz@=SZ2aMH{)#Dz8;#kX2hd9e%>{qCtbT=QwC?G-!ynh=SPB(dB~O7T%TQXPfY( z6l^@jf8kF_4e;NHFDCZ8#F9JJ>ii3y60vH+;{@c!fdkb59J9IUG2*O#BkiD?W zA}OR2A|oNXVo??iIE$C~Z(}QmxG(GNxk*fiVh&$@S!8M~Z;E(;626~=#eaR{Kltza zmm0pJ42MpCV8t&;a)hfZN*i@=)wnLEZd_Zd%TFy^9$Pnf8`~CuoXA|?c$BmI;!?vK zRByjb%3{|9TtqhR;^BjQC&&)ilx;Jkrd3&IN6ntuUv!CP{pP4naeANbBj4ZB2lx(q z1mdfnX012kte$42H{z|HhN{}Y@lP4?P2*&ILkI0?LkI0?LkI0?LkI0?LkI0?LkI0? z=%B@xqSOjEk+|`%ZN{_yVO;sOIR9qu%dW*S+VRBZMVq(8e;4T|43NI&HQ`CX;d@Q6 zMuYj7y(P10i~P>IPgQm!;vo7WqQrjpI*jkV(l4t0$EC4G>5JGJBKoe>Ld`4Izo)At z^nXAZXn9#cmS2C(3XR}&rByYn3Hv*Rv!;3KB#S!PV!ZxBon%ud+l<#AtCQ^NWV`Wt zlRC+%PIelvIqIY&b#jvN`aN}0vN}20cs*a8l%h^fFAzpF{`B8}w64#0D5iK5q&m{jRn@gwPBk}e3!RgHM^BT|ddri1 zM{=YEc$+wUrwEhQTo@(hhi{<86CO$@(%j_m4K_ZRT4=2xoK7hGA;tJ$qqV~yY~n*I zjP&WFTXZhRacVdH&5Cm+1O<6v(@gaoVJ&M>Z$nC@ISitIn+-s)xLZX1BJ(W;^X*3J zwEvar(FX~IE34M_*Z-<&Mt|3!=V)CJtH>r~bbthUTD2=nc_(zMZQT3H@lDtvx(0=A zFi9|_d^^yT{%r@Z6TTLK{&Xxn zOrSsAl1#s|Bo!uCTZ_sxtTE@^-QjH~v2A2D5R;#6&kCf~O$u$*FbgcYdS`bZeNH+^Bs5 zi^ZQ#{2abXma^{CUWzUh+Ny+B;qP~U%wHGt%@D&N#YBQNYt6 zMBt_~ZU76X(rJH?aD3?ptT{#F4RF46Dq+x-nyy;cQpQw!T5ukIw5~%*dx!yq37~a7 zf5v*$MjrV{$58Ej@2IX$p(by@THQkfq&7Ao@)Y9zw~mT7{e$*l)sSt0kGU&}U_dO1UT+DomoyVDDg*#8PZO%MJ(+(J#m|_FAV>S_ zGo5;z^YDJ8zH@2&nTul$Y_QrBh2QXq%(sJ1I0MUlG4J9254Xf_wMp3>-hNPues;u0C$4^xc^IMaO=~{15sX2JIDRO5TLL)j;6Lq&m#wB%;rYz`uv7%B z-P$JF-={D&phEN1?n15yfGMZ2a8^4X<&Rdv%A*p+y$xW2b0v~Hgexm34_Y}{tiSU| zE8Dq1w*x)d*nYXRZ#i^%#Ol-QM z?&ZRW8E}|U0HLxdn?Z}K?DOyScmGzBd7zZB;+4;W)vn}VxhsYKIq6^T`?qpeftL4C zBB0fiVY!?AHQ&LV$^BdAQQW|lYE!-(9)ANj`}8+(SAI3T*A3hy#{FAMOt@BN!! z2fWVvJ;On@xpjOASMAEsHXlxmUcyzo(rPxEKpk+A20@Oa?j9Q^MY)(174#DEvRG;} zN&gq2jRPL(#aU;lZE1e%F!o^3Pwe#;o5Kq%~9F@1v1Nk?r zIs;c-zrEPnzsIVZsM_ycZQKjb=6hkbwFpcY9W{QqI`V3`$ZDF;hz39Y;3ELA{EmL$ zB5Ok2P9gdro7FNMbyr%V-YQ2biv#6j3>{iWXav4(ghDYzg!dQeT^ z-g>*HHj(WksV%;y3WhbBUIA;_xCJ%R*VJV(|f7EK+`2g4F!3jp+=5th`Hf`$s9u>tc z=)4aK4gC$^R7!qLBzd_Unkt|TUp#>)ntc>IG#U%)Y`C}AhD_*n%imw#=Rtol(}-9h z^LErNhZm@g(hF3OU)~ol)tI)@m*f0NZqK?B8lT(mi;mADJE0Id5{Aay(O0HQD7Gxe$1}^YV7cTjZI^(mPWPqtq0TvCH_Q&C7~rX5%>%0 znTuj7Q%03}LjYh%-+GU)Gkl7;i?oHmj*fqVF=?UJj><;-Np9omU<$pA`38lc-P*7P z#%S->uJT5oM^_EtbtAZXjEaRrp!-_c+kAF-!dQBN$vAzj0JW05$Fxt(`kbe^Yq6Z@qs4*!h< z3}v(*%*s-3_P);Z%_a#ZAOoOn7@3GBbIe3M&TJodMgJ3J-vD3w=;K7E{qtv%M0;c; z?C6Nn6BSRARJ`jhgQksA8hF^2H85op*QYmpcSRy?_B=(00{GK;{UaKJVBje`*l`Ss z_rOQ}lL%}&V={O!o2CM6*B(cJPX^!}%KTD z6*caRy0nbDRFrG;jxoyRE=!aCY`bQNl5O6CGb;HEwzrEf7+`ywVvwxrY+sM_ksnZh zeR3uD*X(o{Off;KYhc2@SKB2%`SFRdMC$8C=Zq1q0({3JQ|!<~x<<$8@_b*0 z4`T4k;q5|~$KQkq21MG|{~j*&cyeO!(O8!U{|S8b^|r~BUB>a@216sx=nnrvxK3RT z&{Q$Ih%LB~)j}?0)q;ZHV)l5j00uS}K6Nx|mUYWeQ!KDvssz@<^-gFpdwU@cyO2$q z_dpmY1(rC-LXjaP0RG*f2DWW4`8l0)rrFgYA>krNot7NRw{)yv(DiX`>NF?IxH^Pt zf;!DYrz5vIEu{n3+|!ci$D>XI2zr(}Ej6@(o#8MHHCNZm3kd}0-}@HPIZSw08C;CMI0wRtWgbHFucuCh23mg*f?r>Q~{L; zJiys}qAPb*IO@iODjJ5t4nH(jI;>d>GMy#udpvM5pze`+J2*Nr!2HhX;oa;sO!||- zQLs?m$$B7CMLG`}76-RbHCHf1br`x0!V^^(BAXR12rsm*x24hu922pMfl|j#da9hh zp43Oq4hAg5ope5pZN`15_N#w7aEFI5%wWet7HS$wTHdNS0BAOPy;B;!{ut+fHp<=0 z_OUm{zQ?iI6)P#(xNNJ3eivK}NNO3ajSg zm`y46#in>cq}XpL*AAulYejt2t)h4Q)&0a@C*ymdyH3j}g_=CQQm2K6lBI@E!?qK8 z=62c%@?7p9&2L8ov3n+E@nY_vUs#CMP#F&b<6<(kvGQJ1H<0LKB?6h>jYymu9ta&x93dDAi3B zcgtZ~*y8%?>PQ!%Iy5U?KHXepQ`YnFjr8dWc;T@btgHvQ0ZuY}-1?f$*#&YG7C-c@ zu6HitO;mgR@uNDOa0`MjgA@lhf)qRIZek`bj7?>XGCjxbNneo33k9AW5$tGKf)NWG zMwFEGo~MYJR(1x(+SPt`FeuB1lG1mg-z zJvK_9{gh%VGbV84owT4SUn7dU)N_i?hFWf~C~KHj6AyY~K6A;3KH~bNi{nB}Y7MCq z21>1s>pFK>jUYNO%mD3*4R+cV#%W2WhiYint=4y1T_F(*7tSEwzB9xqvmE_PUkpvpj87 zc{rxTY@phjmSi}6%@x}5!-wbC*duvW2v1Pm*jy$S|F=zzlCtAK447U5KdluBBZ zbsXCb3kv2cz;+%j(r|_AYEPW|GEJImxfT~_Q$HEbmP74{^IpaY?+5g*{HBK2wlFwi zzq9}E)af>=laL+PHm93$ngl;2zSDHd5gGfpd#{3fX#YO1CA-aA7|FNH4Slqj{Du~3 z5xq2}no{WAixt5~Q6|Z;35i8#f0zH5uJu)A6ZhGSFDOy@m}~pe`x9jyb1wdqUfjt! zwS|weOX&$pE5r093!ym-gAD-}{Z0U1sO?%PVEU4^JN?5rqzMrJM|5w)xj8!lh64KSK80 zkbQ~Qfi*vYdHm*&6VLun@c*Fo3bVzbx?JQFd<>cP$t`}oK{2dPZ80IejA7I2YUyKYv*_hd1ZUt**=c}S$GlCbkX zch%y4UsGUi;$FLY}K z#L3#t@AoTVD*lM2>)^$cDan2~ldW@w1?Rrw0EHghtZvtqd`M)f)xUkg2(|m5)PEiN znptvUv{9G4oXVpw&EU}5yf-T+yzQs{@VsLy|97m1Z z9gE){j4V)sW64{+qaQ;Z|1wydL~gY>>VAjkIlXbH=b@A}^o_dek6{nb5_t$ZS7SxKC>UKW^0^0383nV0H~%DMb40vIgcsxRgVhFoTjxd7zR23pb}7Kpto1r zX_v@2PwW!W){8T{SkS>I`^=+FU$PvSy#Qbf1wFXX977N0Ki4V!eVj$Xo(i$jvfk}? zQOx{Mu2yBYcRH^(ZP>fL)?_(ySmt7nGmH7CJH61Yxv)-zD_qLJU4qMz%+J1!Z;Yn^ zY~Y4>`Yrpz4`kWi3PJJms)VBrvFw#&n(v)nRS}E+50p^30F;qdZ4#vjFXnZuy>k#O z2G*u5g|G$#G)&j&M@AW zP!%E+a9Dz%fP)iaDBzdo|CR!_n4=W%4Rc=#_>%cMDIjAHNeVcxcXXzJcbcOVu*BS( z0)kYDk)CkBpn&Z$6fixG0-ir01?>FB#K*u#O%^HE7%k!uo53lRJ(WYef);jJJ zp7p{{#>lGU04O`gjLgfhaSP3_Q1(_0@n}>YO}lqN&X4Zy|I*Cx_sZ61Ol4=#>5Ku( zrM-Np0gzdR!lp0Ia2`M>e-G)nB<{XPXHYcZBm>3{+Mg~SV(K4Fk`W|n)Nk9}&#f;G zZf#=*0Fb2m;;xkHyFngFvsm7Z*`24UmT!RDhR2PjZ*;>mOrExs-ju>KC z7aD0D)|C%j6`RIom@1}854y`lnpBoM8_ujDbzWcN}puVy}#I;|3aQXu#0={<@>L{)_PUC|7r~wt7(P>Ru69-9QJ#GPR2pvX9j- z_Gk}2uj`%l(fh=hwd(N01-)mza5FReZlRnLv%>1!{9P=m+`Er|mtF?8=LeH|sg=wz z(boLKZ0lV5UGV+yyxOXHt@>;bD62NhWrM`AXECp$=o)(e!Xdo1^_UElOHWRXF*<+T z8b3PDenw~XJ#ijsVjxzHwIm?*&G2{RmRj9nJ*8+tvs_Op+TGurimay;EtlRR>nRm5 z9o5mrloDM`7{nMyLR#A8JTKUVken4Wg>%1t^^Xm0#2Gakdzg}+(=Tn-_s!g*@F-iq zf((MU#p+aJfHAz4!pCF%H@ZG^OQlJB{!6_#LIjmJ(d)8#$Dj<0#V`b5^jSC3^{OJ* z@3m1pA3ridxW~J|)s?&p$h|@7p%Xpj2ky4gTq?bP**Vj%Phrp7G9bdlg8vrnj%VKu za`sI+qtwO>9LZ3skxJtFp!YVjhSo%kdNHkEv2;Ce(Jmcbzo(4-+33d7n0~ZTlZ7WJ zv(W3irNXn7=5sIrM6ec?(K7S#qn3UTA3u9q-N z0zcT*#w>nts*O4Pkfb)|@06ZHHQ0aB^17%n|PAc@4@dIU8&JUE~ zY<{2&EBJvj^zZ{^IENo7!?}2%{Ji6&BIP`3q}5oBhe2v1JZ%H&vayaI=u166P)UpV zfl6A+4^+}JexQ<`;s+{e1wT+pD`91tU*4m8*3~flbA15*y|pLa>SqsE#9d}l;SB0( zJeAJCwjT8B`l|Hvh(aZ1;P2Qh{2euc{tCyOu}(ROss$V2nM>JA{%B=ABtS}%j0@cQH(P&-?>9fs-_mZiiJzD0#})e+qh!(t~Xol255wEVMgH< z!iAaI2t+-KTOmvlg)1}Rdrder!+v;I)HT_!=s9aFh?3WqHFhod#8C;pGVc(`t#!;XgEdEhZU|Q8cy#}V(kBP^ebHn!`W&ju~Iu* z#8P8A-*)gTz;^G{TDE=_>kzExP3%|>{|$rUyp4T8!MN~o)WHp2CbTLtpdktekMu8% z{-wuhzUIopAmG!kf1SNbP4I8uR>!qVFw#5fUIq_GIry6T)q_oU)PdjdH!V1)P|M!K zCz8yj*kZeWosB2qy(^uSV$=F6#h$a5kJ(w7P4Ll4QRZL9o|TfCQL!NXz$Ig<^lS9;s;ZVtS|E7F@kSa7k$`VzOG%y0+K3>?}Ef zE!tmRWeuYV*)1}fp=7jtC(n{#HO-@HnwZtDbZp%c3A)YW7Bo`(c2h5Dl=mJet6TT5 zI9-SnSXmuT7`0RnyIr=Md-3Zw3fP7fLvNaSV~0M{q(=cU&gL{zkAZ{r zgd<%zYA@^6*@A50)Q=Ce_NawaKYE6c$PHv1Ra6@9HaiN*0>RbduRhUF<8~~?NOyqJ zWh-v@d)iam;$gEG+}q-Id;oK=S+~dU1g~%@eXwAMN-S84)8yY?byl!`-nqgdn{tCi z5vb|SA10ptc5tcjA%?&79kHJl5;wN#YIm3^zqt6w^y^+%oca#qAP<)~$U|#3m4WH2 zI$I4OQRhzwi_?@XdYa1h9Fr62?{D%%N*4ROTiddeXOJDvhp(iB>#GaFnXQQl9)tW6 zQ~sFc^gZZenCP@WXF1i$X`NqMaW;d&y|k=%)ZJ>0fh=eWzsQ#M#yjIc3PhPE4`*5%`hcTu5g4^VBo(I!@;8P%KrRM6q+P;8N<;w$i3~72qyUKwFlIqtm`IM_ zc^((U6$)&KW^a!w|NNA1J&lTeW{Sk$9!>-XX1L)Hr`{@3JB{4d8Mztx+C*h#wQaK6 zv;lYhVtr~%5e-~KYXpQdyd$=tgPqYH5rsr5?v$XNq6yAEJw%1dK3smry~KbHw7nE# z9Mu5A3`_Gdz|d3!TFlUPc0&hox#-_EIm+ln88;jocQf>H8c?5;&AC;VsDXF6F>OX; zAlx?aw;mddWVrS7eXuDsN%!?!@W7*vS893|o$J(H{&xCd{KOZLI>th)p`G&#N1GB~ z-|Ge$Z4Zmyh-M;8xnShwJWU4QB*0WJ%pEMc<-$lq`-{IjA-0h4EAPg9G+@nW6X(7- zxYMYFXv?9h}@nCDkIiVe>J8NwuuoQpvLF?mo@G*opFu7 zw>Mhjfot1&Gw}(Lh7mbsV0gUKk}Mw^S&|P|(m@lSx?>m5q=%dsdqBw3@ss{`O;H1Y zo0p4Id@hTAzEr%0U*sel_25?rw})f1faYXi_E_04Z5D6>W(we8>qEonV~R_)xP+s` z4pp_bmX3EuKdW({!{T@8xeL#A7SX?D^lx1Kxz26$Z(i_RXG7pzr^!4xVesI=g9eNL zEQ90!v&QVNQnbiOPsz$>RihvXO@nh1?P4WIUwOId}&O+7} zacQ5ri0LhMrMi{D0R4@+*JR{h`_FZ@XoXoie=@+yq`~Pv=KmNBcWq3|BC14v!|02Eq6nt7J}`=&wR0;v^_qxKp2iS z*Z@HJN?q9#3;;}CTlojb2kmk4L0`4YZgM=)M87t`*U+y8heSJt1*vR$v~)h8M=)9W zdy(UbEmWa;+L#%{<5oef&liKsoLxSC7e9S)&W?EnYA&BAl#CwF}6TAfx9l`olRr@wF9C*im zN8J;#JfEdETDT;jmOc#Bnecrv9G{@romX&S8+hYiA4{JNwQ}KXQdBR&QP&gY`*!rp z_t|^#eKwJM9ICk`j&Zd_*fRTfFl5`~kj?KEl}h#A6~`s+?87Bm`sEVsyr3qVA^p2k3ew0W4@-! zuQaRZYYH3fN57^>v@Xi!Kpv;#t2BWoCV?_tB4oV?aYjR$jIK6-@{?nL@>3%~`4`ag zQr|i#f%0iKp8_3kI=DyOeeeWZYs8cgJ|f`qI}arE&+HC2`UHd8`3dxt2V`@_VIz*s zwZyTxj)f~I17da8Ji)B)Qu<*l2TKgID{|?>>~{W)l2YV&2GKf^Ib*%=9__wJ}skqCdL2Y8%*}t;)ll- zK6;?Stzhb?BP~WjTGHQ#M}~JmRR7 z=r|y#$H60lUeIsMI@l1UJA%dt+SA;AEnGpUn-3n9R!%OpIo=V(ehg?YM-b!G?H!+r z2*cFvQIgp;0+Y87wMqs-Y%|t#taFH__CpcZ5Uznvv&1SmH#<#n@PE9ZB{^-3kP3zX zl+im5%%r*>p#O!jyhWg*TOU8yc@_P;i~fB`|9>yZn% z!UKFr06i2uGEqRA)I+2%gW7_RL9N8M)zU(P4uM{0@K6i_e9RSH%njYo58;N6HDP+) zkIqnzRO+4seN=LMM{GqUrNN&a%RJy&J&u@*J5Fg!Zy{E>6>`d(k7V!MHszGr>(b6% zL*(t4vZtOR{CXx(3YD8rNLk~onXGZ2c~X3K_y2jly8K+{77Csv z>0P-(fh1}X5Teu(&Y_UE&RxXrDYAigw+IHg-VA!YW>Jj)NeWAWzlqo|VNgGTFMYVT zvi`}+FK@5YbpepMjY_a-=7tl}IS<~4Xp?S+M~^)DK3E&i@5%Drs$IzM3>r%B7Ogg0 zmu`UYK1iGNqeN4vVU2uG(8lw7nS3{Eg-;JM33OAuo3ytJsQdSzwR{_sa78!#57}l$ zOj4a&aQ;c>gHM)US3YypK`S|Ox^eie^jf4eo*K=#_{mVTo}p^(o$B`Xs}DV!;cQRL zrDILFIsB#6Qc3BsE~OJtc)K=;2wHn$+j|*KMFXOU_Mde`O}hOD*i&l5i3Hq6ti5== zGqcI}wMEq&i+=$#wqKrVK1zoQ-&^L8<a%hOz3tb7 z#HKznMBSlI&+kgE)lX0kkDSd~j{i}AoZmHw!qpuzhq~b~QE`Tr@vqEh!&!KVWMxwy zhZ;gxs?2Mc%i{&J$1Sj@ss2{`DO-JN*AgAb&*IyoTMNqJ70;f;COyJ0!)2yGP{p-LHuZxbiy&Hs@SIK`^F z^RM6vUPkAV3XWgZnZ4B68{h3@Ivp0AGTFll_toxKq;st=(=h0R&%!0f0CpbPE7 znsvLQ?g2AfkU3+th#iaXj8P(G6;VzuZD4=dly0@(Khf!N!9J>0Tk|5H>nF*J?&~_# z&at$*vRgaCj?e^p8n?S6O#CWCvMa6QcH?=!*OhXxvvUmd#vS3#+v!2vke5?!N78A2 zf~TeyHhp|9emoD#crXBBMKj=a4^rH@e5jo#q9N234V@beO^b$lWaz5YKV><6^IRtH zMY3(;Mnb7!Yq*^-e#&us%4^w`#Md>~E){yd4See&+_cZH@b#SY$SmXTN2;*gniT-J zZPi`9G;(a7f?A>bvyCH|yQVOJK09I+P2I~1XYQOz6Bms4E`QC&FIdBewQmYUezs^f zrp)ayrwY9cOCX6iG^O7QqYt3|^@z<>m9AV+=*gu5w8yWMrjOUBO8LX&tV**dO%&d? zZHmo|%)aB6D8|ddNW;m&PWD%E8MJ;GKOGu)%H;C!U0|is*kPj!mx~xlDCg~<*l-@2 zYg4=1x2wXBXa*oFcdEJe@J@V6lI}$(sJSTzDL9$fPfnA1P<+VJelj9a2=y3GK30Nf z_MWYF8>fqFI_P{v+jINXfQ?sUaODqe7#@o&-=4_0@^hk!;|vEKk0#%KiOeNM8bhb> zin{`T4}o6217Z;bR5ixmcFLMdzhuYACvoB$E1_RaM7%&qL81+6%rMZAeZy^#UhGX&3Zz7gDciirsShLXq2sy7a z;`5TW#w@cJ9zC;6Y|}bMnV#kLIOPA`2<*YT7ai3@)BfY@NpmdpP?+PD`sCl@SQa|9>Re}>Zed?wi&@HE zZS0Y6;&lu84sVLQ!@IU$-NMV{m7rfI-es@$Os541Lc^gU{*<)4XBhZCzJb0&*Aevz zb^VX)b5=}!R`jkw2VITU1_lr;_LliAzgI-QS?vwz5`Sg05ZJONad_ z`ry|7@$ztKIyR17S`x#X-8#P-IDMY1CxcF@q_anxJMFk0)35|@83msAzN|(>Y$48o z%PSx(x2`;YJZ0cv!Fns}_RJ9i`^LHS+kT0ADh?~&nD6t(N_SIBS3G~NGkxW`&JFbM zk2K%@O}`h@@5xm-Ya|#4jo#;s{srX`drDZ+^Odyp!9CQMMFa7lp-}iM^{ih3yu9m= zN3BJEEy#FcrCl(?+R!fs$E%3i4q7Y4}FSTBC5?%)! zi@z{oTZiWX#}W)b#oUXStyjN(oIJE=W=uQpZ3))AT4!(wi!@U+#|LML{d0 z%N?PJ-k~+uuI7gkVg}|SIT%mrly zYt##itI>vQi9{vQ;=9jTRHE(u;%goHU62EYep%r;R?GHnWzY3{S2F)k(l*3Vw+G9x zonwbK;sm6*M4D@qGK(@UG{u}f%{Kq6O7@)i;8oarEXp*?jOlLejWx77Z?#B!-))x5 zDNlRw`(1Y76n?LEMP{PO(^|{D7uty9me#Ujz|2$PGN1WfneRsQ>n%!>-%#^t3$oYO zfiyZC{uQQyzYe7HyA@RUhuPLvP~kIITzo-}-hZX%I3Qm0_sPmE_p}SJI4%)8U@zjhP*P>Gw*r61^oS_<5605 zF<-7|t9E>(M;&;j!AgoM$xZUClUvBIp6LhoBgXy)gD>oF#tdYB)2kQUB{Q_UJ|_aI z`L?FOao(Ql%*LIp2LsNe~`y*y?J zli~kN-4fmfJSNGYrYHgxDWwnGt^u_xP<9z`H|k=IlYFO(H5p5SGda~}`IN+8*%XHP zUPH4WiJy{)<{G9l&JJrGcVh-{3rcAhx73QsNL!jQoY3fHpU0+fkvEO{8)B6#;Lo4tQy$iy)N2@@G z5P$a2QC(U}Moy5*o=UTKh$mde){?vEN8cA&hW9h~w^}SbNQ~&XONZRdqp^m;4rv%X zMCt1MJ~I;R6=>t3es8lK{r^BQMTrAH2lUS)C&y&4q?S(i7Fdb@f|iD`S4)Ma-~_xqUHWw#%4r;H}}1jQMB0Ek7({KF={V8S_Hq z*@(`9*YV#`o?=%spB-AhQl|J7#i`wPLMT&do4n|&+vEN5e5PsCQPM~gJrZ~0#PkGx zC=cVbqJ_p4ZI3y=BM?#9S=ki&20*n}xLq;Pa&xEqj-*P@X32DNE+&2kDg#z;FjIO% ztGSaoqDU^pW|t^cHvh>|N8MCx zMlHZK&f~v_(*n1$Lwk?95YD)8o-I&w zlV4|R4|dw)>_CNc0nzDHb5j_GGZzq@a9Ys8&MQ*an&!jnYqG1kM|{n8-=}6Z z_e2~*XMqd8fNnd&k4|+9yd206z~H=1Ky#)sG-n1wb7mpUz|eMTU9m*dBVO zr+jtnDe+XGjzt}Cu2#od#8a_4)+V0H)UkH)RIZM7il^D?*d*~(p^i&J#D94~erfOXWgdS~ zyMs(r-=GDRX2idimu(Z9NOn90|Y!8dr_P^9tIad-3CYY!K%{o`76bA4o3w&7h_ z92u5ZuElUJ?K7N>9KI|XF6$lO@TdAri@Q0zwNE}J9KNAXKG$)0Q=fb;Dh`Ya~3c7LB>OlfUapJ2>rEuVt%8sv$rO~k7i zub9W$74-T=_`Ies!XgePvo?wXeS7unmMVJ-PiOqsb=$-@kb~DHHok0L2-TxAn}QrJ z(grzx*JU(u#k5r&pHfENBuPJpSDNWGp9k$>m_OXt6bwbMeB|{r)nS4b8wo=n7WxdyU*RD?a1vtD45UM-;AU@jkOmdub9Q!6MYh4 zGHdhu1Y<61cl8OzRMy;mf?Ij~`vhYmYZvti#yr-}rr=pYArEoRyhD8!vpCAq82&Of zDMxww2tU}=r;jD{hi~9JlENHg8tvpZwd?+l$!Y+57lWDbE~W;gZ}w!hk@vO$_PKUz8RSEac%JFoniAKE)PIKT z;TDO?Sv5srPZPecbni7vExG`zJ=t!HcN{TpkMZ9580Y`kO7R!H^caqb>eElr-_9l2As+9wjo3MWS-elcAlC4P3%Kz2I$^izf*P^}nQ{#(0Je5VeJ3=@`#FUFD^$UJooaO0Y|EEi0Q7>>cNepI{ofvsVb=0i~nD@;#|90=C z;9k=8eV+CAG91>=>Y{iF^DJHVO%RK9SvTd-iuOZNoz_CD7P-=|i=9tX9k)@q$va(X z3AxSEzvLntt=YQmRbPzT459>})}EyKcG?bzaJaNzog<7Xl)XlLbA19|tefkas{RA# zP*6}&5*N#3PBOa*<+<-xd{{eFMatD3nwm!cQrsoHXuiJs!eGPPdz+tszsf7c$@$< zEfC0mm>>JSQSA{|#Fqf(sO;0>e+3~t?5Yo^wF9h*75<<-v2D+IOyFk`>RXS{ z^XJ>eXAww~LWn&?WiciS5FjjyJ4a;Jezo?^RZipbNBa6xezVjZ>6tjG$Qd_~oq8&X zJP&DJR*hB?dj|WR3@P;+v}L;Xs~wuYYWB-nP9@h?lVx(${oNEfC=bUC>g3r1nX&4+ zRqQd;rWBTWTtxK%kDkaWiBej8TWx$(K!J-Hnz(ZYQHkmHQgL-? zw0kLNmnY|^xIl>pzQB|_`o30NT2Y9zCd--GG;xwksOVlrnJ@LDUbYTv{aqQ(&^$9- z3_=Syd{3H^m{-G|P?MRVo0JAr=I~CoB;tF!{jbOd%3Hkl*61_u>&Ejgbu-QTXNkva zrWIg?N=tj<`uFI4JMIMA6T$z1hO1i+?m768y6fN;b?3p&2R{`Tki7S^aG7`kabLJY z2&N+}PGA|o9o!yf2`nmIW#fsT(nW*6^==+~ECndEou2NoDNwRun|croyM@z`Nmau-_N&cZV?@d>Ro!7wVhd5?CUsA^*>Ep%9(zOCZum1f z54HrHEV=8_j{Qb)?Hy{@ej{_rgO=%j$trDE0*mDlpg&5zLu#Ysa^&c9k}5P(zGI2- zj8R&$X~xjBb}Zie4Ro2r?3er!F&`Gl+`wrlgB2EtDY^DiWh1C~;dGd*8ltY2fN`>h z09Nwt*ZMX>lxC`$5X!IcTKCbNGLO@&~y! zvxK}aV*4054+2P`b}?__tmE}GCgGqq*medV1ChXqaj9`)TxOgYacJ#jPRjC2ajXMj zOD?5R2(My_4E^5WL<*{@qbJ*o7{INoMDCq@FY|~4AMr)I`fFY9xsjNt>1n4@gsC5^%{|M9F`qoFrOSxV;?Skt@Ib%n zVD15H^6(Whr|mKypFGiWDPd)0ByVnTn`rQd_xIc2&bpXl!sV`XVY{ld4#FMQPOWQL zx2_x$*Hu$#6P)`L`&6b)C`0za&_{=xJX^Vt;8d6PQ15d3qgI|)9`fK|?WX(tZT)mn zt)zGf4hw5oPy@6wOr6zKE^W{g000fqjVb`VCbVCIzHe`m?@3X$7-+a0Y;I3nDTvwx zZF^O}9n)8EwczmtIv3f+ z?4!+z8$E|kbTjDy;yj4wv`#bFsTah$YxVZqhVDlVy#2l2J3ULW6E%AK?W*5D%B|&& zhXcBLirWrWAf1)>jk>Vz{|8i!4r>i!?7-pjAaOR~Tbx~Ydp1zVYB}D1d*J$*60jVm z+CA&wmD^F#7K&Oq`unS2-CmLoHyKEDLl=g14!=SyeA=f(VhLK_lS zGD%o+IHA8tSHHaC8@_Ol@*~@7k(;?QY1X&8cwb|M+Ajy2I{#e!t-dSvzrqq> z(jzh#3R<2Mi*VQJNdem4Efv~R_w+k*&oAJSvmIb99vUoZYwdMe+P{}9M#{o0x5eOO zZN%-upnVh|V2)8K5li=nKcS;1KvOI+Xy9kFDjnok#His)6ILim(*d*S25YHpyz3Kv z=4~!jPn>ZqJHBYQe)5o{V3xw(h_G!t(aiLjK@M6`?9Z|HgO0d$jU3 zJy7JETxI}~FXrPDWti+Xph%XAAewJ{9Q2ee;e76y+iy3`n=eQsmQ8_(Ic;vg3-^}! zF>pmGu0j^~??gv9HCGpWk!%Jd z)Xj7(+k9{*9qu-_xAGCr#%C&91c1olKbNXPk@MQe=GO_$RjXsskhofc(3i+cI)w&n zZ@pFUf{dWjk>|>n+(&D8`-Lyr}i5DGu5ZlFejsqSKHV7&8E7yxWq*Z9jE700~UhjgcIFF^$vb1jxr1n1c|Ev2P$_JOK2d-WCb)vIagD7 zIK^?OLiVgj+jKI~^cqWZr&@g5?7mJjgnm;`!M+WaY|sOIaN#pgRV|w-4bNQa^E?f~-#-7YRmx+Z$MnPW)>xl%*6p=h zu>kZB<$9=jjR~FC+WFf*cIllRr}TJwXXxXUi>^_o*wo|_(K$GH@%2TSMf6c6J=!o8 zxdbs2b6`*+CStCbhM)J)Mj5c)51>U+ic`KAaI?VuS+i zqG843_R!yg&|S}k(;JhCZ zJF{Ct9+*YR@?Kd-TxZ9M-6OScU+afY{pU)z5@^hcX)l3&By@~t+hKYBfw zzd)-^>SyM$`l^h2Pf=g?EruW5AQS!gCh&ts{snl8(lYX*C z{=SXK%;cy8nG(0!qaliaG{?-6wpMMS@*k1=qy2+c{Z{4~U zMeDYK>xO*#RBgf{3oDNDazmH+w|mdmx)%Zc7q2z-o2iUk@oi@3=buZ@58GdM=DrPmh0f~zd>6=Df|$F z?{+Ny6TO}cY@z2=hrf|te1Uq3XfB%ZrZZA?I1O_Lt6jzPFQ5Ju#38NE4sH;Bi#y60 zWxY6i@3uTwH79b^J%g>bn0Bqn4kcXsF9EN9lW*s->&I=$CZg}H&Td&SrcgUH*Fdd~ zl+yK+?9IxT@NCbZ>iK;^)wkZmpz37?sCxeV3lw_`6;~XzZ&nIgEc_S&Q_rvdz^{9U zmTLEj!ZLB|Tt@ZuXg?`u81=Fm+@{ZQ&3*%7Wq1M#0)@P27JCqmVS!6|OYbgfZ^oHG zT+4)cXHO=A+I|V3)FYBLvDqf8LIVbydYV?F*`sLXZtd;gB$_<%g6C=V$e@h~0(0-F zSR#Y|L_wvYyDKP!jU%6(BmD{&bqxyLU{V~4+1K=yTZlGZ-bC~JRttcnb1j9q@^^0& zmTZHxJAWgP%z8&@sgY&A%(CjQo^lcLkDubeamVew95?`DztAl-&Wi4{nmlfb zUZS)HdERTOyERGP#0TyB8;pQTvka=<6Ip>P#e(9O;M&#LA~-HKuN1M|MP{Hqo~EBr zq1T;tT=#9s>Z(*xF;E2;6%ZpT>JmtZNeCKgz zcJ={D+yD3beV_iq&fGco&Yg2#=Y8HHRU+>Zyg|S|pzc3S!Cj(rgdOZ;0q$U(QFgG{ zuV@Dw*a1I7W$GUG!RSMZ%Y6p)XW7e~{{#xkTnG8)#nDlAxMe44HDgT6T$pZd3hn+) zISqyrR*l;ve{v7SbuVhf90zVSsC&oHX~tH-`f>$>^gI+T{?Zkfj_6Nf=JV4Q2i-e} zE3SILafr^(%P}9s@?#hmDBkYNu;DS;mpI}nx*_`G){5TYPto9it#zU2X=~vt;peM3 z|0-koRJ;5}JpX3L^7)qxcU5t|1?V4%Cty(ydT96s>|J=*yhFg|;@yH?0XdN%_XtQk zLCOMhLJkRO4hqow>9*->0XvOun+FB(Z47)!0AJ6*hXwEz4E&ASUj{xRfD;(_TLJu? zgMfzw@KaUpEkhw?1vj^YT;yzR|}VLh}IU?CP|Y@eU_mrGeUGjF=myoEv%8FXq1qCm{ba< zuVXE)>HQDzkyl<u6G{7G<=1Cnb6(C3z<$dncuMC)vG|QoWPXypz(slQO-Nvb>XW zypwXhlM1}GP|57AwRmf7`g}_zlwcc!!_olEZ5pJ%h;VPy4*Kh-<9~Zb``i}9{M;5C zH(za~w+SbO>(viof7&v)XG&~5f|Q>%=*N0A)Y4`I(R0gCmAsxX=N^X?T@fF;3*qyWt#xgPLxcMs-br`| ziS2N7JZ6$Q`r{p|YGb5T7=sF*K4!{j@3-7=Q@+(u8`p|Ijt+hjB`w+9HB8v4J^M-- z)b;a+v4_I5kDX?J4^e+7aep1_YAw=7SM+!n;9bAfbJSQnW-WCI8^~@x&CX2@>0D~PA{f62xXz6$~uIAJJSkD(Hx!>F? zBUXs>besFn_YB426{TH`sXpV#>J)c;eK!(`p6(R!5;|L2D&(N5feiTLbkMfyvrGiF7#`Y{A9KEz8tPLo2^$eM% z@z{pXk}DNGX~Mih)mLXIh~{?R0_>$U2GC}bj!H5%yNZz8?>RZT#talgA`-5A2wLBU zTO%zxFP$6`T{~8bd8(}cqXQ6(KMt`y;Pc1gdCodk&$(gb#W}Zywi&UL9HPItT)LWY zS20S>xHUMs-8VQoYO+;Ys099VrGLnDkaQyxkXye4;_%saqX=+N;cdaHGp6GfiUX43=&SmL3Y0t_zkP4wg2Ejza0z!P5Cb*l&WRvxBflf~B*9rQZfiX9i1$ zf~BRw(xbuBLQyJ7J49g)bWC(Xl6tAIn0It%4j0Nn$Xu=ZK(O#3QGH5Adzo18(v3cs zvC1&TRS3^KYjb4BZVQK18%2Y?#xn%#&9kB%7ofG-4OnkYKcOf-3HR$OR)2_V9Va}- zrNjoksKmIc%2>uNuE~-6dMyTN3O7nU+K4^6Dz?Z@j~^LzOmUXEdf`LlwqEh_TbPU| zPz00@-*8-!uJWwO%qguN~qw&WuRwyno-nyi#prQ6P-1{rSx&)tEmu+ax0sK}A-9Lh-!9Da&KJi)T zt6?En-#1HF6_>e^2_#DnM1oM{GYm2cyF=_cBu6AfIJ&D?=PkuBj}us!GPx8j3~-lk z#ewgZcf2Mm71L2sWKrX#mP9ZUYP?i*f`1x%ftJH-Cpjq@+oV$Oq(bkcB91wl@U22Y zMWwFLu%g<3(7t?mwD!y31!f?XkW%v2*b|Xj`fw(Z64GXkUc%juEXz$pgZLs#Mc8}# z`j3Q2@R>jQ*+~3R)ih*|4t+ZqDO^PVI#sy1b<~J#(chv!ZvD;v5!oX0cm7W2Z~r{> zL+5WF^ei}VrB1$+SfXXa{rB}Sv&-%28UcIGbvgppf-wXbtBBE`S1@r_YPSGQtD#l? z=2`$Nuv-B4FphIBlI|*XY;h+P%Q;X%LPr7}(b|WR79Ep{tPQ`Xi^3_=gD?w<9*c#k zkg;C!hs$hiFpkm?i+Yk86&S=c{0z6v)t)-RR#IgylN-MGrGfz~fC`vYhyX+_)|S?u zE0wM>KzArz-Wrp)w1Xr&lL~A0Htu(io+AHo22YZ5dnR@?Lz(VaOMVP@71mxpzsl?* zU&3xV=32r8E)}n32t5qFr9&oME5JV0d;;YuFlO3pN@0o-Fn`1nF<-S*<97MD5%z1jwm69J(SZ=pDc6w*olLh+9s>~(wEh&UPAo8;xMD!&S`eu~YrbvlvXrE>b zCaytJONYc>X-1(`Tjco6+K5)^*$+CaM~S5^-Q|ouDy@C)+oM+;gN|PS$aAd0Nt8OB znHcRun$-_!#EauN8%Fq)82zU-ZpYplgR_A?=QkH>pED`sb51!M=9TkoxT_Gyy3}s> zCa!~@RB!1zlV?()5%*uD1%kUqXuuEL7kF^nkfFwS6u7>&$Y*824-rYv{Ibx+blp494|oEh?@Qukyi zNB`t@c!IVblMgG(GNYbG6SP|s(RWQm-w}sBw~6SxCZg|%J8w0@e}X?`20-wKHyj!a zvSZ7R3@V6AIEl0^zHh~MA^R_^p9RC7CvwZ~W5CxVqSfPnZTzcnz{s~ajte0MpDRB( zI?TB;V*A_zHjNWZu7gJSZ5$9g7ha+Zi2chS$dLiDaerQ@==SI1Ux)0^tRIG6m@fM&9l;ro_W~g%*q$h}6DT+5qiSfmg-J=0?HX1u#h{+`7%Z~#p zLqPxlF(g~zk*CY-S#9)ums;~~#*u8defA2pjDV$@SX+eh=6NJe_T_vn8^1dW3j^sj z`N+wmh`J<4hZ{Jn?E*#L)tr>3C*IX5mtHnPXFUh$UNQ^Yf{~_ z*mG2IyRX=Q?B35kHFme5wgN^v1xLD774&-^J^_rR&-tAvn|>9~!zOIG$Mdk63XGnI zEp!p%dDuo5z;?#_W`F1DNb)F_nl^HhU1jtfi>p!Jp0Qr-Vl1+o>Q^0&sU1`Q#Z9=6 zq21#=JCce%(>1_&J_c34rH&89;`LX29fQ}O;k9~T!ugiwOU!*4c}1yczFGR{;!!b1c=k&`SIEGhty255l=XXKNqKjdXY+|k9!0S>Vq8m@PPVCp(lXdNFt9-VYTQ-jU!)&b zkOIB`n&V{a{sugn3K&E8(W*nJXCiXYc+V=$TIRspN1?jt> zgP!k9k0oos^0BKKY%rbL+y!*Q5oopgN}BaR2>KfAn7`Fo-$Bi8_(tda@Fbw^- z(Klh6u3!gV5-l&}mh<6xgWW&^i*EVi%+O~3`HiB`8)4-kNzy{i9WGS`k~PRCExw=7Ys3;6CTttX~!@nAMeF3UbQK1U|obay{m8bBp9RAIOf0gjh1^;HjzlY$TTjaUU zf`2()oGRDAUclYM`PyJFH|qLF-~P#Ag?h++sM!v4b9S6N5w|Ovu46DuwsqbOc=9w5 z45EADde2at)rY%}G#=-iFNa9ghV|9=Kd_Z%<0_-HN;ThP@hCdRA9rB0ikTmnQRw7v>YhVIwHrgc!Kq6QT*up&fMq{{J zs!j(b^urOvsa`*PB9`^T2lqx1fjABaMIc*_42&cKSr+|q>%-p-F9Olz8#6D{+poAv zGPVQV?0WV0ls>cT3FKmoT~7@15Ard_xi1nU^CB@aFA^j3A~7;A5+n2CVdBSbj;hc) zt2oqW$|5YzAH6;3wVAv#P`ooBZPBcGlkIW>V59H*XpD?L@@6$}Dq1g^g)_jvVcGP> z6Ua{gl&QYAait=wi?Sa=`-Ua_=h40;`wf~m`lUxoQa=ASX{-ry*VUFRZOjPpUaiP0M z7t0xUijxND+x^+F&i{{JTYD+5f0WS3u=Zs-TT_t#SnjCbJ^BUM3<^A@ub=UnAi)L=nK%f4-ng6Q5MVjArP5J z;{4qxTHgo|ljRf*A^`;w#h(KRx&hK_IswEfpHw?geFlYly%J#_QeU)^UKG&5U>M=i z2velK>Ro!(dpej{fcbJaz@)10T0-yooestXFdYbEQr~rA-9&iT&)4WbY70VqZV+!e z3=os$J3=6)V_(`0;yvvjBI>>$0&!n2K(rXdThiZ1hqsK<(B6nfOr(VI-BsDu#JgM* z?{ZDN%bh|UN4wmmH%8s%ieP@Sf{#_IC`xDbzuq=L_U|HD15Tn!nz#G5T43{DX1ovj z?9?V^`6{!41;<!trC^nJp$b5kSIn5$eqTtHP~h}W^bE;gyz z>t3F8NRi&Bw;XYwqh?T+9!LImD#q&GB9BVaj7Zqu%l8h#qkYN73lAxt8x8IYVDV9N zE1&=s5z>TWP7^FY03FdxP+Awh7V>@H!a;$1t2=IDJH~{7FM^2@x6BAPPu6w7qjq`J zksiBI%a@Dhhnk1eC}nFj`>#hwJl`=mCE0&{HW*#a)$o|Xn2VwtpV8l~?L-fb3 z74hethW%IZ%ZP8lYL@i1{PvyjW*&gDLMH}p<`|pqU#o*(<*Ti9*`6Uc%sfX_f2sR# zQ`15`(=iujgUi6+XF8&IETVj`*ePcO>#UMCPnMf7O;Gb{B%muoP4-cgSl5i>VlKnJ zyvmKgcYV~pY%kRHrFlLYFD~Q?4cABYpL{b|q1`o)K<7674vi^Gu8->TbuUy1w7g`d z)$FR_#xINB_?`O3XNkuDf{h=^j@tOiTe0z1xp6g&+W5M*-;q7GB@pL=Mi`)Eif`#+ z_?_aagfdqtcVyRZ6AZwrrvwi87hrf9k&_cscH-@hxOEuSW4fWMX$dx|W(3JmUy|yDLad z-S9CrNPSw{G|$YIGu8$=3WTkxEBzU^T$k3te*U#;Z90Lh(VeP2ssMQXK29qXVsP`W znHw0vch&}E2r#^f8pE3?-1c1yU-jDLIlY65Yi=$12wu!zX5%lzV4E*LE-U>LNyUt} zzsK8)Ly`^0W<-B#6b_;zcbB3;1O2YO{iDyh^wC6QIDN}#*V@-=tuQ_5V1 zdoZWFT(E0UU7^8zUg?3jMYAT_0t+nY?Y97B*nyTr{2P}Wa}AUyQF$_zV=y+U32k9n z75bxm=33jxJeG`#y<{;TvICGZNTQD;r&`?d<#A}2iA8|$bAp(lBZN=i+~G<-dr^-E>QH9xJb;7cCQ& z?}EFG?b$7OUz_5qv>Swzgz1k6d^J1SI9QO$y;vf8 zaSNfKQ*`1J8@zUZ_ENmB&B)j;H6u=?#IXqa4;zAFfv+gmgl}_UH-M*EZF5kSnk~|T zZKC0MwnK_)tE29}fu^Sg`YFl^=CQ@Fwp-+{_9hqtO$*KJ=t$vY6Y&L~Q2C-@c`14U zZ;?NQa`so@`fqc6V|e{%seGO>y#5@jkE2>;!ur2}dzdYHej7l|->w?&gom?Ys&vg{ zr!*;7q#EGI?lWNh!BH|9StgV6d9cMg_-JYaPp&|o#b0O&82qDmbp-N~{Do$YwHe5> zVR@d(j|F+j>I0~dNDs{XAcY^0Bc3T><48`ntFF<IUDu zKH|kTeDV6o_nQ2-B}RRzjXVNPfoUdng?R=nsA7BXf_*4rJqO60cs;qJhR<3}hwmQV z#IrJ@9=14pZoTx6z^GO^p=VkT>L_5v!Jj>*pLZc+l?vm z%Z~F4J;EjVk+B>*s_#jp>bG%kpPU%Ax4+9hWdIKg9`T*eP~-%QE^>lRzII~NW9)|9 zV1jNAsA2J=d~X6Rn=rQoaeR^FLOoGnqrKwOK_w738$O9j{aiZ!(?O2p6vaN3P9XIA zf9V==1ksn`nR7vntjt)g9YI#Qji`U{8BT^YGot>kM?>mwc{HNFSYD&O^|-9j@-H_u zyVmbSxqt^^`Sl25nVd|2HTRgZHr})#!2{lzA|?5hH+ z(o35gJ7;;xdCJ?gjsD^h5vH+L;{oH2M$w&U_rmB-i_FpD0sHC^GWgBBUdq@zln>EA z2V}*`2`bSqbgL7s^*9^UR`d%>bjrV37K1URTAn7-&IT{82(+kgZoxOJV+;7%7Kxt{ zR;Zg9KfIfx^j9@L#q;z`2cM@cgpC%$Mhjv?B5XuSD|AyfCsK7^J1mGk^H2#V9=iVH zLB-ob@PuICd&g&;k5or>LX}kz8DGZti9^S+x(iKWv4;;`KWq-W;P981Vfd=I}x>Zl$oWxhw5FOO84 z*J2$*|2XL`n6Ex{zJBDjVIcZxCX74}Tjs5nioi6B&Yyw#z5WsTJ-_hxN%<&>JoosC zpgdRU9g)-PjsCdxw#4B%JzPnxCw2A`-8~`p643dfYa=_K2y|A){xa$4a$M_Z@k1+c zmOTajC1SpC=?Oe{9c$n^t$Nh*@`V|;{)^B6eAMzHyqn(r1QZf9T0058Q=wqE1rtnS-u(QsKE<)deIcMz+UBjaCrxqX-V4;- zy0tMs!45rQl0W_DAge`E957vK+AvtKV~uLE=00C4JmGujge(03Ye1C0n102C170WT^2tG^nSK3o0i4SKMI1_*wId(h!H2KPdU(|Ks)!yZ-zkRnK7d2M9PDmE<9p0Mb5FxmoP8u-%yJd=fdl!-Y_zQ?T%p% z6+iC}NQU2%#QonHhhDZ*VYn0#g8r6d@f~-jh?ivwjFw^u3V~6KU6KRWX&6>R`wnRf zCWI`v5Nfu#ZhMSf) zbrkhu2t)=u2&f}jQ^lh==SN6?lnWPIT!_gbTTn{)7L*3)x5E|$T%jV}C_Rgo>#+Gy z^=Ie94en7X&M9bWKmnBc&mnJTjOX;DedKV9MjKOvd3~0P#_-~k#AV6`+c%|hVlTgHoTNvJYR75>hLWyLk~mn4k1Zo^<`+mw{g%t`rb&kk-& zF6l;qDANQSVHnF2Z%OCLE#ku41W0)ycVuQEc0`2vFv@S#YTud~3rGjX`1L?XFiBGC$H| z4`R%vC1<@+Dl4xt($Sdc6jZjJ;+CF36ipHHDwOH!x#Sx51qWt!;X+~$e`b%)9!?Af z7aT<#R(~8ZO7ivL6;$r%od;ot&f`|chXZZ93Emv$;_h4)CbpD@_<5NoxZFR zMA`b~7}O()8N(&86o6@;po_vrjz27`vsH~D$Nj!rjH(~MsON@y>cdp>fit>EA$Ae< zkH;HX3wgsg(TA>A9lXyB`oXIcgLl@&BMu%)dQLrfMPl%x4rZqqOidy@b78tF5&mYW z9<@;r+hC$CBm}V!wIX7tF@j3gs{&73|2P*)&($Z{@I(X?m_?Q(S%0^v%D7pqzw0Ee za%GUx^mJA-4?+42{hBfkv!*u7#fKF|pxoN|huaArr^Q3Wxr33Yqxt z3rvQ(*YN|t8bJnPVhMPd%te!cKL)sfuEuBWgIVY&y$DNUrXU61S-jRziC>zFUJnKq zt72WE={L*2KpeOIg|_Pl&IUqkFHW9pq?QT^g>13+QA5i z;^_V?=tDLoW4~h!hOm!E=aT-j@C_Cgr0h-@yAIFs?4`&Kmy}r>zr^orL-~l~Yg701XQov~k9 zuuK!O_$>3$D+kRwRp|&dQgI)=#YB0F9cy4%zTZDCT#`4=Lqa$sNnNJv9C-9XvnqwB zwrfJF-k!`>GDVtYPi4m*R_1Jy4F=MdTu93NL`>3sK%tE@Q94^y<}U)W&qqrdpKpfe zKGN$J!<`N4`xg9;G#8uX7`!7*1wtm`UGqx<@?)$gR5+VtKi&xq&L(*SzEcRGaIS2) zOtbxis*w^X`8KRjU{80fMuv9IRZ=rm?nZY#-(}>kmq}u-@S1&xaDp}QWZOsAG%z#; zfHJqSd=s6)#ia9xPr6B|K~KJ9^5jc31Sefdm8nerM;}c(#Y^T_!6ZDkHjJff3Q9!vQI^4iWgMDLb!cp%L-W?9;ZByRM*Yy(%H#`W9vV~;cIpO& zb)eSfM-6Q1)CwoRa>Xj&?G~J2{fNdSsrayTEVd(-gsS!XwAK587^|EQXsi^SWV6g9 zh8NZ^*OS7?@`Abl3uU11R6jI~bT|aZqP!o7+ z@ivooF+BrQKH~}d2_pQ46N8#^ag+&%Qk_Q1?X~8L@|oeWn)A!3fAR+Z%KgB#ChdTmexr-JO zXwQx_|0bK9z^%4tdcCwRA}lZk1m@Vl!LYzw`BY(n6a7V+)FzR{F66P0R~?VC7mQ%rxZT>Y z6W3oXe^>ViADOCsWSaJoD40nj2_)to`~T=is?IU$1WK~b>!utrK)k>3s#!G{x{?TQ znwGmn6QeyJ5MA9riZ6`S<|(kIANxu{!vBTEYF(>T$D%niPjONIk%@I5kjYVXWmh9! z7*#^NRBA+eKqJTB8juxqX|=BwHQW=W#(7M=(>l9Yt$CK@qnd_duK7!-$&DMh0bFnF z{`uQVSMGDj8p$<@M898(#LYAl99GG&>fg9@A25Cu?MwJ0~V#yhO3g*+YHY_)gEH!3|CZ)pFY@OGH2S2aq zT8t}5uh=AiaTay#GI(1Ob_cp;qi(e?RJ&!PZrP?|2&TFZnQgKH_Fq)lu7XGGm`uj? zIj}N!7Z$r_K9P=1%lQcgSKa-@a?Sd1P(eFN_aQ|oApqaqm2#H`u|>WeAf{_ko;3e} zYeVQc`xMrK(qXu@QK`K_T8gK@eB2~PRwFuqb+$IlKq2EQqi2oH@kz}FIP`KGx0Y0! zGq$5$djEJSKb-loO93ecExn7S4U#icgm+Hm(9XCFF@6zt0oS6naWrmNZKxu43;}}_ zp(5^+iSXwGe+OL$Es&7x?^MbDuw%aS0yySnJmzP?X}KSc@EjclR#s)3B4_o|F~8qS zC{Yb_5RxdcUy|#d>T$-0=iP^6JEYOlNA;f*=8h(0Tb>aId*U;+p>Ne4?6*g#EQdU6 z=wM$9*L1MoN(Vce_dGAruS6g2kgbhnwCz)@ROF&IYFyMt9T%0c9{8xEbJBzwx5GWw zDKAZCmSVk^2_vEjyv>Br7-JBjM-ZVWB_^|A?>RMEe|~-tt%;0QV;EX@?-~)UZr>f2 z6+}3}3yE6tna2LFYC-8DWWji!}UnpIz zzWlcEA;qJT0(Ymf=}sEF*+Y#RCAU2{QkZQO!t7C9P5UorHZ5H*fJ!G4q<#P$++b5& zNltBziuAR$0p%v1w#5dPG%?wL@?z9tu;!^59|m$0tHc@45&bDxdL{=_7b`J~1F5HQ zkoB~HfpQ_HLvwU4a@6`X5w#xMc2S`6a{H_REAueXXD0l*g`#7&2;W*m7+GYmBk6U( zKt0KkHvc!V-RW3IO%$aSq%5JvDuX|TY)nt?UVapKp)-cyO{s>I3i zz5ps~7YiBFOw!aspD%QFBwc&zuUic(R-Y-6cJI5lZ~s z#qchCGFiS*eNgW7v2!9m>!fF~@C+smJj}vcGw}&_Z8Gudc+7m%tDwzTRb3ky0K0Ej zg}`~z*-{LukZE$3{-DuTbCn}*8vM?=G!XZ6K?bTO^Jlu|pZ6pgIk8C6DHJ_bXT`A^ zdMY+ivCKH5z7xN2mDx}+u|0Uh`~u})7Z0cP(5P%#TIBMaJNguiUSJ-Lt1-tS%%WsUhB{p*r?*I5StnKzP*YPWRllZpeN3) zP$oiE_TBewh(CsBzzj#L8~D9G$2ZkuW=w3K?&&nINhfkMuyQjkIp+-i~5 z1&sFzDi25^%*SvmEFF_pM2TK+MSDc8=p8H?709y)zhSHNA_1WxrB+1ADsM#-O0DSO z7_#_E3K(hgAie6&Qd+YCEr<55Rq!e{1%TV`T^;^B3vF5l{H69}mQJI2V%$HBwD4&^ zLeM-k*h{DCl!I#(c5s!@+^*MGRs)TBjv1@R@BFl4=chAv`X{#U3{2nY=h1TXSR4Or zP;t2PQ^^SboH;;&e?SF+DrKi1ka-a8#&XzhEHH_XsbNV%W@AzaE@x9w2rhquMe%xE zmJm<~E_10U1eaHm*XlB(m|lTSk?9l}wRHdIf^Ez6e?fbD5aEO+yNab$4MCV)=JU^b zcYcZt#MKDay@T?Qn*EzNFDMyUh_QgnU4u{)v!Nu59PP6Sp!EZ# zYQdoiaH^KfopFWFWvDi@sbpn34%I5gF#S_TsRf=ui9D+S+9xuj1rW3L>L`#auQTy2%`f!2t+V*aSr5? zktob*JSj^lDVEtYV8OgAW~tMUau+i^&GkAlu6o+84sRjervr?NN2wF2g3Jc-q`3s2{-Yk+X+?0m==^n4dk%-=c{ntkMlJoHEiwDg>Za(SSr12aJ+m8c>w}UwGPB zKJimeo10jO>8R~L$j0E0nNDSdQK|G~H(|xhjI}Y(sChG48#0ZmMXs+Uy3s9b)Ll!SV~yj6SbF=ztVpkW3>#@|g2(&l3V0;1NM_O4UOm`JaXB?Pit-xCQLv|LIcNu|$6J3SD^NL-a+(SAX+_aV&IXd?X07Hn?Lo^B8O6{jo$nkv9JfHPud)8S%(u?T_Dpk0vQ2Iy2U-XdO?};a!#z z<B$RheoMZC#NfM@~#vYDce2+eb&Q_y~1O zRMw6ezEgxZ1MFj%bkxR407;T)^NXCdBt<+Qu_Y1$;vOLH*KAco4g_NK_|y{zLRRR3 zK&N-$fH=7ew3E9~oZLms*uj=rKs$CSGf{;?!UM+!@G!h*leiVCWs&I}rA(#G5E{~n zTk%lICHn}~AWl`ipj5)z{EiU?2`F;tj~DT_h-sHBsj+J!r7`ZQFn;r9&F%#0;R+izFd6y?r>{By$wW&uElJFdLyr zaOyiPu>7S_7;TIK9soH2+jucij;J5vju+VzeFbJyAPFf15+0aXcwi>AFCM!LTaJZo`u_er#F?!hce<7VWb~Cge+e5FB-Sk+@^l=V>jtPxBszpV2v=kwvcC$ zU?{emKKBW0(V21A zcOv7<`J&`de{Z(ezaw=-dwt=b{m{!%{j={WXaDTdKeB)JZSRa|wSRf^$F0iEr{kY3 zdBY5W!k!bmyGQR&|EYga2{iS_Xs&{z1A{?@Q!qaTRzmnEdk2;Ju3q6!zp{5QBwvH8 zauMv_3mekK7QW~u+QKv1ML$}5Uk{BJ0IM4fOVxyaQb%3u_t5(0$s)UH?%a#uvU*fk z9sU}v@6X3yX)b@_WaNA-$Wsc@|H0L=67GTXWPM7k-v6dG^^K7CqdyMJR+Cfunv;Zf zE}~;|EqyN;u>ILVPSKZ22~K!RnUoD<`mY2S)A4xkly0qby#$pe!sesjjdsoY0 zvP~tbtp!u3)4thqB1UZ2oY_qlIpSLeM2`5DgGiCEpDelb1o1Bm{QvtC#Q$M|{~ezU z4wH{og}?nXJ$%k*q41u8B=XS2=RQtUnc~zej?IB4>^p6;7Ju`J7-0oXjr&nagF9u@ z0N+{YoM}49cNE`C-f`_dg^rgNoH6i8NueXu`p_HU`C}OMcoM27>;;<2rpiDL#qu{V zYwpA|R-@S!Y9(jDBx-F9AKxUA9`~WjR`lIhMs}EtXi8qP5s!S%}t>NEh(INwGZE>{)<^Y@)CEFi(Z#VrY+C z@BQ7bZO|RyZ?PLuJl|CXbzY$#V3IC=b=Z7)@?W?g91iXWN4Mbw8WI!e_`gCXP-Oj2cc68Js4p+tp|52;Z?l1N7EXk<6gk!7T!)E+o#AaMSP&k&7s02k$S(KTPXbMZl zdKU89Sjd~mLf#}6@+Py8H$}K_*o5u8MO9qJEhjM#8tzvjRWJ{p&eJ`Op{MhbdGN44b4Kwb)`dQ<~qT_@am zMyd1}J0^}&lL^x;NpdNG;NHpvL*LUuc2T6SWIr9;8Pp_~Oo^F<4+JJ57A_DLA=Xb9 zV*Jbi?`%HYe8G{L6@!C8$ zs^sK5*BwH7$2nFPVy?ckbgXtHviy68AP6WN_4*zlW10m)Ag{o?1-Q7en}2~jB!Th1 z_CmBjq@6;tEJWC+ddPOiL62+pNDGEUwbwtvJq349v8TM&g9YYb!P8V=2^M&$ATe0* zE9^*9a;(VzrQHo1!@&q3*7uuJxCwbO+!hDuY~cpKN6!}^wL$sC=(g!1@q&vP9Jh4_ zAKWr1kc+Kt`xwz;5Oo)9!F>%S@+D+|1XRM;a>t>)IyMKR5|*JQG2*7mjITKOT7p4C zrd`ncv~x;{&sC*h!7ao0(q$= zxN}UwZN-I4BamAOYp=-PG1(Otmtr8tF+L7l{tnha&Y}z0Vy5|b^Tt(97wLZ2OjWIy zCDf9+LhV=})RBckJy|4nBd6GnO67m-fF-GRO7i=kz*Ug3zj6DHbz&bR(X6%k7e+kO zik1{LuD3Q!MeX<&Yr|ded!)V{@Mp3%pkPQZeFoCH#^Q)u2fx;4J4HrYQltYQX35OV zQtiyJhp!Iu!^QAhdvo1~G)TgxDHPH&tI=kiM;797HoShSRRDRG3;oXj6LHG@Ue_$mPUq*W3%M^99LXIih-93`zL;SNO28qqM&h-ET1*X z(zxH+ypZs?(&pG~ZKyXyj<%UcyViw3!;SPEVGk{nv+1D?ervD7hk+&{R$&+xp^Jxt zTGbL4CQP3?6-R-p=cm9msv=KX=eZP=+D-?~*^sMeuY~<2Zhxx|!1hPsz&Kb-%*(h9 zh4W`pz068ElT&tX7>wZG>N{lGaf|F`*|s(ExoYpJf78O363{0{&cz-y(|((RJ7e8T zkv;GX%+mOuGJlbMHU;fRFE=7IC}})RA4(Zd^a16Hr=?GOIne{e0?{@=?2p(8aAFq9 z5BxR(k9i>ead*1nW8c$b%qJD)LtJ0;f$sBkSOR&6J*zC9uZ)4b6EMkvytgpb&&
ob@ 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 z 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%0A|ME_vRh|miq=G@r)<%XXC#W+ZFv=RE&V?M> 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%0AYVIRwWNf0qIRDia1m_(2fFLK?;tF+4zM{l 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%0D^i%5`~}7B!aq3D#83f@sqy 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%0D$$`Sru)Kd=6VvC2R3#p|CjR{5uS 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 bO!F=zSjxH>dngZ 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

    i>0&DgR7q8hzs*h8Ahmfo`UBQR(r& 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", NULL); + +//----------------------------------------------------------------------- +// +// hhZone +// +// NOTE: You do not get a EntityLeaving() callback for entities that are +// removed. Could possibly use hhSafeEntitys to tell when they are removed +// but what's the point of calling EntityLeaving() with an invalid pointer. +//----------------------------------------------------------------------- + +ABSTRACT_DECLARATION(hhTrigger, hhZone) + EVENT( EV_DeactivateZone, hhZone::Event_TurnOff ) + EVENT( EV_Enable, hhZone::Event_Enable ) + EVENT( EV_Disable, hhZone::Event_Disable ) + EVENT( EV_Touch, hhZone::Event_Touch ) +END_CLASS + +//NOTE: If this works, this entity can cease inheriting from trigger, just need to take the +// tracemodel creation logic, isSimpleBox variable, make our own enable/disable functions +// touch goes away, triggeraction goes away, much simpler interface +#define ZONES_ALWAYS_ACTIVE 1 // testing: want to be able to use dormancy to turn off --pdm + +void hhZone::Spawn(void) { + slop = 0.0f; // Extra slop for bounds check +#if !ZONES_ALWAYS_ACTIVE + fl.neverDormant = true; +#endif + +#if ZONES_ALWAYS_ACTIVE + fl.neverDormant = false; + BecomeActive(TH_THINK); + bActive = true; + bEnabled = true; +#endif +} + +void hhZone::Save(idSaveGame *savefile) const { + savefile->WriteInt(zoneList.Num()); // idList + for (int i=0; iWriteInt(zoneList[i]); + } + + savefile->WriteFloat(slop); +} + +void hhZone::Restore( idRestoreGame *savefile ) { + int num; + + zoneList.Clear(); // idList + savefile->ReadInt(num); + zoneList.SetNum(num); + for (int i=0; iReadInt(zoneList[i]); + } + + savefile->ReadFloat(slop); +} + +bool hhZone::ValidEntity(idEntity *ent) { + return (ent && ent!=this && + ent->GetPhysics() && + !ent->GetPhysics()->IsType(idPhysics_Static::Type) && + ent->GetPhysics()->GetContents() != 0); +} + +void hhZone::Empty() { +} + +bool hhZone::ContainsEntityOfType(const idTypeInfo &t) { + idEntity *touch[ MAX_GENTITIES ]; + idBounds clipBounds; + + clipBounds.FromTransformedBounds( GetPhysics()->GetBounds(), GetOrigin(), GetAxis() ); + int num = gameLocal.clip.EntitiesTouchingBounds( clipBounds.Expand(slop), MASK_SHOT_BOUNDINGBOX, touch, MAX_GENTITIES ); + for (int i=0; iIsType(t)) { + gameLocal.Printf("Contains a %s\n", t.classname); + return true; + } + } + gameLocal.Printf("Doesn't contain a %s\n", t.classname); + return false; +} + +bool PointerInList(idEntity *target, idEntity **list, int num) { + for (int j=0; j < num; j++ ) { + if (list[j] == target) { + return true; + } + } + return false; +} + +void hhZone::ResetZoneList() { + // Call Leaving for anything previously entered + idEntity *previouslyInZone; + for (int i=0; i < zoneList.Num(); i++ ) { + previouslyInZone = gameLocal.entities[zoneList[i]]; + + if (previouslyInZone) { + EntityLeaving(previouslyInZone); + } + } + zoneList.Clear(); +} + +void hhZone::TriggerAction(idEntity *activator) { + CancelEvents(&EV_DeactivateZone); + // Turn on until all encroachers are gone + BecomeActive(TH_THINK); +} + +void hhZone::ApplyToEncroachers() { + idEntity *touch[ MAX_GENTITIES ]; + idEntity *previouslyInZone; + idEntity *encroacher; + int i, num; + + idBounds clipBounds; + clipBounds.FromTransformedBounds( GetPhysics()->GetBounds(), GetOrigin(), GetAxis() ); + + // Find all encroachers + if (isSimpleBox) { + num = gameLocal.clip.EntitiesTouchingBounds( clipBounds.Expand(slop), MASK_SHOT_BOUNDINGBOX | CONTENTS_PROJECTILE | CONTENTS_TRIGGER, touch, MAX_GENTITIES ); // CONTENTS_TRIGGER for walkthrough movables + } + else { + num = hhUtils::EntitiesTouchingClipmodel( GetPhysics()->GetClipModel(), touch, MAX_GENTITIES, MASK_SHOT_BOUNDINGBOX | CONTENTS_TRIGGER ); + } + + // for anything previously applied, but no longer encroaching, call EntityLeaving() + for (i=0; i < zoneList.Num(); i++ ) { + previouslyInZone = gameLocal.entities[zoneList[i]]; + + if (previouslyInZone) { + if (!ValidEntity(previouslyInZone) || !PointerInList(previouslyInZone, touch, num)) { + // We've applied before, but it's no longer encroaching + EntityLeaving(previouslyInZone); + + // NOTE: Rather than removing and dealing with the list shifting, we reconstruct the list + // from the touch list later + } + } + } + + // Check touch list for any newly entered encroachers + for (i = 0; i < num; i++ ) { + encroacher = touch[i]; + if (ValidEntity(encroacher)) { + if (zoneList.FindIndex(encroacher->entityNumber) == -1) { + EntityEntered(encroacher); + } + } + } + + // Call all encroachers and rebuild list + zoneList.Clear(); //fixme: could make a version of clear() that doesn't deallocate the memory + for (i = 0; i < num; i++ ) { + encroacher = touch[i]; + if (ValidEntity(encroacher)) { + zoneList.Append(encroacher->entityNumber); + EntityEncroaching(encroacher); + } + } + + // Deactivate if no encroachers left + if (!zoneList.Num()) { + Empty(); +#if !ZONES_ALWAYS_ACTIVE + PostEventMS(&EV_DeactivateZone, 0); +#endif + } +} + +void hhZone::Think() { + if (thinkFlags & TH_THINK) { + ApplyToEncroachers(); + } +} + +void hhZone::Event_TurnOff() { + BecomeInactive(TH_THINK); + bActive = false; +} + +void hhZone::Event_Enable( void ) { + hhTrigger::Event_Enable(); + TriggerAction(this); +} + +void hhZone::Event_Disable( void ) { + BecomeInactive(TH_THINK); + ResetZoneList(); + hhTrigger::Event_Disable(); +} + +void hhZone::Event_Touch( idEntity *other, trace_t *trace ) { + CancelEvents(&EV_DeactivateZone); + // Turn on until all encroachers are gone + BecomeActive(TH_THINK); + + bActive = true; +} + +//healthzone begin + +//let's specify our new class with its parent class. this hooks us in with the rest of +//the idClasses. +CLASS_DECLARATION(hhZone, hhHealthZone) +END_CLASS + +//our spawn function is called in sequence with spawn functions for any parent classes. +//in here, we can optionally parse any extra spawn args and do other on-spawn logic. +void hhHealthZone::Spawn() { + regenAmount = spawnArgs.GetInt("regenAmount"); //the amount to regen each entity, each frame +} + +//write all of our necessary state data for savegames +void hhHealthZone::Save( idSaveGame *savefile ) const { + savefile->WriteInt(regenAmount); +} + +//loading a savegame, so load state data back in +void hhHealthZone::Restore( idRestoreGame *savefile ) { + savefile->ReadInt(regenAmount); +} + +//let's only accept players who aren't dead +bool hhHealthZone::ValidEntity(idEntity *ent) { + if (!ent || !ent->IsType(hhPlayer::Type) || ent->health <= 0) { + return false; + } + + return true; +} + +//a player is in the zone, let's give him some health +void hhHealthZone::EntityEncroaching(idEntity *ent) { + if ((ent->health+regenAmount) > 100) { + return; //let's not go over 100 health + } + + ent->health += regenAmount; +} +//healthzone end + +//----------------------------------------------------------------------- +// +// hhTriggerZone +// +// Zone used for precise trigger/untrigger mechanic. Fires trigger once +// upon a valid entity entering, and again when a valid entity leaves. Also, +// optionally calls a function for each entity in the volume each tick. +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhTriggerZone) +END_CLASS + +void hhTriggerZone::Spawn() { + funcRefInfo.ParseFunctionKeyValue( spawnArgs.GetString("inCallRef") ); +} + +void hhTriggerZone::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject( funcRefInfo ); +} + +void hhTriggerZone::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject( funcRefInfo ); +} + +bool hhTriggerZone::ValidEntity(idEntity *ent) { + return (hhZone::ValidEntity(ent) && !IsType(hhProjectile::Type)); +} + +void hhTriggerZone::EntityEntered(idEntity *ent) { + ActivateTargets(ent); +} + +void hhTriggerZone::EntityLeaving(idEntity *ent) { + ActivateTargets(ent); +} + +void hhTriggerZone::EntityEncroaching( idEntity *ent ) { + if (funcRefInfo.GetFunction() != NULL) { + funcRefInfo.SetParm_Entity( ent, 0 ); + funcRefInfo.Verify(); + funcRefInfo.CallFunction( spawnArgs ); + } +} + +//----------------------------------------------------------------------- +// +// hhGravityZoneBase +// +//----------------------------------------------------------------------- + +ABSTRACT_DECLARATION(hhZone, hhGravityZoneBase) +END_CLASS + +void hhGravityZoneBase::Spawn(void) { + bReorient = spawnArgs.GetBool("reorient"); + bShowVector = spawnArgs.GetBool("showVector"); + bKillsMonsters = spawnArgs.GetBool("killmonsters"); + + //rww - avoid dictionary lookup post-spawn + gravityOriginOffset = vec3_origin; + if (spawnArgs.GetVector("override_origin", gravityOriginOffset.ToString(), gravityOriginOffset)) { + gravityOriginOffset -= GetOrigin(); + } + + //rww - sync over network + fl.networkSync = true; +} + +void hhGravityZoneBase::Save(idSaveGame *savefile) const { + savefile->WriteBool(bReorient); + savefile->WriteBool(bKillsMonsters); + savefile->WriteBool(bShowVector); + savefile->WriteVec3(gravityOriginOffset); +} + +void hhGravityZoneBase::Restore( idRestoreGame *savefile ) { + savefile->ReadBool(bReorient); + savefile->ReadBool(bKillsMonsters); + savefile->ReadBool(bShowVector); + savefile->ReadVec3(gravityOriginOffset); +} + +//rww - network code +void hhGravityZoneBase::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(bReorient, 1); + msg.WriteFloat(slop); +} + +void hhGravityZoneBase::ReadFromSnapshot( const idBitMsgDelta &msg ) { + bReorient = !!msg.ReadBits(1); + slop = msg.ReadFloat(); +} + +void hhGravityZoneBase::ClientPredictionThink( void ) { + Think(); +} +//rww - end network code + +const idVec3 hhGravityZoneBase::GetGravityOrigin() const { + return GetOrigin()+gravityOriginOffset; +} + +bool hhGravityZoneBase::ValidEntity(idEntity *ent) { + if (!ent) { + return false; + } + if (ent->fl.ignoreGravityZones) { + return false; + } + + if (ent->IsType(hhProjectile::Type) && ent->GetPhysics()->GetGravity() == vec3_origin) { + return false; // Projectiles with zero gravity + } + if (ent->IsType(hhPlayer::Type)) { + hhPlayer *pl = static_cast(ent); + if (pl->noclip) { + return false; // Noclipping players + } + else if (pl->spectating) { + return false; //spectating players + } + else if (gameLocal.isMultiplayer && ent->health <= 0) { + return false; //dead mp players (only the prox ragdoll needs gravity) + } + } + if (ent->IsType(hhVehicle::Type)) { + if (static_cast(ent)->IsNoClipping()) { + return false; // Noclipping vehicles + } + if (ent->IsType(hhShuttle::Type) && static_cast(ent)->IsConsole()) { + return false; // unpiloted shuttles + } + } + if (ent->IsType(hhPortal::Type) ) { + return true; // Portals are always valid entities in zones + } + + if (!hhZone::ValidEntity( ent )) { + return false; + } + + return true; +} + +bool hhGravityZoneBase::TouchingOtherZones(idEntity *ent, bool traceCheck, idVec3 &otherInfluence) { //rww + if (!ent->GetPhysics()) { + return false; + } + + bool hitAny = false; + + otherInfluence.Zero(); + + idBounds clipBounds; + + idEntity *touch[ MAX_GENTITIES ]; + clipBounds.FromTransformedBounds( ent->GetPhysics()->GetBounds(), ent->GetOrigin(), ent->GetAxis() ); + int num = gameLocal.clip.EntitiesTouchingBounds( clipBounds, GetPhysics()->GetContents(), touch, MAX_GENTITIES ); + for (int i = 0; i < num; i++) { + if (touch[i] && touch[i]->entityNumber != entityNumber && touch[i]->IsType(hhGravityZoneBase::Type)) { + //touching the object, isn't me, and seems to be another gravity zone + bool touchValid = false; + + if (traceCheck) { //let's perform a trace from the ent's origin to see which zone is hit first. (this is not an ideal solution, but it works) + trace_t tr; + const int checkContents = GetPhysics()->GetContents(); + const idVec3 &start = ent->GetOrigin(); + const float testLength = 512.0f; + idVec3 end; + + //first trace against the other + end = (touch[i]->GetPhysics()->GetBounds().GetCenter()-start).Normalize()*testLength; + gameLocal.clip.TracePoint(tr, start, end, checkContents, ent); + if (tr.c.entityNum == touch[i]->entityNumber) { //if the trace actually hit the other one + float otherFrac = tr.fraction; + + //now trace against me + end = (GetPhysics()->GetBounds().GetCenter()-start).Normalize()*testLength; + gameLocal.clip.TracePoint(tr, start, GetPhysics()->GetBounds().GetCenter(), checkContents, ent); + if (tr.c.entityNum != entityNumber || tr.fraction >= otherFrac) { //if the impact was further away (or same, don't want fighting), i lose. + touchValid = true; + } + } + } + else { + touchValid = true; + } + + if (touchValid) { + //accumulate force from other zones + hhGravityZoneBase *zone = static_cast(touch[i]); + if (zone->isSimpleBox || ent->GetPhysics()->ClipContents(zone->GetPhysics()->GetClipModel())) { //if not simple box perform a clip check + idVec3 grav = zone->GetCurrentGravity(ent->GetOrigin()); + hitAny = true; + + grav.Normalize(); + otherInfluence += grav; + + otherInfluence.Normalize(); + } + } + } + } + + return hitAny; +} + +void hhGravityZoneBase::EntityEntered( idEntity *ent ) { + if( ent->RespondsTo(EV_ShouldRemainAlignedToAxial) ) { + ent->ProcessEvent( &EV_ShouldRemainAlignedToAxial, (int)false ); + } + if( ent->RespondsTo(EV_OrientToGravity) ) { + ent->ProcessEvent( &EV_OrientToGravity, (int)bReorient ); + } +} + +void hhGravityZoneBase::EntityLeaving( idEntity *ent ) { + if( ent->RespondsTo(EV_ShouldRemainAlignedToAxial) ) { + ent->ProcessEvent( &EV_ShouldRemainAlignedToAxial, (int)true ); + } + + // Instead of reseting gravity here, post a message to do it, so if we are transitioning + // to another gravity zone or wallwalk, there won't be any discontinuities + if (gameLocal.isClient && !ent->fl.clientEvents && !ent->fl.clientEntity && ent->IsType(hhProjectile::Type)) { + ent->fl.clientEvents = true; //hackery to let normal projectiles reset their gravity for prediction + ent->PostEventMS( &EV_ResetGravity, 200 ); + ent->fl.clientEvents = false; + } + else { + ent->PostEventMS( &EV_ResetGravity, 200 ); + } +} + +void hhGravityZoneBase::EntityEncroaching( idEntity *ent ) { + // Cancel any pending gravity resets from other zones + ent->CancelEvents( &EV_ResetGravity ); + + idVec3 curGravity = GetCurrentGravity( ent->GetOrigin() ); + idVec3 otherGravity; + if (TouchingOtherZones(ent, false, otherGravity)) { //factor in gravity for all other zones being touched to avoid back-and-forth behaviour + float l = curGravity.Normalize(); + curGravity += otherGravity; + curGravity *= l*0.5f; + } + if (ent->GetPhysics()->IsAtRest() && ent->GetGravity() != curGravity) { + ent->SetGravity( curGravity ); + ent->GetPhysics()->Activate(); + } + else { + ent->SetGravity( curGravity ); + } + + if (ent->IsType( hhMonsterAI::Type )) { + if (bKillsMonsters && ent->health > 0 && + !static_cast(ent)->OverrideKilledByGravityZones() && + !ent->IsType(hhCrawler::Type) && + (idMath::Fabs(curGravity.x) > 0.01f || idMath::Fabs(curGravity.y) > 0.01f || curGravity.z >= 0.0f) && + static_cast(ent)->IsActive() ) { + + const char *monsterDamageType = spawnArgs.GetString("def_monsterdamage"); + ent->Damage(this, NULL, vec3_origin, monsterDamageType, 1.0f, 0); + } + } +} + + +//----------------------------------------------------------------------- +// +// hhGravityZone +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZoneBase, hhGravityZone) + EVENT( EV_SetGravityVector, hhGravityZone::Event_SetNewGravity ) +END_CLASS + +void hhGravityZone::Spawn(void) { + zeroGravOnChange = spawnArgs.GetBool("zeroGravOnChange"); + idVec3 startGravity( spawnArgs.GetVector("gravity") ); + interpolationTime = SEC2MS(spawnArgs.GetFloat("interpTime")); + gravityInterpolator.Init( gameLocal.time, 0, startGravity, startGravity ); + + if (startGravity != gameLocal.GetGravity()) { + if (!gameLocal.isMultiplayer) { //don't play sound in mp + StartSound("snd_gravity_loop_on", SND_CHANNEL_MISC1, 0, true); + } + } +} + +void hhGravityZone::Save(idSaveGame *savefile) const { + savefile->WriteFloat( gravityInterpolator.GetStartTime() ); // idInterpolate + savefile->WriteFloat( gravityInterpolator.GetDuration() ); + savefile->WriteVec3( gravityInterpolator.GetStartValue() ); + savefile->WriteVec3( gravityInterpolator.GetEndValue() ); + + savefile->WriteInt(interpolationTime); + savefile->WriteBool(zeroGravOnChange); +} + +void hhGravityZone::Restore( idRestoreGame *savefile ) { + float set; + idVec3 vec; + + savefile->ReadFloat( set ); // idInterpolate + gravityInterpolator.SetStartTime( set ); + savefile->ReadFloat( set ); + gravityInterpolator.SetDuration( set ); + savefile->ReadVec3( vec ); + gravityInterpolator.SetStartValue( vec ); + savefile->ReadVec3( vec ); + gravityInterpolator.SetEndValue( vec ); + + savefile->ReadInt(interpolationTime); + savefile->ReadBool(zeroGravOnChange); +} + +void hhGravityZone::Think() { + hhGravityZoneBase::Think(); + if (thinkFlags & TH_THINK) { + if (bShowVector) { + gameRenderWorld->DebugArrow(colorGreen, renderEntity.origin, renderEntity.origin + GetCurrentGravity(vec3_origin), 10); + } + } +} + +const idVec3 hhGravityZone::GetDestinationGravity() const { + return gravityInterpolator.GetEndValue(); +} + +const idVec3 hhGravityZone::GetCurrentGravity(const idVec3 &location) const { + return gravityInterpolator.GetCurrentValue( gameLocal.time ); +} + +void hhGravityZone::SetGravityOnZone( idVec3 &newGravity ) { + idVec3 startGrav; + + if (!gameLocal.isMultiplayer) { //don't play sound in mp + if ( newGravity.Compare(gameLocal.GetGravity(), VECTOR_EPSILON) ) { + StartSound("snd_gravity_off", SND_CHANNEL_ANY); + StopSound(SND_CHANNEL_MISC1, true); + StartSound("snd_gravity_loop_off", SND_CHANNEL_MISC1, 0, true); + } + else { + StartSound("snd_gravity_on", SND_CHANNEL_ANY); + StopSound(SND_CHANNEL_MISC1, true); + StartSound("snd_gravity_loop_on", SND_CHANNEL_MISC1, 0, true); + } + } + + if ( zeroGravOnChange ) { // nla + startGrav = vec3_origin; + } + else { + startGrav = GetCurrentGravity(vec3_origin); + } + // Interpolate to new gravity + gravityInterpolator.Init( gameLocal.time, interpolationTime, startGrav, newGravity ); +} + +//rww - network code +void hhGravityZone::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhGravityZoneBase::WriteToSnapshot(msg); + + msg.WriteFloat(gravityInterpolator.GetStartTime()); + msg.WriteFloat(gravityInterpolator.GetDuration()); + idVec3 vecStart = gravityInterpolator.GetStartValue(); + msg.WriteFloat(vecStart.x); + msg.WriteFloat(vecStart.y); + msg.WriteFloat(vecStart.z); + idVec3 vecEnd = gravityInterpolator.GetEndValue(); + msg.WriteDeltaFloat(vecStart.x, vecEnd.x); + msg.WriteDeltaFloat(vecStart.y, vecEnd.y); + msg.WriteDeltaFloat(vecStart.z, vecEnd.z); +} + +void hhGravityZone::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhGravityZoneBase::ReadFromSnapshot(msg); + gravityInterpolator.SetStartTime(msg.ReadFloat()); + gravityInterpolator.SetDuration(msg.ReadFloat()); + idVec3 vecStart; + vecStart.x = msg.ReadFloat(); + vecStart.y = msg.ReadFloat(); + vecStart.z = msg.ReadFloat(); + gravityInterpolator.SetStartValue(vecStart); + idVec3 vecEnd; + vecEnd.x = msg.ReadDeltaFloat(vecStart.x); + vecEnd.y = msg.ReadDeltaFloat(vecStart.y); + vecEnd.z = msg.ReadDeltaFloat(vecStart.z); + gravityInterpolator.SetEndValue(vecEnd); +} + +void hhGravityZone::ClientPredictionThink( void ) { + hhGravityZoneBase::ClientPredictionThink(); +} +//rww - end network code + +void hhGravityZone::Event_SetNewGravity( idVec3 &newGravity ) { + SetGravityOnZone( newGravity ); +} + + +//----------------------------------------------------------------------- +// +// hhGravityZoneInward +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZoneBase, hhGravityZoneInward) + EVENT( EV_SetGravityFactor, hhGravityZoneInward::Event_SetNewGravityFactor ) +END_CLASS + +void hhGravityZoneInward::Spawn(void) { + float startFactor = spawnArgs.GetFloat("factor", "50000"); + monsterGravityFactor = spawnArgs.GetFloat("monsterGravFactor", "1"); + interpolationTime = SEC2MS(spawnArgs.GetFloat("interpTime")); + factorInterpolator.Init( gameLocal.time, 0, startFactor, startFactor ); +} + +void hhGravityZoneInward::Save(idSaveGame *savefile) const { + savefile->WriteFloat( factorInterpolator.GetStartTime() ); // idInterpolate + savefile->WriteFloat( factorInterpolator.GetDuration() ); + savefile->WriteFloat( factorInterpolator.GetStartValue() ); + savefile->WriteFloat( factorInterpolator.GetEndValue() ); + + savefile->WriteInt(interpolationTime); + savefile->WriteFloat(monsterGravityFactor); +} + +void hhGravityZoneInward::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadFloat( set ); // idInterpolate + factorInterpolator.SetStartTime( set ); + savefile->ReadFloat( set ); + factorInterpolator.SetDuration( set ); + savefile->ReadFloat( set ); + factorInterpolator.SetStartValue(set); + savefile->ReadFloat( set ); + factorInterpolator.SetEndValue( set ); + + savefile->ReadInt(interpolationTime); + savefile->ReadFloat(monsterGravityFactor); +} + +void hhGravityZoneInward::EntityEntered(idEntity *ent) { + hhGravityZoneBase::EntityEntered(ent); + if ( ent && ent->IsType( hhMonsterAI::Type ) ) { + static_cast(ent)->GravClipModelAxis( true ); + } + // Disallow slope checking, it makes us stutter when walking on convex surfaces + + // aob - commented this because it allows the player to walk up vertical walls while in inward gravity zone + //Didn't see any studdering when thia was commented out. Do we still need it? + //if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + // static_cast(ent->GetPhysics())->SetSlopeCheck(false); + //} + //now done constantly while in a gravity zone + /* + if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + static_cast(ent->GetPhysics())->SetInwardGravity(1); + } + */ +} + +void hhGravityZoneInward::EntityLeaving(idEntity *ent) { + hhGravityZoneBase::EntityLeaving(ent); + if ( ent && ent->IsType( hhMonsterAI::Type ) ) { + static_cast(ent)->GravClipModelAxis( false ); + } + // Re-enable slope checking + + // aob - commented this because it allows the player to walk up vertical walls while in inward gravity zone + //Didn't see any studdering when thia was commented out. Do we still need it? + //if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + // static_cast(ent->GetPhysics())->SetSlopeCheck(true); + //} + if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + static_cast(ent->GetPhysics())->SetInwardGravity(0); + } +} + +// This is actually called each tick if for entities inside +void hhGravityZoneInward::EntityEncroaching( idEntity *ent ) { + + // Cancel any pending gravity resets from other zones + ent->CancelEvents( &EV_ResetGravity ); + + idVec3 curGravity = GetCurrentGravity( ent->GetOrigin() ); + idVec3 otherGravity; + if (TouchingOtherZones(ent, false, otherGravity)) { //factor in gravity for all other zones being touched to avoid back-and-forth behaviour + float l = curGravity.Normalize(); + curGravity += otherGravity; + curGravity *= l*0.5f; + } + if (ent->GetPhysics()->IsAtRest() && ent->GetGravity() != curGravity) { + ent->SetGravity( curGravity ); + ent->GetPhysics()->Activate(); + } + else { + ent->SetGravity( curGravity ); + } + if (ent->IsType( idAI::Type )) { + if (bKillsMonsters && ent->health > 0 && + !ent->IsType(hhCrawler::Type) && + (curGravity.x != 0.0f || curGravity.y != 0.0f || curGravity.z >= 0.0f) && + !static_cast(ent)->OverrideKilledByGravityZones() && + static_cast(ent)->IsActive() ) { + const char *monsterDamageType = spawnArgs.GetString("def_monsterdamage"); + ent->Damage(this, NULL, vec3_origin, monsterDamageType, 1.0f, 0); + } + } + //rww + else if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + static_cast(ent->GetPhysics())->SetInwardGravity(1); + } + + + if( ent->IsType(idAI::Type) && ent->health > 0 ) { + ent->GetPhysics()->SetGravity( ent->GetPhysics()->GetGravity() * monsterGravityFactor ); + ent->GetPhysics()->Activate(); + } + + if( bShowVector ) { + hhUtils::DebugCross( colorBlue, GetOrigin(), 100, 10 ); + idVec3 newGrav = GetCurrentGravity( ent->GetOrigin() ); + gameRenderWorld->DebugArrow( colorGreen, ent->GetRenderEntity()->origin, ent->GetRenderEntity()->origin + newGrav, 10 ); + } +} + +const idVec3 hhGravityZoneInward::GetCurrentGravity( const idVec3 &location ) const { + idVec3 grav; + idVec3 origin = GetGravityOrigin(); + float factor = factorInterpolator.GetCurrentValue( gameLocal.GetTime() ); + idVec3 inward = origin - location; + inward.Normalize(); + grav = inward * DEFAULT_GRAVITY * factor; + return grav; +} + +void hhGravityZoneInward::Event_SetNewGravityFactor( float newFactor ) { + // Interpolate to new gravity factor + float curFactor = factorInterpolator.GetCurrentValue( gameLocal.GetTime() ); + factorInterpolator.Init( gameLocal.GetTime(), interpolationTime, curFactor, newFactor ); +} + +//----------------------------------------------------------------------- +// +// hhAIWallwalkZone +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZone, hhAIWallwalkZone) +END_CLASS + +bool hhAIWallwalkZone::ValidEntity(idEntity *ent) { + // allow AI that isnt dead + return ent->IsType(idAI::Type) && ent->health > 0; +} + +void hhAIWallwalkZone::EntityEncroaching( idEntity *ent ) { + // Cancel any pending gravity resets from other zones + ent->CancelEvents( &EV_ResetGravity ); + + trace_t TraceInfo; + gameLocal.clip.TracePoint(TraceInfo, ent->GetOrigin(), ent->GetOrigin() + (idVec3(0,0,-300)*ent->GetRenderEntity()->axis), ent->GetPhysics()->GetClipMask(), ent); + if( TraceInfo.fraction < 1.0f ) { // && ent->health > 0 ) { + ent->SetGravity( -TraceInfo.c.normal ); + ent->GetPhysics()->Activate(); + } +} + +//----------------------------------------------------------------------- +// +// hhGravityZoneSinkhole +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZoneInward, hhGravityZoneSinkhole) + EVENT( EV_SetGravityFactor, hhGravityZoneSinkhole::Event_SetNewGravityFactor ) +END_CLASS + +void hhGravityZoneSinkhole::Spawn(void) { + bReorient = false; + maxMagnitude = spawnArgs.GetFloat("maxMagnitude", "10000"); + minMagnitude = spawnArgs.GetFloat("minMagnitude", "0"); +} + +void hhGravityZoneSinkhole::Save(idSaveGame *savefile) const { + savefile->WriteFloat(maxMagnitude); + savefile->WriteFloat(minMagnitude); +} + +void hhGravityZoneSinkhole::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat(maxMagnitude); + savefile->ReadFloat(minMagnitude); +} + +// Still have this in case we want to do something mass based +const idVec3 hhGravityZoneSinkhole::GetCurrentGravityEntity(const idEntity *ent) const { + idVec3 grav = vec3_origin; + if (ent) { + // precalc mass product / G as a constant and expose that as the fudge factor + idVec3 origin = GetGravityOrigin(); + float factor = factorInterpolator.GetCurrentValue( gameLocal.time ); + idVec3 inward = origin - ent->GetOrigin(); + float distance = inward.Normalize(); + float distanceSquared = distance*distance; + + // Some different gravitational fields + // float gravMag = (mass * ent->GetPhysics()->GetMass() * GRAVITATIONAL_CONSTANT) / distanceSquared; + // float gravMag = factor*factor / distanceSquared; // Inverse squared distance + // float gravMag = factor / sqrt(distance); // Inverse sqrt distance + // float gravMag = factor * sqrt(distance); // sqrt distance + float gravMag = factor*factor / 2 + 0.2f * distanceSquared; // Inverse squared distance + //gameLocal.Printf("factor=%.0f gravity magnitude=%.2f\n", factor, gravMag); + + gravMag = hhMath::ClampFloat(minMagnitude, maxMagnitude, gravMag); // This will cut off extremely large forces + grav = inward * gravMag; + } + return grav; +} + +const idVec3 hhGravityZoneSinkhole::GetCurrentGravity(const idVec3 &location) const { + idVec3 grav; + + // precalc mass product / G as a constant and expose that as the fudge factor + idVec3 origin = GetGravityOrigin(); + float factor = factorInterpolator.GetCurrentValue( gameLocal.time ); + idVec3 inward = origin - location; + float distance = inward.Normalize(); + float distanceSquared = distance*distance; + + // Some different gravitational fields + float gravMag = factor*factor / 2 + 0.2f * distanceSquared; // Inverse squared distance + gravMag = hhMath::ClampFloat(minMagnitude, maxMagnitude, gravMag); // This will cut off extremely large forces + grav = inward * gravMag; + return grav; +} + +void hhGravityZoneSinkhole::Event_SetNewGravityFactor( float newFactor ) { + // Interpolate to new gravity factor + float curFactor = factorInterpolator.GetCurrentValue(gameLocal.time); + factorInterpolator.Init( gameLocal.time, interpolationTime, curFactor, newFactor ); +} + + +//----------------------------------------------------------------------- +// +// hhVelocityZone +// +//----------------------------------------------------------------------- + +const idEventDef EV_SetVelocityVector("setvelocity", "v"); + +CLASS_DECLARATION(hhZone, hhVelocityZone) + EVENT( EV_SetVelocityVector, hhVelocityZone::Event_SetNewVelocity ) +END_CLASS + +void hhVelocityZone::Spawn(void) { + bReorient = spawnArgs.GetBool("reorient"); + interpolationTime = SEC2MS(spawnArgs.GetFloat("interpTime")); + idVec3 startVelocity = spawnArgs.GetVector("velocity"); + //slop = 25.0f; // we use a slightly larger bounds to catch things that are rotated by bReorient + bShowVector = spawnArgs.GetBool("showVector"); + bKillsMonsters = spawnArgs.GetBool("killmonsters"); + + velocityInterpolator.Init(gameLocal.time, 0, startVelocity, startVelocity); +} + +void hhVelocityZone::Save(idSaveGame *savefile) const { + savefile->WriteFloat( velocityInterpolator.GetStartTime() ); // idInterpolate + savefile->WriteFloat( velocityInterpolator.GetDuration() ); + savefile->WriteVec3( velocityInterpolator.GetStartValue() ); + savefile->WriteVec3( velocityInterpolator.GetEndValue() ); + + savefile->WriteBool(bKillsMonsters); + savefile->WriteBool(bReorient); + savefile->WriteBool(bShowVector); + savefile->WriteInt(interpolationTime); +} + +void hhVelocityZone::Restore( idRestoreGame *savefile ) { + float set; + idVec3 vec; + + savefile->ReadFloat( set ); // idInterpolate + velocityInterpolator.SetStartTime( set ); + savefile->ReadFloat( set ); + velocityInterpolator.SetDuration( set ); + savefile->ReadVec3( vec ); + velocityInterpolator.SetStartValue( vec ); + savefile->ReadVec3( vec ); + velocityInterpolator.SetEndValue( vec ); + + savefile->ReadBool(bKillsMonsters); + savefile->ReadBool(bReorient); + savefile->ReadBool(bShowVector); + savefile->ReadInt(interpolationTime); +} + +void hhVelocityZone::EntityLeaving(idEntity *ent) { + ent->GetPhysics()->SetLinearVelocity(idVec3(0, 0, 0)); + if( ent->RespondsTo(EV_OrientToGravity) ) { + ent->ProcessEvent( &EV_OrientToGravity, (int)bReorient ); + } +} + +void hhVelocityZone::EntityEncroaching(idEntity *ent) { + idVec3 baseVelocity = velocityInterpolator.GetCurrentValue(gameLocal.time); + idVec3 baseVelocityDirection = baseVelocity; + baseVelocityDirection.Normalize(); + + idVec3 curVelocity; + curVelocity = ent->GetPhysics()->GetLinearVelocity(); + curVelocity.ProjectOntoPlane(baseVelocityDirection); + + ent->GetPhysics()->SetLinearVelocity( curVelocity + baseVelocity ); + if( ent->RespondsTo(EV_OrientToGravity) ) { + ent->ProcessEvent( &EV_OrientToGravity, (int)bReorient ); + } + else if (ent->IsType( idAI::Type )) { + if (bKillsMonsters && ent->health > 0 && + !static_cast(ent)->IsFlying()) { + const char *monsterDamageType = spawnArgs.GetString("def_monsterdamage"); + ent->Damage(this, NULL, vec3_origin, monsterDamageType, 1.0f, 0); + } + } +} + +void hhVelocityZone::Think() { + hhZone::Think(); + if (thinkFlags & TH_THINK) { + if (bShowVector) { + idVec3 baseVelocity = velocityInterpolator.GetCurrentValue(gameLocal.time); + gameRenderWorld->DebugArrow(colorGreen, renderEntity.origin, renderEntity.origin+baseVelocity, 10); + } + } +} + +void hhVelocityZone::Event_SetNewVelocity( idVec3 &newVelocity ) { + // Interpolate to new velocity + idVec3 currentVelocity = velocityInterpolator.GetCurrentValue(gameLocal.time); + velocityInterpolator.Init(gameLocal.time, interpolationTime, currentVelocity, newVelocity); +} + + +//----------------------------------------------------------------------- +// +// hhShuttleRecharge +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhShuttleRecharge) +END_CLASS + +void hhShuttleRecharge::Spawn(void) { + amountHealth = spawnArgs.GetInt("amounthealth"); + amountPower = spawnArgs.GetInt("amountpower"); +} + +void hhShuttleRecharge::Save(idSaveGame *savefile) const { + savefile->WriteInt(amountHealth); + savefile->WriteInt(amountPower); +} + +void hhShuttleRecharge::Restore( idRestoreGame *savefile ) { + savefile->ReadInt(amountHealth); + savefile->ReadInt(amountPower); +} + +bool hhShuttleRecharge::ValidEntity(idEntity *ent) { + return ent && ent->IsType(hhShuttle::Type); +} + +void hhShuttleRecharge::EntityEntered(idEntity *ent) { + //static_cast(ent)->SetRecharging(true); +} + +void hhShuttleRecharge::EntityLeaving(idEntity *ent) { + //static_cast(ent)->SetRecharging(false); +} + +void hhShuttleRecharge::EntityEncroaching(idEntity *ent) { + if (ent->IsType(hhVehicle::Type)) { + hhVehicle *vehicle = static_cast(ent); + + //HUMANHEAD bjk PCF (4-27-06) - shuttle recharge was slow + if(USERCMD_HZ == 30) { + vehicle->GiveHealth(2*amountHealth); + vehicle->GivePower(2*amountPower); + } else { + vehicle->GiveHealth(amountHealth); + vehicle->GivePower(amountPower); + } + } +} + + +//----------------------------------------------------------------------- +// +// hhDockingZone +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhDockingZone) +END_CLASS + +void hhDockingZone::Spawn(void) { + dock = NULL; + triggerBehavior = TB_PLAYER_MONSTERS_FRIENDLIES; // Allow all actors to trigger it + + fl.networkSync = true; +} + +void hhDockingZone::Save(idSaveGame *savefile) const { + dock.Save(savefile); +} + +void hhDockingZone::Restore( idRestoreGame *savefile ) { + dock.Restore(savefile); +} + +void hhDockingZone::RegisterDock(hhDock *d) { + dock = d; +} + +bool hhDockingZone::ValidEntity(idEntity *ent) { + if (ent) { + if (dock.IsValid() && dock->ValidEntity(ent)) { + return true; + } + if (ent->IsType(idActor::Type)) { //FIXME: Is this causing the shuttleCount to go wrong + return hhShuttle::ValidPilot(static_cast(ent)); + } + } + return false; +} + +void hhDockingZone::EntityEncroaching(idEntity *ent) { + if (dock.IsValid()) { + dock->EntityEncroaching(ent); + } +} + +void hhDockingZone::EntityEntered(idEntity *ent) { + if (dock.IsValid()) { + dock->EntityEntered(ent); + } +} + +void hhDockingZone::EntityLeaving(idEntity *ent) { + if (dock.IsValid()) { + dock->EntityLeaving(ent); + } +} + +void hhDockingZone::WriteToSnapshot( idBitMsgDelta &msg ) const { + GetPhysics()->WriteToSnapshot(msg); + msg.WriteBits(dock.GetSpawnId(), 32); +} + +void hhDockingZone::ReadFromSnapshot( const idBitMsgDelta &msg ) { + GetPhysics()->ReadFromSnapshot(msg); + dock.SetSpawnId(msg.ReadBits(32)); +} + +void hhDockingZone::ClientPredictionThink( void ) { + if (!gameLocal.isNewFrame) { + return; + } + Think(); +} + +//----------------------------------------------------------------------- +// +// hhShuttleDisconnect +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhShuttleDisconnect) +END_CLASS + +void hhShuttleDisconnect::Spawn(void) { +} + +bool hhShuttleDisconnect::ValidEntity(idEntity *ent) { + return ent && ent->IsType(hhShuttle::Type); +} + +void hhShuttleDisconnect::EntityEntered(idEntity *ent) { + static_cast(ent)->AllowTractor(false); +} + +void hhShuttleDisconnect::EntityEncroaching(idEntity *ent) { +} + +void hhShuttleDisconnect::EntityLeaving(idEntity *ent) { + static_cast(ent)->AllowTractor(true); +} + + +//----------------------------------------------------------------------- +// +// hhShuttleSlingshot +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhShuttleSlingshot) +END_CLASS + +void hhShuttleSlingshot::Spawn(void) { +} + +bool hhShuttleSlingshot::ValidEntity(idEntity *ent) { + return ent && ent->IsType(hhShuttle::Type); +} + +void hhShuttleSlingshot::EntityEntered(idEntity *ent) { +} + +void hhShuttleSlingshot::EntityEncroaching(idEntity *ent) { + float factor = spawnArgs.GetFloat("BoostFactor"); + + hhShuttle *shuttle = static_cast(ent); + shuttle->ApplyBoost( 255.0f * factor); + + // CJR: Alter the player's view when zooming through a slingshot zone + shuttle->GetPilot()->PostEventMS( &EV_SetOverlayMaterial, 0, spawnArgs.GetString( "mtr_speedView" ), -1, false ); + +} + +void hhShuttleSlingshot::EntityLeaving(idEntity *ent) { + // CJR: Reset the player's view after zooming through a slingshot zone + hhShuttle *shuttle = static_cast(ent); + shuttle->GetPilot()->PostEventMS( &EV_SetOverlayMaterial, 0, "", -1, false ); +} + + + +//----------------------------------------------------------------------- +// +// hhRemovalVolume +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhRemovalVolume) +END_CLASS + +void hhRemovalVolume::Spawn(void) { +} + +bool hhRemovalVolume::ValidEntity(idEntity *ent) { + return ent && ( + ent->IsType(idMoveable::Type) || + ent->IsType(idItem::Type) || + ent->IsType(hhAFEntity::Type) || + ent->IsType(hhAFEntity_WithAttachedHead::Type) || + (ent->IsType(hhMonsterAI::Type) && ent->health<=0 && !ent->fl.isTractored) ); +} + +void hhRemovalVolume::EntityEntered(idEntity *ent) { + ent->PostEventMS(&EV_Remove, 0); +} + +void hhRemovalVolume::EntityEncroaching(idEntity *ent) { +} + +void hhRemovalVolume::EntityLeaving(idEntity *ent) { +} diff --git a/examples/03 - New Entities/healthzone/game_zone.h b/examples/03 - New Entities/healthzone/game_zone.h new file mode 100644 index 0000000..e71c4e8 --- /dev/null +++ b/examples/03 - New Entities/healthzone/game_zone.h @@ -0,0 +1,282 @@ + +#ifndef __GAME_GRAVITYZONE_H__ +#define __GAME_GRAVITYZONE_H__ + +class hhDock; + +class hhZone : public hhTrigger { +public: + ABSTRACT_PROTOTYPE( hhZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think( void ); + virtual void Present( void ) { } // HUMANHEAD mdl: Not used by zones + + // hhTrigger interface + void TriggerAction(idEntity *activator); + + // hhZone interface + void ResetZoneList(); + void ApplyToEncroachers(); + bool ContainsEntityOfType(const idTypeInfo &t); + virtual void EntityEntered(idEntity *ent) {} + virtual void EntityLeaving(idEntity *ent) {} + virtual void EntityEncroaching(idEntity *ent) {} + virtual bool ValidEntity(idEntity *ent); + virtual void Empty(); + +protected: + void Event_TurnOff(); + void Event_Enable( void ); + void Event_Disable( void ); + void Event_Touch( idEntity *other, trace_t *trace ); + +protected: + idList zoneList; // List of valid entities in zone last frame + float slop; +}; + +//healthzone begin +class hhHealthZone : public hhZone { +public: + CLASS_PROTOTYPE( hhHealthZone ); //the necessary idClass prototypes + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); +protected: + int regenAmount; +}; +//healthzone end + +class hhTriggerZone : public hhZone { +public: + CLASS_PROTOTYPE( hhTriggerZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + + hhFuncParmAccessor funcRefInfo; +}; + +class hhGravityZoneBase : public hhZone { +public: + ABSTRACT_PROTOTYPE( hhGravityZoneBase ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual const idVec3 GetGravityOrigin() const; + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const = 0; + + virtual bool TouchingOtherZones(idEntity *ent, bool traceCheck, idVec3 &otherInfluence); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +protected: + bool bReorient; + bool bKillsMonsters; + bool bShowVector; + idVec3 gravityOriginOffset; //rww - avoid dictionary lookup +}; + +class hhGravityZone : public hhGravityZoneBase { +public: + CLASS_PROTOTYPE( hhGravityZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Think( void ); + virtual const idVec3 GetDestinationGravity() const; + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const; + virtual void SetGravityOnZone( idVec3 &newGravity ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +protected: + void Event_SetNewGravity( idVec3 &newgrav ); + +protected: + idInterpolate gravityInterpolator; + int interpolationTime; + bool zeroGravOnChange; +}; + +class hhAIWallwalkZone : public hhGravityZone { +public: + CLASS_PROTOTYPE( hhAIWallwalkZone ); + virtual void EntityEncroaching(idEntity *ent); + virtual bool ValidEntity(idEntity *ent); +}; + +class hhGravityZoneInward : public hhGravityZoneBase { +public: + CLASS_PROTOTYPE( hhGravityZoneInward ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void EntityEntered(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const; + +protected: + virtual void Event_SetNewGravityFactor( float newFactor ); + +protected: + idInterpolate factorInterpolator; + int interpolationTime; + float monsterGravityFactor; +}; + + +#define GRAVITATIONAL_CONSTANT 1.03416206832413664e-7f + +class hhGravityZoneSinkhole : public hhGravityZoneInward { +public: + CLASS_PROTOTYPE( hhGravityZoneSinkhole ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const; + const idVec3 GetCurrentGravityEntity(const idEntity *ent) const; + +protected: + void Event_SetNewGravityFactor( float newFactor ); + +protected: + float maxMagnitude; + float minMagnitude; +}; + + +class hhVelocityZone : public hhZone { +public: + CLASS_PROTOTYPE( hhVelocityZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Think( void ); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: + void Event_SetNewVelocity( idVec3 &newvel ); + +protected: + idInterpolate velocityInterpolator; + bool bKillsMonsters; + bool bReorient; + bool bShowVector; + int interpolationTime; +}; + +class hhShuttleRecharge : public hhZone { +public: + CLASS_PROTOTYPE( hhShuttleRecharge ); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + +protected: + int amountHealth; + int amountPower; +}; + +class hhDockingZone : public hhZone { +public: + CLASS_PROTOTYPE( hhDockingZone ); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + void RegisterDock(hhDock *d); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +protected: + idEntityPtr dock; +}; + +class hhShuttleDisconnect : public hhZone { +public: + CLASS_PROTOTYPE( hhShuttleDisconnect ); + + void Spawn(void); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: +}; + +class hhShuttleSlingshot : public hhZone { +public: + CLASS_PROTOTYPE( hhShuttleSlingshot ); + + void Spawn(void); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: +}; + +class hhRemovalVolume : public hhZone { +public: + CLASS_PROTOTYPE( hhRemovalVolume ); + + void Spawn(void); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: +}; + +#endif /* __GAME_GRAVITYZONE_H__ */ diff --git a/examples/03 - New Entities/healthzone/healthzone.pk4 b/examples/03 - New Entities/healthzone/healthzone.pk4 new file mode 100644 index 0000000000000000000000000000000000000000..95744c4d74c2a7aebdc0e18d2f2302557dbabf1c GIT binary patch literal 4094 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 W_is!6+dcspke^| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/2005game.vcproj b/src/2005game.vcproj new file mode 100644 index 0000000..d8ada14 --- /dev/null +++ b/src/2005game.vcproj @@ -0,0 +1,6529 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/2005idlib.vcproj b/src/2005idlib.vcproj new file mode 100644 index 0000000..1826141 --- /dev/null +++ b/src/2005idlib.vcproj @@ -0,0 +1,1124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MayaImport/Maya4.5/maya.h b/src/MayaImport/Maya4.5/maya.h new file mode 100644 index 0000000..73b47e0 --- /dev/null +++ b/src/MayaImport/Maya4.5/maya.h @@ -0,0 +1,47 @@ +#ifdef _WIN32 + +#define _BOOL + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef _BOOL + +#endif // _WIN32 diff --git a/src/MayaImport/Maya6.0/maya.h b/src/MayaImport/Maya6.0/maya.h new file mode 100644 index 0000000..73b47e0 --- /dev/null +++ b/src/MayaImport/Maya6.0/maya.h @@ -0,0 +1,47 @@ +#ifdef _WIN32 + +#define _BOOL + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef _BOOL + +#endif // _WIN32 diff --git a/src/MayaImport/exporter.h b/src/MayaImport/exporter.h new file mode 100644 index 0000000..1e40946 --- /dev/null +++ b/src/MayaImport/exporter.h @@ -0,0 +1,434 @@ +#define MAYA_DEFAULT_CAMERA "camera1" + +#define ANIM_TX BIT( 0 ) +#define ANIM_TY BIT( 1 ) +#define ANIM_TZ BIT( 2 ) +#define ANIM_QX BIT( 3 ) +#define ANIM_QY BIT( 4 ) +#define ANIM_QZ BIT( 5 ) + +typedef enum { + WRITE_MESH, + WRITE_ANIM, + WRITE_CAMERA +} exportType_t; + +typedef struct { + idCQuat q; + idVec3 t; +} jointFrame_t; + +typedef struct { + idCQuat q; + idVec3 t; + float fov; +} cameraFrame_t; + +/* +============================================================================================== + + idTokenizer + +============================================================================================== +*/ + +class idTokenizer { +private: + int currentToken; + idStrList tokens; + +public: + idTokenizer() { Clear(); }; + void Clear( void ) { currentToken = 0; tokens.Clear(); }; + + int SetTokens( const char *buffer ); + const char *NextToken( const char *errorstring = NULL ); + + bool TokenAvailable( void ) { return currentToken < tokens.Num(); }; + int Num( void ) { return tokens.Num(); }; + void UnGetToken( void ) { if ( currentToken > 0 ) { currentToken--; } }; + const char *GetToken( int index ) { if ( ( index >= 0 ) && ( index < tokens.Num() ) ) { return tokens[ index ]; } else { return NULL; } }; + const char *CurrentToken( void ) { return GetToken( currentToken ); }; +}; + +/* +============================================================================================== + + idExportOptions + +============================================================================================== +*/ + +class idNamePair { +public: + idStr from; + idStr to; +}; + +class idAnimGroup { +public: + idStr name; + idStrList joints; +}; + +class idExportOptions { +private: + idTokenizer tokens; + + void Reset( const char *commandline ); + +public: + idStr commandLine; + idStr src; + idStr dest; + idStr game; + idStr prefix; + float scale; + exportType_t type; + bool ignoreMeshes; + bool clearOrigin; + bool clearOriginAxis; + bool ignoreScale; + int startframe; + int endframe; + int framerate; + float xyzPrecision; + float quatPrecision; + idStr align; + idList renamejoints; + idList remapjoints; + idStrList keepjoints; + idStrList skipmeshes; + idStrList keepmeshes; + idList exportgroups; + idList groups; + float rotate; + float jointThreshold; + int cycleStart; + + // HUMANHEAD pdm: Allow bounds expansion (shared animations calculate bounds based on a different mesh, so give some slop) + float boundsExpansion; + // HUMANHEAD END + + idExportOptions( const char *commandline, const char *ospath ); + + bool jointInExportGroup( const char *jointname ); +}; + +/* +============================================================================== + +idExportJoint + +============================================================================== +*/ + +class idExportJoint { +public: + idStr name; + idStr realname; + idStr longname; + int index; + int exportNum; + bool keep; + + float scale; + float invscale; + + MFnDagNode *dagnode; + + idHierarchy mayaNode; + idHierarchy exportNode; + + idVec3 t; + idMat3 wm; + + idVec3 idt; + idMat3 idwm; + + idVec3 bindpos; + idMat3 bindmat; + + int animBits; + int firstComponent; + jointFrame_t baseFrame; + int depth; + + idExportJoint(); + idExportJoint &operator=( const idExportJoint &other ); +}; + +/* +============================================================================== + +misc structures + +============================================================================== +*/ + +typedef struct { + idExportJoint *joint; + float jointWeight; + idVec3 offset; +} exportWeight_t; + +typedef struct { + idVec3 pos; + idVec2 texCoords; + int startweight; + int numWeights; +} exportVertex_t; + +typedef struct { + int indexes[ 3 ]; +} exportTriangle_t; + +typedef struct { + idVec2 uv[ 3 ]; +} exportUV_t; + +ID_INLINE int operator==( exportVertex_t a, exportVertex_t b ) { + if ( a.pos != b.pos ) { + return false; + } + + if ( ( a.texCoords[ 0 ] != b.texCoords[ 0 ] ) || ( a.texCoords[ 1 ] != b.texCoords[ 1 ] ) ) { + return false; + } + + if ( ( a.startweight != b.startweight ) || ( a.numWeights != b.numWeights ) ) { + return false; + } + + return true; +} + +/* +======================================================================== + +.MD3 triangle model file format + +======================================================================== +*/ + +#define MD3_IDENT (('3'<<24)+('P'<<16)+('D'<<8)+'I') +#define MD3_VERSION 15 + +// limits +#define MD3_MAX_LODS 4 +#define MD3_MAX_TRIANGLES 8192 // per surface +#define MD3_MAX_VERTS 4096 // per surface +#define MD3_MAX_SHADERS 256 // per surface +#define MD3_MAX_FRAMES 1024 // per model +#define MD3_MAX_SURFACES 32 // per model +#define MD3_MAX_TAGS 16 // per frame + +// vertex scales +#define MD3_XYZ_SCALE (1.0/64) + +// surface geometry should not exceed these limits +#define SHADER_MAX_VERTEXES 1000 +#define SHADER_MAX_INDEXES (6*SHADER_MAX_VERTEXES) + + +// the maximum size of game reletive pathnames +#define MAX_Q3PATH 64 + +typedef struct md3Frame_s { + idVec3 bounds[2]; + idVec3 localOrigin; + float radius; + char name[16]; +} md3Frame_t; + +typedef struct md3Tag_s { + char name[MAX_Q3PATH]; // tag name + idVec3 origin; + idVec3 axis[3]; +} md3Tag_t; + +/* +** md3Surface_t +** +** CHUNK SIZE +** header sizeof( md3Surface_t ) +** shaders sizeof( md3Shader_t ) * numShaders +** triangles[0] sizeof( md3Triangle_t ) * numTriangles +** st sizeof( md3St_t ) * numVerts +** XyzNormals sizeof( md3XyzNormal_t ) * numVerts * numFrames +*/ +typedef struct { + int ident; // + + char name[MAX_Q3PATH]; // polyset name + + int flags; + int numFrames; // all surfaces in a model should have the same + + int numShaders; // all surfaces in a model should have the same + int numVerts; + + int numTriangles; + int ofsTriangles; + + int ofsShaders; // offset from start of md3Surface_t + int ofsSt; // texture coords are common for all frames + int ofsXyzNormals; // numVerts * numFrames + + int ofsEnd; // next surface follows +} md3Surface_t; + +typedef struct { + char name[MAX_Q3PATH]; + int shaderIndex; // for in-game use +} md3Shader_t; + +typedef struct { + int indexes[3]; +} md3Triangle_t; + +typedef struct { + float st[2]; +} md3St_t; + +typedef struct { + short xyz[3]; + short normal; +} md3XyzNormal_t; + +typedef struct { + int ident; + int version; + + char name[MAX_Q3PATH]; // model name + + int flags; + + int numFrames; + int numTags; + int numSurfaces; + + int numSkins; + + int ofsFrames; // offset for first frame + int ofsTags; // numFrames * numTags + int ofsSurfaces; // first surface, others follow + + int ofsEnd; // end of file +} md3Header_t; + +/* +============================================================================== + +idExportMesh + +============================================================================== +*/ + +class idExportMesh { +public: + + idStr name; + idStr shader; + + bool keep; + + idList verts; + idList tris; + idList weights; + idList uv; + + idExportMesh() { keep = true; }; + void ShareVerts( void ); + void GetBounds( idBounds &bounds ) const; + void Merge( idExportMesh *mesh ); +}; + +/* +============================================================================== + +idExportModel + +============================================================================== +*/ + +class idExportModel { +public: + idExportJoint *exportOrigin; + idList joints; + idHierarchy mayaHead; + idHierarchy exportHead; + idList cameraCuts; + idList camera; + idList bounds; + idList jointFrames; + idList frames; + int frameRate; + int numFrames; + int skipjoints; + int export_joints; + idList meshes; + + idExportModel(); + ~idExportModel(); + idExportJoint *FindJointReal( const char *name ); + idExportJoint *FindJoint( const char *name ); + bool WriteMesh( const char *filename, idExportOptions &options ); + bool WriteAnim( const char *filename, idExportOptions &options ); + bool WriteCamera( const char *filename, idExportOptions &options ); +}; + +/* +============================================================================== + +Maya + +============================================================================== +*/ + +class idMayaExport { +private: + idExportModel model; + idExportOptions &options; + + void FreeDagNodes( void ); + + float TimeForFrame( int num ) const; + int GetMayaFrameNum( int num ) const; + void SetFrame( int num ); + + + void GetBindPose( MObject &jointNode, idExportJoint *joint, float scale ); + void GetLocalTransform( idExportJoint *joint, idVec3 &pos, idMat3 &mat ); + void GetWorldTransform( idExportJoint *joint, idVec3 &pos, idMat3 &mat, float scale ); + + void CreateJoints( float scale ); + void PruneJoints( idStrList &keepjoints, idStr &prefix ); + void RenameJoints( idList &renamejoints, idStr &prefix ); + bool RemapParents( idList &remapjoints ); + + MObject FindShader( MObject& setNode ); + void GetTextureForMesh( idExportMesh *mesh, MFnDagNode &dagNode ); + + idExportMesh *CopyMesh( MFnSkinCluster &skinCluster, float scale ); + void CreateMesh( float scale ); + void CombineMeshes( void ); + + void GetAlignment( idStr &alignName, idMat3 &align, float rotate, int startframe ); + + const char *GetObjectType( MObject object ); + + float GetCameraFov( idExportJoint *joint ); + void GetCameraFrame( idExportJoint *camera, idMat3 &align, cameraFrame_t *cam ); + void CreateCameraAnim( idMat3 &align ); + + void GetDefaultPose( idMat3 &align ); + void CreateAnimation( idMat3 &align ); + +public: + idMayaExport( idExportOptions &exportOptions ) : options( exportOptions ) { }; + ~idMayaExport(); + + void ConvertModel( void ); + void ConvertToMD3( void ); +}; diff --git a/src/MayaImport/maya5.0/maya.h b/src/MayaImport/maya5.0/maya.h new file mode 100644 index 0000000..672a5bc --- /dev/null +++ b/src/MayaImport/maya5.0/maya.h @@ -0,0 +1,47 @@ +#ifdef _WIN32 + +#define _BOOL + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef _BOOL + +#endif // _WIN32 diff --git a/src/MayaImport/maya_main.cpp b/src/MayaImport/maya_main.cpp new file mode 100644 index 0000000..8fc0107 --- /dev/null +++ b/src/MayaImport/maya_main.cpp @@ -0,0 +1,3152 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +// HUMANHEAD CJR : Set-up for Maya 6.0 +#define USE_MAYA_6 1 + +#if USE_MAYA_6 +#undef _GLOBAL_USING +#define _GLOBAL_USING 0 +#include "Maya6.0/maya.h" // must also change include directory in project from "MayaImport\Maya4.5\include" to "MayaImport\Maya6.0\include" (requires MSDev 7.1) +#else +#include "Maya5.0/maya.h" +#endif +// HUMANHEAD END + +#include "exporter.h" +#include "maya_main.h" + +idStr errorMessage; +bool initialized = false; + +#define DEFAULT_ANIM_EPSILON 0.125f +#define DEFAULT_QUAT_EPSILON ( 1.0f / 8192.0f ) + +#define SLOP_VERTEX 0.01f // merge xyz coordinates this far apart +#define SLOP_TEXCOORD 0.001f // merge texture coordinates this far apart + +const char *componentNames[ 6 ] = { "Tx", "Ty", "Tz", "Qx", "Qy", "Qz" }; + +idSys * sys = NULL; +idCommon * common = NULL; +idCVarSystem * cvarSystem = NULL; + +idCVar * idCVar::staticVars = NULL; + +// HUMANHEAD pdm: Output all the animation text with Green color +#define Printf APrintf +// HUMANHEAD END + +/* +================= +MayaError +================= +*/ +void MayaError( const char *fmt, ... ) { + va_list argptr; + char text[ 8192 ]; + + va_start( argptr, fmt ); + idStr::vsnPrintf( text, sizeof( text ), fmt, argptr ); + va_end( argptr ); + + throw idException( text ); +} + +/* +================= +FS_WriteFloatString +================= +*/ +#define MAX_PRINT_MSG 4096 +static int WriteFloatString( FILE *file, const char *fmt, ... ) { + long i; + unsigned long u; + double f; + char *str; + int index; + idStr tmp, format; + va_list argPtr; + + va_start( argPtr, fmt ); + + index = 0; + + while( *fmt ) { + switch( *fmt ) { + case '%': + format = ""; + format += *fmt++; + while ( (*fmt >= '0' && *fmt <= '9') || + *fmt == '.' || *fmt == '-' || *fmt == '+' || *fmt == '#') { + format += *fmt++; + } + format += *fmt; + switch( *fmt ) { + case 'f': + case 'e': + case 'E': + case 'g': + case 'G': + f = va_arg( argPtr, double ); + if ( format.Length() <= 2 ) { + // high precision floating point number without trailing zeros + sprintf( tmp, "%1.10f", f ); + tmp.StripTrailing( '0' ); + tmp.StripTrailing( '.' ); + index += fprintf( file, "%s", tmp.c_str() ); + } + else { + index += fprintf( file, format.c_str(), f ); + } + break; + case 'd': + case 'i': + i = va_arg( argPtr, long ); + index += fprintf( file, format.c_str(), i ); + break; + case 'u': + u = va_arg( argPtr, unsigned long ); + index += fprintf( file, format.c_str(), u ); + break; + case 'o': + u = va_arg( argPtr, unsigned long ); + index += fprintf( file, format.c_str(), u ); + break; + case 'x': + u = va_arg( argPtr, unsigned long ); + index += fprintf( file, format.c_str(), u ); + break; + case 'X': + u = va_arg( argPtr, unsigned long ); + index += fprintf( file, format.c_str(), u ); + break; + case 'c': + i = va_arg( argPtr, long ); + index += fprintf( file, format.c_str(), (char) i ); + break; + case 's': + str = va_arg( argPtr, char * ); + index += fprintf( file, format.c_str(), str ); + break; + case '%': + index += fprintf( file, format.c_str() ); + break; + default: + MayaError( "WriteFloatString: invalid format %s", format.c_str() ); + break; + } + fmt++; + break; + case '\\': + fmt++; + switch( *fmt ) { + case 't': + index += fprintf( file, "\t" ); + break; + case 'n': + index += fprintf( file, "\n" ); + default: + MayaError( "WriteFloatString: unknown escape character \'%c\'", *fmt ); + break; + } + fmt++; + break; + default: + index += fprintf( file, "%c", *fmt ); + fmt++; + break; + } + } + + va_end( argPtr ); + + return index; +} + +/* +================ +OSPathToRelativePath + +takes a full OS path, as might be found in data from a media creation +program, and converts it to a qpath by stripping off directories + +Returns false if the osPath tree doesn't match any of the existing +search paths. +================ +*/ +bool OSPathToRelativePath( const char *osPath, idStr &qpath, const char *game ) { + char *s, *base; + + // skip a drive letter? + + // search for anything with BASE_GAMEDIR in it + // Ase files from max may have the form of: + // "//Purgatory/purgatory/doom/base/models/mapobjects/bitch/hologirl.tga" + // which won't match any of our drive letter based search paths + base = (char *)strstr( osPath, BASE_GAMEDIR ); //HUMANHEAD rww vs2005 - strstr now returns const char + + // _D3XP added mod support + if ( base == NULL && strlen(game) > 0 ) { + + base = s = (char *)strstr( osPath, game ); //HUMANHEAD rww vs2005 - strstr now returns const char + + while( s = strstr( s, game ) ) { + s += strlen( game ); + if ( s[0] == '/' || s[0] == '\\' ) { + base = s; + } + } + } + + if ( base ) { + s = strstr( base, "/" ); + if ( !s ) { + s = strstr( base, "\\" ); + } + if ( s ) { + qpath = s + 1; + return true; + } + } + + common->Printf( "OSPathToRelativePath failed on %s\n", osPath ); + qpath = osPath; + + return false; +} + +/* +=============== +ConvertFromIdSpace +=============== +*/ +idMat3 ConvertFromIdSpace( const idMat3 &idmat ) { + idMat3 mat; + + mat[ 0 ][ 0 ] = idmat[ 0 ][ 0 ]; + mat[ 0 ][ 2 ] = -idmat[ 0 ][ 1 ]; + mat[ 0 ][ 1 ] = idmat[ 0 ][ 2 ]; + + mat[ 1 ][ 0 ] = idmat[ 1 ][ 0 ]; + mat[ 1 ][ 2 ] = -idmat[ 1 ][ 1 ]; + mat[ 1 ][ 1 ] = idmat[ 1 ][ 2 ]; + + mat[ 2 ][ 0 ] = idmat[ 2 ][ 0 ]; + mat[ 2 ][ 2 ] = -idmat[ 2 ][ 1 ]; + mat[ 2 ][ 1 ] = idmat[ 2 ][ 2 ]; + + return mat; +} + +/* +=============== +ConvertFromIdSpace +=============== +*/ +idVec3 ConvertFromIdSpace( const idVec3 &idpos ) { + idVec3 pos; + + pos.x = idpos.x; + pos.z = -idpos.y; + pos.y = idpos.z; + + return pos; +} + +/* +=============== +ConvertToIdSpace +=============== +*/ +idMat3 ConvertToIdSpace( const idMat3 &mat ) { + idMat3 idmat; + + idmat[ 0 ][ 0 ] = mat[ 0 ][ 0 ]; + idmat[ 0 ][ 1 ] = -mat[ 0 ][ 2 ]; + idmat[ 0 ][ 2 ] = mat[ 0 ][ 1 ]; + + idmat[ 1 ][ 0 ] = mat[ 1 ][ 0 ]; + idmat[ 1 ][ 1 ] = -mat[ 1 ][ 2 ]; + idmat[ 1 ][ 2 ] = mat[ 1 ][ 1 ]; + + idmat[ 2 ][ 0 ] = mat[ 2 ][ 0 ]; + idmat[ 2 ][ 1 ] = -mat[ 2 ][ 2 ]; + idmat[ 2 ][ 2 ] = mat[ 2 ][ 1 ]; + + return idmat; +} + +/* +=============== +ConvertToIdSpace +=============== +*/ +idVec3 ConvertToIdSpace( const idVec3 &pos ) { + idVec3 idpos; + + idpos.x = pos.x; + idpos.y = -pos.z; + idpos.z = pos.y; + + return idpos; +} + +/* +=============== +idVec +=============== +*/ +idVec3 idVec( const MFloatPoint &point ) { + return idVec3( point[ 0 ], point[ 1 ], point[ 2 ] ); +} + +/* +=============== +idVec +=============== +*/ +idVec3 idVec( const MMatrix &matrix ) { + return idVec3( matrix[ 3 ][ 0 ], matrix[ 3 ][ 1 ], matrix[ 3 ][ 2 ] ); +} + +/* +=============== +idMat +=============== +*/ +idMat3 idMat( const MMatrix &matrix ) { + int j, k; + idMat3 mat; + + for( j = 0; j < 3; j++ ) { + for( k = 0; k < 3; k++ ) { + mat[ j ][ k ] = matrix[ j ][ k ]; + } + } + + return mat; +} + +/* +=============== +GetParent +=============== +*/ +MFnDagNode *GetParent( MFnDagNode *joint ) { + MStatus status; + MObject parentObject; + + parentObject = joint->parent( 0, &status ); + if ( !status && status.statusCode() == MStatus::kInvalidParameter ) { + return NULL; + } + + while( !parentObject.hasFn( MFn::kTransform ) ) { + MFnDagNode parentNode( parentObject, &status ); + if ( !status ) { + return NULL; + } + + parentObject = parentNode.parent( 0, &status ); + if ( !status && status.statusCode() == MStatus::kInvalidParameter ) { + return NULL; + } + } + + MFnDagNode *parentNode; + + parentNode = new MFnDagNode( parentObject, &status ); + if ( !status ) { + delete parentNode; + return NULL; + } + + return parentNode; +} + +/* +============================================================================================== + + idTokenizer + +============================================================================================== +*/ + +/* +==================== +idTokenizer::SetTokens +==================== +*/ +int idTokenizer::SetTokens( const char *buffer ) { + const char *cmd; + + Clear(); + + // tokenize commandline + cmd = buffer; + while ( *cmd ) { + // skip whitespace + while( *cmd && isspace( *cmd ) ) { + cmd++; + } + + if ( !*cmd ) { + break; + } + + idStr ¤t = tokens.Alloc(); + while( *cmd && !isspace( *cmd ) ) { + current += *cmd; + cmd++; + } + } + + return tokens.Num(); +} + +/* +==================== +idTokenizer::NextToken +==================== +*/ +const char *idTokenizer::NextToken( const char *errorstring ) { + if ( currentToken < tokens.Num() ) { + return tokens[ currentToken++ ]; + } + + if ( errorstring ) { + MayaError( "Error: %s", errorstring ); + } + + return NULL; +} + +/* +============================================================================================== + + idExportOptions + +============================================================================================== +*/ + +/* +==================== +idExportOptions::Reset +==================== +*/ +void idExportOptions::Reset( const char *commandline ) { + scale = 1.0f; + type = WRITE_MESH; + startframe = -1; + endframe = -1; + ignoreMeshes = false; + clearOrigin = false; + clearOriginAxis = false; + framerate = 24; + align = ""; + rotate = 0.0f; + commandLine = commandline; + prefix = ""; + jointThreshold = 0.05f; + ignoreScale = false; + xyzPrecision = DEFAULT_ANIM_EPSILON; + quatPrecision = DEFAULT_QUAT_EPSILON; + cycleStart = -1; + + // HUMANHEAD pdm: Allow bounds expansion (shared animations calculate bounds based on a different mesh, so give some slop) + boundsExpansion = 0.0f; + // HUMANHEAD END + + src.Clear(); + dest.Clear(); + + tokens.SetTokens( commandline ); + + keepjoints.Clear(); + renamejoints.Clear(); + remapjoints.Clear(); + exportgroups.Clear(); + skipmeshes.Clear(); + keepmeshes.Clear(); + groups.Clear(); +} + +/* +==================== +idExportOptions::idExportOptions +==================== +*/ +idExportOptions::idExportOptions( const char *commandline, const char *ospath ) { + idStr token; + idNamePair joints; + int i; + idAnimGroup *group; + idStr sourceDir; + idStr destDir; + + Reset( commandline ); + + token = tokens.NextToken( "Missing export command" ); + if ( token == "mesh" ) { + type = WRITE_MESH; + } else if ( token == "anim" ) { + type = WRITE_ANIM; + } else if ( token == "camera" ) { + type = WRITE_CAMERA; + } else { + MayaError( "Unknown export command '%s'", token.c_str() ); + } + + src = tokens.NextToken( "Missing source filename" ); + dest = src; + + for( token = tokens.NextToken(); token != ""; token = tokens.NextToken() ) { + if ( token == "-force" ) { + // skip + } else if ( token == "-game" ) { + // parse game name + game = tokens.NextToken( "Expecting game name after -game" ); + + } else if ( token == "-rename" ) { + // parse joint to rename + joints.from = tokens.NextToken( "Missing joint name for -rename. Usage: -rename [joint name] [new name]" ); + joints.to = tokens.NextToken( "Missing new name for -rename. Usage: -rename [joint name] [new name]" ); + renamejoints.Append( joints ); + + } else if ( token == "-prefix" ) { + prefix = tokens.NextToken( "Missing name for -prefix. Usage: -prefix [joint prefix]" ); + + } else if ( token == "-parent" ) { + // parse joint to reparent + joints.from = tokens.NextToken( "Missing joint name for -parent. Usage: -parent [joint name] [new parent]" ); + joints.to = tokens.NextToken( "Missing new parent for -parent. Usage: -parent [joint name] [new parent]" ); + remapjoints.Append( joints ); + + } else if ( !token.Icmp( "-sourcedir" ) ) { + // parse source directory + sourceDir = tokens.NextToken( "Missing filename for -sourcedir. Usage: -sourcedir [directory]" ); + + } else if ( !token.Icmp( "-destdir" ) ) { + // parse destination directory + destDir = tokens.NextToken( "Missing filename for -destdir. Usage: -destdir [directory]" ); + + } else if ( token == "-dest" ) { + // parse destination filename + dest = tokens.NextToken( "Missing filename for -dest. Usage: -dest [filename]" ); + + } else if ( token == "-range" ) { + // parse frame range to export + token = tokens.NextToken( "Missing start frame for -range. Usage: -range [start frame] [end frame]" ); + startframe = atoi( token ); + token = tokens.NextToken( "Missing end frame for -range. Usage: -range [start frame] [end frame]" ); + endframe = atoi( token ); + + if ( startframe > endframe ) { + MayaError( "Start frame is greater than end frame." ); + } + + } else if ( !token.Icmp( "-cycleStart" ) ) { + // parse start frame of cycle + token = tokens.NextToken( "Missing cycle start frame for -cycleStart. Usage: -cycleStart [first frame of cycle]" ); + cycleStart = atoi( token ); + + } else if ( token == "-scale" ) { + // parse scale + token = tokens.NextToken( "Missing scale amount for -scale. Usage: -scale [scale amount]" ); + scale = atof( token ); + + } else if ( token == "-align" ) { + // parse align joint + align = tokens.NextToken( "Missing joint name for -align. Usage: -align [joint name]" ); + + // HUMANHEAD pdm: Allow bounds expansion (shared animations calculate bounds based on a different mesh, so give some slop) + } else if ( token == "-expandbounds" ) { + // parse boundsexpansion + token = tokens.NextToken( "Missing expansion amount for -expandbounds. Usage: -expandbounds [expansion amount]" ); + boundsExpansion = atof( token ); + // HUMANHEAD END + + } else if ( token == "-rotate" ) { + // parse angle rotation + token = tokens.NextToken( "Missing value for -rotate. Usage: -rotate [yaw]" ); + rotate = -atof( token ); + + } else if ( token == "-nomesh" ) { + ignoreMeshes = true; + + } else if ( token == "-clearorigin" ) { + clearOrigin = true; + clearOriginAxis = true; + + } else if ( token == "-clearoriginaxis" ) { + clearOriginAxis = true; + + } else if ( token == "-ignorescale" ) { + ignoreScale = true; + + } else if ( token == "-xyzprecision" ) { + // parse quaternion precision + token = tokens.NextToken( "Missing value for -xyzprecision. Usage: -xyzprecision [precision]" ); + xyzPrecision = atof( token ); + if ( xyzPrecision < 0.0f ) { + MayaError( "Invalid value for -xyzprecision. Must be >= 0" ); + } + + } else if ( token == "-quatprecision" ) { + // parse quaternion precision + token = tokens.NextToken( "Missing value for -quatprecision. Usage: -quatprecision [precision]" ); + quatPrecision = atof( token ); + if ( quatPrecision < 0.0f ) { + MayaError( "Invalid value for -quatprecision. Must be >= 0" ); + } + + } else if ( token == "-jointthreshold" ) { + // parse joint threshold + token = tokens.NextToken( "Missing weight for -jointthreshold. Usage: -jointthreshold [minimum joint weight]" ); + jointThreshold = atof( token ); + + } else if ( token == "-skipmesh" ) { + token = tokens.NextToken( "Missing name for -skipmesh. Usage: -skipmesh [name of mesh to skip]" ); + skipmeshes.AddUnique( token ); + + } else if ( token == "-keepmesh" ) { + token = tokens.NextToken( "Missing name for -keepmesh. Usage: -keepmesh [name of mesh to keep]" ); + keepmeshes.AddUnique( token ); + + } else if ( token == "-jointgroup" ) { + token = tokens.NextToken( "Missing name for -jointgroup. Usage: -jointgroup [group name] [joint1] [joint2]...[joint n]" ); + group = groups.Ptr(); + for( i = 0; i < groups.Num(); i++, group++ ) { + if ( group->name == token ) { + break; + } + } + + if ( i >= groups.Num() ) { + // create a new group + group = &groups.Alloc(); + group->name = token; + } + + while( tokens.TokenAvailable() ) { + token = tokens.NextToken(); + if ( token[ 0 ] == '-' ) { + tokens.UnGetToken(); + break; + } + + group->joints.AddUnique( token ); + } + } else if ( token == "-group" ) { + // add the list of groups to export (these don't affect the hierarchy) + while( tokens.TokenAvailable() ) { + token = tokens.NextToken(); + if ( token[ 0 ] == '-' ) { + tokens.UnGetToken(); + break; + } + + group = groups.Ptr(); + for( i = 0; i < groups.Num(); i++, group++ ) { + if ( group->name == token ) { + break; + } + } + + if ( i >= groups.Num() ) { + MayaError( "Unknown group '%s'", token.c_str() ); + } + + exportgroups.AddUnique( group ); + } + } else if ( token == "-keep" ) { + // add joints that are kept whether they're used by a mesh or not + while( tokens.TokenAvailable() ) { + token = tokens.NextToken(); + if ( token[ 0 ] == '-' ) { + tokens.UnGetToken(); + break; + } + keepjoints.AddUnique( token ); + } + } else { + MayaError( "Unknown option '%s'", token.c_str() ); + } + } + + token = src; + src = ospath; + src.BackSlashesToSlashes(); + src.AppendPath( sourceDir ); + src.AppendPath( token ); + + token = dest; + dest = ospath; + dest.BackSlashesToSlashes(); + dest.AppendPath( destDir ); + dest.AppendPath( token ); + + // Maya only accepts unix style path separators + src.BackSlashesToSlashes(); + dest.BackSlashesToSlashes(); + + if ( skipmeshes.Num() && keepmeshes.Num() ) { + MayaError( "Can't use -keepmesh and -skipmesh together." ); + } +} + +/* +==================== +idExportOptions::jointInExportGroup +==================== +*/ +bool idExportOptions::jointInExportGroup( const char *jointname ) { + int i; + int j; + idAnimGroup *group; + + if ( !exportgroups.Num() ) { + // if we don't have any groups specified as export then export every joint + return true; + } + + // search through all exported groups to see if this joint is exported + for( i = 0; i < exportgroups.Num(); i++ ) { + group = exportgroups[ i ]; + for( j = 0; j < group->joints.Num(); j++ ) { + if ( group->joints[ j ] == jointname ) { + return true; + } + } + } + + return false; +} + +/* +============================================================================== + +idExportJoint + +============================================================================== +*/ + +idExportJoint::idExportJoint() { + index = 0; + exportNum = 0; + + mayaNode.SetOwner( this ); + exportNode.SetOwner( this ); + + dagnode = NULL; + + t = vec3_zero; + wm = mat3_default; + bindpos = vec3_zero; + bindmat = mat3_default; + keep = false; + scale = 1.0f; + invscale = 1.0f; + animBits = 0; + firstComponent = 0; + baseFrame.q.Set( 0.0f, 0.0f, 0.0f ); + baseFrame.t.Zero(); +} + +idExportJoint &idExportJoint::operator=( const idExportJoint &other ) { + name = other.name; + realname = other.realname; + longname = other.longname; + index = other.index; + exportNum = other.exportNum; + keep = other.keep; + + scale = other.scale; + invscale = other.invscale; + + dagnode = other.dagnode; + + mayaNode = other.mayaNode; + exportNode = other.exportNode; + + t = other.t; + idt = other.idt; + wm = other.wm; + idwm = other.idwm; + bindpos = other.bindpos; + bindmat = other.bindmat; + + animBits = other.animBits; + firstComponent = other.firstComponent; + baseFrame = other.baseFrame; + + mayaNode.SetOwner( this ); + exportNode.SetOwner( this ); + + return *this; +} + +/* +============================================================================== + +idExportMesh + +============================================================================== +*/ + +void idExportMesh::ShareVerts( void ) { + int i, j, k; + exportVertex_t vert; + idList v; + + v = verts; + verts.Clear(); + for( i = 0; i < tris.Num(); i++ ) { + for( j = 0; j < 3; j++ ) { + vert = v[ tris[ i ].indexes[ j ] ]; + vert.texCoords[ 0 ] = uv[ i ].uv[ j ][ 0 ]; + vert.texCoords[ 1 ] = 1.0f - uv[ i ].uv[ j ][ 1 ]; + + for( k = 0; k < verts.Num(); k++ ) { + if ( vert.numWeights != verts[ k ].numWeights ) { + continue; + } + if ( vert.startweight != verts[ k ].startweight ) { + continue; + } + if ( !vert.pos.Compare( verts[ k ].pos, SLOP_VERTEX ) ) { + continue; + } + if ( !vert.texCoords.Compare( verts[ k ].texCoords, SLOP_TEXCOORD ) ) { + continue; + } + + break; + } + + if ( k < verts.Num() ) { + tris[ i ].indexes[ j ] = k; + } else { + tris[ i ].indexes[ j ] = verts.Append( vert ); + } + } + } +} + +void idExportMesh::Merge( idExportMesh *mesh ) { + int i; + int numverts; + int numtris; + int numweights; + int numuvs; + + // merge name + sprintf( name, "%s, %s", name.c_str(), mesh->name.c_str() ); + + // merge verts + numverts = verts.Num(); + verts.SetNum( numverts + mesh->verts.Num() ); + for( i = 0; i < mesh->verts.Num(); i++ ) { + verts[ numverts + i ] = mesh->verts[ i ]; + verts[ numverts + i ].startweight += weights.Num(); + } + + // merge triangles + numtris = tris.Num(); + tris.SetNum( numtris + mesh->tris.Num() ); + for( i = 0; i < mesh->tris.Num(); i++ ) { + tris[ numtris + i ].indexes[ 0 ] = mesh->tris[ i ].indexes[ 0 ] + numverts; + tris[ numtris + i ].indexes[ 1 ] = mesh->tris[ i ].indexes[ 1 ] + numverts; + tris[ numtris + i ].indexes[ 2 ] = mesh->tris[ i ].indexes[ 2 ] + numverts; + } + + // merge weights + numweights = weights.Num(); + weights.SetNum( numweights + mesh->weights.Num() ); + for( i = 0; i < mesh->weights.Num(); i++ ) { + weights[ numweights + i ] = mesh->weights[ i ]; + } + + // merge uvs + numuvs = uv.Num(); + uv .SetNum( numuvs + mesh->uv.Num() ); + for( i = 0; i < mesh->uv.Num(); i++ ) { + uv[ numuvs + i ] = mesh->uv[ i ]; + } +} + +void idExportMesh::GetBounds( idBounds &bounds ) const { + int i; + int j; + idVec3 pos; + const exportWeight_t *weight; + const exportVertex_t *vert; + + bounds.Clear(); + + weight = weights.Ptr(); + vert = verts.Ptr(); + for( i = 0; i < verts.Num(); i++, vert++ ) { + pos.Zero(); + weight = &weights[ vert->startweight ]; + for( j = 0; j < vert->numWeights; j++, weight++ ) { + pos += weight->jointWeight * ( weight->joint->idwm * weight->offset + weight->joint->idt ); + } + bounds.AddPoint( pos ); + } +} + +/* +============================================================================== + +idExportModel + +============================================================================== +*/ + +/* +==================== +idExportModel::idExportModel +==================== +*/ +ID_INLINE idExportModel::idExportModel() { + export_joints = 0; + skipjoints = 0; + frameRate = 24; + numFrames = 0; + exportOrigin = NULL; +} + +/* +==================== +idExportModel::~idExportModel +==================== +*/ +ID_INLINE idExportModel::~idExportModel() { + meshes.DeleteContents( true ); +} + +idExportJoint *idExportModel::FindJointReal( const char *name ) { + idExportJoint *joint; + int i; + + joint = joints.Ptr(); + for( i = 0; i < joints.Num(); i++, joint++ ) { + if ( joint->realname == name ) { + return joint; + } + } + + return NULL; +} + +idExportJoint *idExportModel::FindJoint( const char *name ) { + idExportJoint *joint; + int i; + + joint = joints.Ptr(); + for( i = 0; i < joints.Num(); i++, joint++ ) { + if ( joint->name == name ) { + return joint; + } + } + + return NULL; +} + +bool idExportModel::WriteMesh( const char *filename, idExportOptions &options ) { + int i, j; + int numMeshes; + idExportMesh *mesh; + idExportJoint *joint; + idExportJoint *parent; + idExportJoint *sibling; + FILE *file; + const char *parentName; + int parentNum; + idList jointList; + + file = fopen( filename, "w" ); + if ( !file ) { + return false; + } + + for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + jointList.Append( joint ); + } + + for( i = 0; i < jointList.Num(); i++ ) { + joint = jointList[ i ]; + sibling = joint->exportNode.GetSibling(); + while( sibling ) { + if ( idStr::Cmp( joint->name, sibling->name ) > 0 ) { + joint->exportNode.MakeSiblingAfter( sibling->exportNode ); + sibling = joint->exportNode.GetSibling(); + } else { + sibling = sibling->exportNode.GetSibling(); + } + } + } + + jointList.Clear(); + for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + joint->exportNum = jointList.Append( joint ); + } + + numMeshes = 0; + if ( !options.ignoreMeshes ) { + for( i = 0; i < meshes.Num(); i++ ) { + if ( meshes[ i ]->keep ) { + numMeshes++; + } + } + } + + // write version info + WriteFloatString( file, MD5_VERSION_STRING " %d\n", MD5_VERSION ); + WriteFloatString( file, "commandline \"%s\"\n\n", options.commandLine.c_str() ); + + // write joints + WriteFloatString( file, "numJoints %d\n", jointList.Num() ); + WriteFloatString( file, "numMeshes %d\n\n", numMeshes ); + + WriteFloatString( file, "joints {\n" ); + for( i = 0; i < jointList.Num(); i++ ) { + joint = jointList[ i ]; + parent = joint->exportNode.GetParent(); + if ( parent ) { + parentNum = parent->exportNum; + parentName = parent->name.c_str(); + } else { + parentNum = -1; + parentName = ""; + } + + idCQuat bindQuat = joint->bindmat.ToQuat().ToCQuat(); + WriteFloatString( file, "\t\"%s\"\t%d ( %f %f %f ) ( %f %f %f )\t\t// %s\n", joint->name.c_str(), parentNum, + joint->bindpos.x, joint->bindpos.y, joint->bindpos.z, bindQuat[ 0 ], bindQuat[ 1 ], bindQuat[ 2 ], parentName ); + } + WriteFloatString( file, "}\n" ); + + // write meshes + for( i = 0; i < meshes.Num(); i++ ) { + mesh = meshes[ i ]; + if ( !mesh->keep ) { + continue; + } + + WriteFloatString( file, "\nmesh {\n" ); + WriteFloatString( file, "\t// meshes: %s\n", mesh->name.c_str() ); + WriteFloatString( file, "\tshader \"%s\"\n", mesh->shader.c_str() ); + + WriteFloatString( file, "\n\tnumverts %d\n", mesh->verts.Num() ); + for( j = 0; j < mesh->verts.Num(); j++ ) { + WriteFloatString( file, "\tvert %d ( %f %f ) %d %d\n", j, mesh->verts[ j ].texCoords[ 0 ], mesh->verts[ j ].texCoords[ 1 ], + mesh->verts[ j ].startweight, mesh->verts[ j ].numWeights ); + } + + WriteFloatString( file, "\n\tnumtris %d\n", mesh->tris.Num() ); + for( j = 0; j < mesh->tris.Num(); j++ ) { + WriteFloatString( file, "\ttri %d %d %d %d\n", j, mesh->tris[ j ].indexes[ 2 ], mesh->tris[ j ].indexes[ 1 ], mesh->tris[ j ].indexes[ 0 ] ); + } + + WriteFloatString( file, "\n\tnumweights %d\n", mesh->weights.Num() ); + for( j = 0; j < mesh->weights.Num(); j++ ) { + exportWeight_t *weight; + + weight = &mesh->weights[ j ]; + WriteFloatString( file, "\tweight %d %d %f ( %f %f %f )\n", j, + weight->joint->exportNum, weight->jointWeight, weight->offset.x, weight->offset.y, weight->offset.z ); + } + + WriteFloatString( file, "}\n" ); + } + + fclose( file ); + + return true; +} + +bool idExportModel::WriteAnim( const char *filename, idExportOptions &options ) { + int i, j; + idExportJoint *joint; + idExportJoint *parent; + idExportJoint *sibling; + jointFrame_t *frame; + FILE *file; + int numAnimatedComponents; + idList jointList; + + file = fopen( filename, "w" ); + if ( !file ) { + return false; + } + + for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + jointList.Append( joint ); + } + + for( i = 0; i < jointList.Num(); i++ ) { + joint = jointList[ i ]; + sibling = joint->exportNode.GetSibling(); + while( sibling ) { + if ( idStr::Cmp( joint->name, sibling->name ) > 0 ) { + joint->exportNode.MakeSiblingAfter( sibling->exportNode ); + sibling = joint->exportNode.GetSibling(); + } else { + sibling = sibling->exportNode.GetSibling(); + } + } + } + + jointList.Clear(); + for( joint = exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + joint->exportNum = jointList.Append( joint ); + } + + numAnimatedComponents = 0; + for( i = 0; i < jointList.Num(); i++ ) { + joint = jointList[ i ]; + joint->exportNum = i; + joint->baseFrame = frames[ 0 ][ joint->index ]; + joint->animBits = 0; + for( j = 1; j < numFrames; j++ ) { + frame = &frames[ j ][ joint->index ]; + if ( fabs( frame->t[ 0 ] - joint->baseFrame.t[ 0 ] ) > options.xyzPrecision ) { + joint->animBits |= ANIM_TX; + } + if ( fabs( frame->t[ 1 ] - joint->baseFrame.t[ 1 ] ) > options.xyzPrecision ) { + joint->animBits |= ANIM_TY; + } + if ( fabs( frame->t[ 2 ] - joint->baseFrame.t[ 2 ] ) > options.xyzPrecision ) { + joint->animBits |= ANIM_TZ; + } + if ( fabs( frame->q[ 0 ] - joint->baseFrame.q[ 0 ] ) > options.quatPrecision ) { + joint->animBits |= ANIM_QX; + } + if ( fabs( frame->q[ 1 ] - joint->baseFrame.q[ 1 ] ) > options.quatPrecision ) { + joint->animBits |= ANIM_QY; + } + if ( fabs( frame->q[ 2 ] - joint->baseFrame.q[ 2 ] ) > options.quatPrecision ) { + joint->animBits |= ANIM_QZ; + } + if ( ( joint->animBits & 63 ) == 63 ) { + break; + } + } + if ( joint->animBits ) { + joint->firstComponent = numAnimatedComponents; + for( j = 0; j < 6; j++ ) { + if ( joint->animBits & BIT( j ) ) { + numAnimatedComponents++; + } + } + } + } + + // write version info + WriteFloatString( file, MD5_VERSION_STRING " %d\n", MD5_VERSION ); + WriteFloatString( file, "commandline \"%s\"\n\n", options.commandLine.c_str() ); + + WriteFloatString( file, "numFrames %d\n", numFrames ); + WriteFloatString( file, "numJoints %d\n", jointList.Num() ); + WriteFloatString( file, "frameRate %d\n", frameRate ); + WriteFloatString( file, "numAnimatedComponents %d\n", numAnimatedComponents ); + + // write out the hierarchy + WriteFloatString( file, "\nhierarchy {\n" ); + for( i = 0; i < jointList.Num(); i++ ) { + joint = jointList[ i ]; + parent = joint->exportNode.GetParent(); + if ( parent ) { + WriteFloatString( file, "\t\"%s\"\t%d %d %d\t// %s", joint->name.c_str(), parent->exportNum, joint->animBits, joint->firstComponent, parent->name.c_str() ); + } else { + WriteFloatString( file, "\t\"%s\"\t-1 %d %d\t//", joint->name.c_str(), joint->animBits, joint->firstComponent ); + } + + if ( !joint->animBits ) { + WriteFloatString( file, "\n" ); + } else { + WriteFloatString( file, " ( " ); + for( j = 0; j < 6; j++ ) { + if ( joint->animBits & BIT( j ) ) { + WriteFloatString( file, "%s ", componentNames[ j ] ); + } + } + WriteFloatString( file, ")\n" ); + } + } + WriteFloatString( file, "}\n" ); + + // write the frame bounds + WriteFloatString( file, "\nbounds {\n" ); + for( i = 0; i < numFrames; i++ ) { +#if 1 // HUMANHEAD pdm: Allow bounds expansion (shared animations calculate bounds based on a different mesh, so give some slop) + idBounds expandedBounds = bounds[i].Expand( options.boundsExpansion ); + WriteFloatString( file, "\t( %f %f %f ) ( %f %f %f )\n", expandedBounds[ 0 ].x, expandedBounds[ 0 ].y, expandedBounds[ 0 ].z, expandedBounds[ 1 ].x, expandedBounds[ 1 ].y, expandedBounds[ 1 ].z ); +#else // HUMANHEAD END + WriteFloatString( file, "\t( %f %f %f ) ( %f %f %f )\n", bounds[ i ][ 0 ].x, bounds[ i ][ 0 ].y, bounds[ i ][ 0 ].z, bounds[ i ][ 1 ].x, bounds[ i ][ 1 ].y, bounds[ i ][ 1 ].z ); +#endif + } + WriteFloatString( file, "}\n" ); + + // write the base frame + WriteFloatString( file, "\nbaseframe {\n" ); + for( i = 0; i < jointList.Num(); i++ ) { + joint = jointList[ i ]; + WriteFloatString( file, "\t( %f %f %f ) ( %f %f %f )\n", joint->baseFrame.t[ 0 ], joint->baseFrame.t[ 1 ], joint->baseFrame.t[ 2 ], + joint->baseFrame.q[ 0 ], joint->baseFrame.q[ 1 ], joint->baseFrame.q[ 2 ] ); + } + WriteFloatString( file, "}\n" ); + + // write the frames + for( i = 0; i < numFrames; i++ ) { + WriteFloatString( file, "\nframe %d {\n", i ); + for( j = 0; j < jointList.Num(); j++ ) { + joint = jointList[ j ]; + frame = &frames[ i ][ joint->index ]; + if ( joint->animBits ) { + WriteFloatString( file, "\t" ); + if ( joint->animBits & ANIM_TX ) { + WriteFloatString( file, " %f", frame->t[ 0 ] ); + } + if ( joint->animBits & ANIM_TY ) { + WriteFloatString( file, " %f", frame->t[ 1 ] ); + } + if ( joint->animBits & ANIM_TZ ) { + WriteFloatString( file, " %f", frame->t[ 2 ] ); + } + if ( joint->animBits & ANIM_QX ) { + WriteFloatString( file, " %f", frame->q[ 0 ] ); + } + if ( joint->animBits & ANIM_QY ) { + WriteFloatString( file, " %f", frame->q[ 1 ] ); + } + if ( joint->animBits & ANIM_QZ ) { + WriteFloatString( file, " %f", frame->q[ 2 ] ); + } + WriteFloatString( file, "\n" ); + } + } + WriteFloatString( file, "}\n" ); + } + + fclose( file ); + + return true; +} + +bool idExportModel::WriteCamera( const char *filename, idExportOptions &options ) { + int i; + FILE *file; + + file = fopen( filename, "w" ); + if ( !file ) { + return false; + } + + // write version info + WriteFloatString( file, MD5_VERSION_STRING " %d\n", MD5_VERSION ); + WriteFloatString( file, "commandline \"%s\"\n\n", options.commandLine.c_str() ); + + WriteFloatString( file, "numFrames %d\n", camera.Num() ); + WriteFloatString( file, "frameRate %d\n", frameRate ); + WriteFloatString( file, "numCuts %d\n", cameraCuts.Num() ); + + // write out the cuts + WriteFloatString( file, "\ncuts {\n" ); + for( i = 0; i < cameraCuts.Num(); i++ ) { + WriteFloatString( file, "\t%d\n", cameraCuts[ i ] ); + } + WriteFloatString( file, "}\n" ); + + // write out the frames + WriteFloatString( file, "\ncamera {\n" ); + cameraFrame_t *frame = camera.Ptr(); + for( i = 0; i < camera.Num(); i++, frame++ ) { + WriteFloatString( file, "\t( %f %f %f ) ( %f %f %f ) %f\n", frame->t.x, frame->t.y, frame->t.z, frame->q[ 0 ], frame->q[ 1 ], frame->q[ 2 ], frame->fov ); + } + WriteFloatString( file, "}\n" ); + + fclose( file ); + + return true; +} + +/* +============================================================================== + +Maya + +============================================================================== +*/ + +/* +=============== +idMayaExport::~idMayaExport + +=============== +*/ +idMayaExport::~idMayaExport() { + FreeDagNodes(); + + // free up the file in Maya + MFileIO::newFile( true ); +} + +/* +=============== +idMayaExport::TimeForFrame +=============== +*/ +float idMayaExport::TimeForFrame( int num ) const { + MTime time; + + // set time unit to 24 frames per second + time.setUnit( MTime::kFilm ); + time.setValue( num ); + return time.as( MTime::kSeconds ); +} + +/* +=============== +idMayaExport::GetMayaFrameNum +=============== +*/ +int idMayaExport::GetMayaFrameNum( int num ) const { + int frameNum; + + if ( options.cycleStart > options.startframe ) { + // in cycles, the last frame is a duplicate of the first frame, so with cycleStart we need to + // duplicate one of the interior frames instead and chop off the first frame. + frameNum = options.cycleStart + num; + if ( frameNum > options.endframe ) { + frameNum -= options.endframe - options.startframe; + } + if ( frameNum < options.startframe ) { + frameNum = options.startframe + 1; + } + } else { + frameNum = options.startframe + num; + if ( frameNum > options.endframe ) { + frameNum -= options.endframe + 1 - options.startframe; + } + if ( frameNum < options.startframe ) { + frameNum = options.startframe; + } + } + + return frameNum; +} + +/* +=============== +idMayaExport::SetFrame +=============== +*/ +void idMayaExport::SetFrame( int num ) { + MTime time; + int frameNum; + + frameNum = GetMayaFrameNum( num ); + + // set time unit to 24 frames per second + time.setUnit( MTime::kFilm ); + time.setValue( frameNum ); + MGlobal::viewFrame( time ); +} + +/* +=============== +idMayaExport::PruneJoints +=============== +*/ +void idMayaExport::PruneJoints( idStrList &keepjoints, idStr &prefix ) { + int i; + int j; + idExportMesh *mesh; + idExportJoint *joint; + idExportJoint *joint2; + idExportJoint *parent; + int num_weights; + + // if we don't have any joints specified to keep, mark the ones used by the meshes as keep + if ( !keepjoints.Num() && !prefix.Length() ) { + if ( !model.meshes.Num() || options.ignoreMeshes ) { + // export all joints + joint = model.joints.Ptr(); + for( i = 0; i < model.joints.Num(); i++, joint++ ) { + joint->keep = true; + } + } else { + for( i = 0; i < model.meshes.Num(); i++, mesh++ ) { + mesh = model.meshes[ i ]; + for( j = 0; j < mesh->weights.Num(); j++ ) { + mesh->weights[ j ].joint->keep = true; + } + } + } + } else { + // mark the joints to keep + for( i = 0; i < keepjoints.Num(); i++ ) { + joint = model.FindJoint( keepjoints[ i ] ); + if ( joint ) { + joint->keep = true; + } + } + + // count valid meshes + for( i = 0; i < model.meshes.Num(); i++ ) { + mesh = model.meshes[ i ]; + num_weights = 0; + for( j = 0; j < mesh->weights.Num(); j++ ) { + if ( mesh->weights[ j ].joint->keep ) { + num_weights++; + } else if ( prefix.Length() && !mesh->weights[ j ].joint->realname.Cmpn( prefix, prefix.Length() ) ) { + // keep the joint if it's used by the mesh and it has the right prefix + mesh->weights[ j ].joint->keep = true; + num_weights++; + } + } + + if ( num_weights != mesh->weights.Num() ) { + mesh->keep = false; + } + } + } + + // find all joints aren't exported and reparent joint's children + model.export_joints = 0; + joint = model.joints.Ptr(); + for( i = 0; i < model.joints.Num(); i++, joint++ ) { + if ( !joint->keep ) { + joint->exportNode.RemoveFromHierarchy(); + } else { + joint->index = model.export_joints; + model.export_joints++; + + // make sure we are parented to an exported joint + for( parent = joint->exportNode.GetParent(); parent != NULL; parent = parent->exportNode.GetParent() ) { + if ( parent->keep ) { + break; + } + } + + if ( parent != NULL ) { + joint->exportNode.ParentTo( parent->exportNode ); + } else { + joint->exportNode.ParentTo( model.exportHead ); + } + } + } + + // check if we have any duplicate joint names + for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + if ( !joint->keep ) { + MayaError( "Non-kept joint in export tree ('%s')", joint->name.c_str() ); + } + + for( joint2 = model.exportHead.GetNext(); joint2 != NULL; joint2 = joint2->exportNode.GetNext() ) { + if ( ( joint2 != joint ) && ( joint2->name == joint->name ) ) { + MayaError( "Two joints found with the same name ('%s')", joint->name.c_str() ); + } + } + } +} + +/* +=============== +idMayaExport::FreeDagNodes +=============== +*/ +void idMayaExport::FreeDagNodes( void ) { + int i; + + for( i = 0; i < model.joints.Num(); i++ ) { + delete model.joints[ i ].dagnode; + model.joints[ i ].dagnode = NULL; + } +} + +/* +=============== +idMayaExport::GetBindPose +=============== +*/ +void idMayaExport::GetBindPose( MObject &jointNode, idExportJoint *joint, float scale ) { + MStatus status; + MFnDependencyNode fnJoint( jointNode ); + MObject aBindPose = fnJoint.attribute( "bindPose", &status ); + + joint->bindpos = vec3_zero; + joint->bindmat = mat3_default; + + if ( MS::kSuccess == status ) { + unsigned ii; + unsigned jointIndex; + unsigned connLength; + MPlugArray connPlugs; + MPlug pBindPose( jointNode, aBindPose ); + + pBindPose.connectedTo( connPlugs, false, true ); + connLength = connPlugs.length(); + for( ii = 0; ii < connLength; ++ii ) { + if ( connPlugs[ ii ].node().apiType() == MFn::kDagPose ) { + MObject aMember = connPlugs[ ii ].attribute(); + MFnAttribute fnAttr( aMember ); + + if ( fnAttr.name() == "worldMatrix" ) { + jointIndex = connPlugs[ ii ].logicalIndex(); + + MFnDependencyNode nDagPose( connPlugs[ ii ].node() ); + + // construct plugs for this joint's world matrix + MObject aWorldMatrix = nDagPose.attribute( "worldMatrix" ); + MPlug pWorldMatrix( connPlugs[ ii ].node(), aWorldMatrix ); + + pWorldMatrix.selectAncestorLogicalIndex( jointIndex, aWorldMatrix ); + + // get the world matrix data + MObject worldMatrix; + MStatus status = pWorldMatrix.getValue( worldMatrix ); + if ( MS::kSuccess != status ) { + // Problem retrieving world matrix + return; + } + + MFnMatrixData dMatrix( worldMatrix ); + MMatrix wMatrix = dMatrix.matrix( &status ); + + joint->bindmat = ConvertToIdSpace( idMat( wMatrix ) ); + joint->bindpos = ConvertToIdSpace( idVec( wMatrix ) ) * scale; + if ( !options.ignoreScale ) { + joint->bindpos *= joint->scale; + } else { + joint->bindmat[ 0 ].Normalize(); + joint->bindmat[ 1 ].Normalize(); + joint->bindmat[ 2 ].Normalize(); + } + + return; + } + } + } + } +} + +/* +=============== +idMayaExport::GetLocalTransform +=============== +*/ +void idMayaExport::GetLocalTransform( idExportJoint *joint, idVec3 &pos, idMat3 &mat ) { + MStatus status; + MDagPath dagPath; + + pos.Zero(); + mat.Identity(); + + if ( !joint->dagnode ) { + return; + } + + status = joint->dagnode->getPath( dagPath ); + if ( !status ) { + return; + } + + MObject transformNode = dagPath.transform( &status ); + if ( !status && ( status.statusCode () == MStatus::kInvalidParameter ) ) { + return; + } + + MFnDagNode transform( transformNode, &status ); + if ( !status ) { + return; + } + + pos = idVec( transform.transformationMatrix() ); + mat = idMat( transform.transformationMatrix() ); +} + +/* +=============== +idMayaExport::GetWorldTransform +=============== +*/ +void idMayaExport::GetWorldTransform( idExportJoint *joint, idVec3 &pos, idMat3 &mat, float scale ) { + idExportJoint *parent; + + GetLocalTransform( joint, pos, mat ); + mat.OrthoNormalizeSelf(); + pos *= scale; + + parent = joint->mayaNode.GetParent(); + if ( parent ) { + idVec3 parentpos; + idMat3 parentmat; + + GetWorldTransform( parent, parentpos, parentmat, scale ); + + pos = parentpos + ( parentmat * ( pos * parent->scale ) ); + mat = mat * parentmat; + } +} + +/* +=============== +idMayaExport::CreateJoints +=============== +*/ +void idMayaExport::CreateJoints( float scale ) { + int i; + int j; + idExportJoint *joint; + idExportJoint *parent; + MStatus status; + MDagPath dagPath; + MFnDagNode *parentNode; + + SetFrame( 0 ); + + // create an initial list with all of the transformable nodes in the scene + MItDag dagIterator( MItDag::kDepthFirst, MFn::kTransform, &status ); + for ( ; !dagIterator.isDone(); dagIterator.next() ) { + status = dagIterator.getPath( dagPath ); + if ( !status ) { + MayaError( "CreateJoints: MItDag::getPath failed (%s)", status.errorString().asChar() ); + continue; + } + + joint = &model.joints.Alloc(); + joint->index = model.joints.Num() - 1; + joint->dagnode = new MFnDagNode( dagPath, &status ); + if ( !status ) { + MayaError( "CreateJoints: MFnDagNode constructor failed (%s)", status.errorString().asChar() ); + continue; + } + + joint->name = joint->dagnode->name().asChar(); + joint->realname = joint->name; + } + + // allocate an extra joint in case we need to add an origin later + model.exportOrigin = &model.joints.Alloc(); + model.exportOrigin->index = model.joints.Num() - 1; + + // create scene hierarchy + joint = model.joints.Ptr(); + for( i = 0; i < model.joints.Num(); i++, joint++ ) { + if ( !joint->dagnode ) { + continue; + } + joint->mayaNode.ParentTo( model.mayaHead ); + joint->exportNode.ParentTo( model.exportHead ); + + parentNode = GetParent( joint->dagnode ); + if ( parentNode ) { + // find the parent joint in our jointlist + for( j = 0; j < model.joints.Num(); j++ ) { + if ( !model.joints[ j ].dagnode ) { + continue; + } + if ( model.joints[ j ].dagnode->name() == parentNode->name() ) { + joint->mayaNode.ParentTo( model.joints[ j ].mayaNode ); + joint->exportNode.ParentTo( model.joints[ j ].exportNode ); + break; + } + } + + delete parentNode; + } + + // create long name + parent = joint->mayaNode.GetParent(); + if ( parent ) { + joint->longname = parent->longname + "/" + joint->name; + } else { + joint->longname = joint->name; + } + + // get the joint's scale + GetLocalTransform( &model.joints[ i ], joint->t, joint->wm ); + joint->scale = joint->wm[ 0 ].Length(); + + if ( parent ) { + joint->scale *= parent->scale; + if ( joint->scale != 0 ) { + joint->invscale = 1.0f / joint->scale; + } else { + joint->invscale = 0; + } + } + + joint->dagnode->getPath( dagPath ); + GetBindPose( dagPath.node( &status ), joint, scale ); + } +} + +/* +=============== +idMayaExport::RenameJoints +=============== +*/ +void idMayaExport::RenameJoints( idList &renamejoints, idStr &prefix ) { + int i; + idExportJoint *joint; + + // rename joints that match the prefix + if ( prefix.Length() ) { + joint = model.joints.Ptr(); + for( i = 0; i < model.joints.Num(); i++, joint++ ) { + if ( !joint->name.Cmpn( prefix, prefix.Length() ) ) { + // remove the prefix from the name + joint->name = joint->name.Right( joint->name.Length() - prefix.Length() ); + } + } + } + + // rename joints if necessary + for( i = 0; i < renamejoints.Num(); i++ ) { + joint = model.FindJoint( renamejoints[ i ].from ); + if ( joint ) { + joint->name = renamejoints[ i ].to; + } + } +} + +/* +=============== +idMayaExport::RemapParents +=============== +*/ +bool idMayaExport::RemapParents( idList &remapjoints ) { + int i; + idExportJoint *joint; + idExportJoint *parent; + idExportJoint *origin; + idExportJoint *sibling; + + for( i = 0; i < remapjoints.Num(); i++ ) { + // find joint to reparent + joint = model.FindJoint( remapjoints[ i ].from ); + if ( !joint ) { + // couldn't find joint, fail + MayaError( "Couldn't find joint '%s' to reparent\n", remapjoints[ i ].from.c_str() ); + } + + // find new parent joint + parent = model.FindJoint( remapjoints[ i ].to ); + if ( !parent ) { + // couldn't find parent, fail + MayaError( "Couldn't find joint '%s' to be new parent for '%s'\n", remapjoints[ i ].to.c_str(), remapjoints[ i ].from.c_str() ); + } + + if ( parent->exportNode.ParentedBy( joint->exportNode ) ) { + MayaError( "Joint '%s' is a child of joint '%s' and can't become the parent.", joint->name.c_str(), parent->name.c_str() ); + } + joint->exportNode.ParentTo( parent->exportNode ); + } + + // if we have an origin, make it the first node in the export list, otherwise add one + origin = model.FindJoint( "origin" ); + if ( !origin ) { + origin = model.exportOrigin; + origin->dagnode = NULL; + origin->name = "origin"; + origin->realname = "origin"; + origin->bindmat.Identity(); + origin->bindpos.Zero(); + } + + origin->exportNode.ParentTo( model.exportHead ); + + // force the joint to be kept + origin->keep = true; + + // make all root joints children of the origin joint + joint = model.exportHead.GetChild(); + while( joint ) { + sibling = joint->exportNode.GetSibling(); + if ( joint != origin ) { + joint->exportNode.ParentTo( origin->exportNode ); + } + joint = sibling; + } + + return true; +} + +/* +=============== +idMayaExport::FindShader + +Find the shading node for the given shading group set node. +=============== +*/ +MObject idMayaExport::FindShader( MObject& setNode ) { + MStatus status; + MFnDependencyNode fnNode( setNode ); + MPlug shaderPlug; + + shaderPlug = fnNode.findPlug( "surfaceShader" ); + if ( !shaderPlug.isNull() ) { + MPlugArray connectedPlugs; + bool asSrc = false; + bool asDst = true; + shaderPlug.connectedTo( connectedPlugs, asDst, asSrc, &status ); + + if ( connectedPlugs.length() != 1 ) { + MayaError( "FindShader: Error getting shader (%s)", status.errorString().asChar() ); + } else { + return connectedPlugs[ 0 ].node(); + } + } + + return MObject::kNullObj; +} + +/* +=============== +idMayaExport::GetTextureForMesh + +Find the texture files that apply to the color of each polygon of +a selected shape if the shape has its polygons organized into sets. +=============== +*/ +void idMayaExport::GetTextureForMesh( idExportMesh *mesh, MFnDagNode &dagNode ) { + MStatus status; + MDagPath path; + int i; + int instanceNum; + + status = dagNode.getPath( path ); + if ( !status ) { + return; + } + + path.extendToShape(); + + // If the shape is instanced then we need to determine which + // instance this path refers to. + // + instanceNum = 0; + if ( path.isInstanced() ) { + instanceNum = path.instanceNumber(); + } + + // Get a list of all sets pertaining to the selected shape and the + // members of those sets. + // + MFnMesh fnMesh( path ); + MObjectArray sets; + MObjectArray comps; + status = fnMesh.getConnectedSetsAndMembers( instanceNum, sets, comps, true ); + if ( !status ) { + MayaError( "GetTextureForMesh: MFnMesh::getConnectedSetsAndMembers failed (%s)", status.errorString().asChar() ); + } + + // Loop through all the sets. If the set is a polygonal set, find the + // shader attached to the and print out the texture file name for the + // set along with the polygons in the set. + // + for ( i = 0; i < ( int )sets.length(); i++ ) { + MObject set = sets[i]; + MObject comp = comps[i]; + + MFnSet fnSet( set, &status ); + if ( status == MS::kFailure ) { + MayaError( "GetTextureForMesh: MFnSet constructor failed (%s)", status.errorString().asChar() ); + continue; + } + + // Make sure the set is a polygonal set. If not, continue. + MItMeshPolygon piter(path, comp, &status); + if (status == MS::kFailure) { + continue; + } + + // Find the texture that is applied to this set. First, get the + // shading node connected to the set. Then, if there is an input + // attribute called "color", search upstream from it for a texture + // file node. + // + MObject shaderNode = FindShader( set ); + if ( shaderNode == MObject::kNullObj ) { + continue; + } + + MPlug colorPlug = MFnDependencyNode(shaderNode).findPlug("color", &status); + if ( status == MS::kFailure ) { + continue; + } + + MItDependencyGraph dgIt(colorPlug, MFn::kFileTexture, + MItDependencyGraph::kUpstream, + MItDependencyGraph::kBreadthFirst, + MItDependencyGraph::kNodeLevel, + &status); + + if ( status == MS::kFailure ) { + continue; + } + + dgIt.disablePruningOnFilter(); + + // If no texture file node was found, just continue. + // + if ( dgIt.isDone() ) { + continue; + } + + // Print out the texture node name and texture file that it references. + // + MObject textureNode = dgIt.thisNode(); + MPlug filenamePlug = MFnDependencyNode( textureNode ).findPlug( "fileTextureName" ); + MString textureName; + filenamePlug.getValue( textureName ); + + // remove the OS path and save it in the mesh + OSPathToRelativePath( textureName.asChar(), mesh->shader, options.game ); + mesh->shader.StripFileExtension(); + + return; + } +} + +/* +=============== +idMayaExport::CopyMesh +=============== +*/ +idExportMesh *idMayaExport::CopyMesh( MFnSkinCluster &skinCluster, float scale ) { + MStatus status; + MObjectArray objarray; + MObjectArray outputarray; + int nGeom; + int i, j, k; + idExportMesh *mesh; + float uv_u, uv_v; + idStr name, altname; + int pos; + + status = skinCluster.getInputGeometry( objarray ); + if ( !status ) { + MayaError( "CopyMesh: Error getting input geometry (%s)", status.errorString().asChar() ); + return NULL; + } + + nGeom = objarray.length(); + for( i = 0; i < nGeom; i++ ) { + MFnDagNode dagNode( objarray[ i ], &status ); + if ( !status ) { + common->Printf( "CopyMesh: MFnDagNode Constructor failed (%s)", status.errorString().asChar() ); + continue; + } + + MFnMesh fnmesh( objarray[ i ], &status ); + if ( !status ) { + // object isn't an MFnMesh + continue; + } + + status = skinCluster.getOutputGeometry( outputarray ); + if ( !status ) { + common->Printf( "CopyMesh: Error getting output geometry (%s)", status.errorString().asChar() ); + return NULL; + } + + if ( outputarray.length() < 1 ) { + return NULL; + } + + name = fnmesh.name().asChar(); + if ( options.prefix.Length() ) { + if ( !name.Cmpn( options.prefix, options.prefix.Length() ) ) { + // remove the prefix from the name + name = name.Right( name.Length() - options.prefix.Length() ); + } else { + // name doesn't match prefix, so don't use this mesh + //return NULL; + } + } + + pos = name.Find( "ShapeOrig" ); + if ( pos >= 0 ) { + name.CapLength( pos ); + } + + MFnDagNode dagNode2( outputarray[ 0 ], &status ); + if ( !status ) { + common->Printf( "CopyMesh: MFnDagNode Constructor failed (%s)", status.errorString().asChar() ); + continue; + } + + altname = name; + MObject parent = fnmesh.parent( 0, &status ); + if ( status ) { + MFnDagNode parentNode( parent, &status ); + if ( status ) { + altname = parentNode.name().asChar(); + } + } + + name.StripLeadingOnce( options.prefix ); + altname.StripLeadingOnce( options.prefix ); + if ( options.keepmeshes.Num() ) { + if ( !options.keepmeshes.Find( name ) && !options.keepmeshes.Find( altname ) ) { + if ( altname != name ) { + common->Printf( "Skipping mesh '%s' ('%s')\n", name.c_str(), altname.c_str() ); + } else { + common->Printf( "Skipping mesh '%s'\n", name.c_str() ); + } + return NULL; + } + } + + if ( options.skipmeshes.Find( name ) || options.skipmeshes.Find( altname ) ) { + common->Printf( "Skipping mesh '%s' ('%s')\n", name.c_str(), altname.c_str() ); + return NULL; + } + + mesh = new idExportMesh(); + model.meshes.Append( mesh ); + + if ( altname.Length() ) { + mesh->name = altname; + } else { + mesh->name = name; + } + GetTextureForMesh( mesh, dagNode2 ); + + int v = fnmesh.numVertices( &status ); + mesh->verts.SetNum( v ); + + MFloatPointArray vertexArray; + + fnmesh.getPoints( vertexArray, MSpace::kPreTransform ); + + for( j = 0; j < v; j++ ) { + memset( &mesh->verts[ j ], 0, sizeof( mesh->verts[ j ] ) ); + mesh->verts[ j ].pos = ConvertToIdSpace( idVec( vertexArray[ j ] ) ) * scale; + } + + MIntArray vertexList; + int p; + + p = fnmesh.numPolygons( &status ); + mesh->tris.SetNum( p ); + mesh->uv.SetNum( p ); + + MString setName; + + status = fnmesh.getCurrentUVSetName( setName ); + if ( !status ) { + MayaError( "CopyMesh: MFnMesh::getCurrentUVSetName failed (%s)", status.errorString().asChar() ); + } + + for( j = 0; j < p; j++ ) { + fnmesh.getPolygonVertices( j, vertexList ); + if ( vertexList.length() != 3 ) { + MayaError( "CopyMesh: Too many vertices on a face (%d)\n", vertexList.length() ); + } + + for( k = 0; k < 3; k++ ) { + mesh->tris[ j ].indexes[ k ] = vertexList[ k ]; + + status = fnmesh.getPolygonUV( j, k, uv_u, uv_v, &setName ); + if ( !status ) { + MayaError( "CopyMesh: MFnMesh::getPolygonUV failed (%s)", status.errorString().asChar() ); + } + + mesh->uv[ j ].uv[ k ][ 0 ] = uv_u; + mesh->uv[ j ].uv[ k ][ 1 ] = uv_v; + } + } + + return mesh; + } + + return NULL; +} + +/* +=============== +idMayaExport::CreateMesh +=============== +*/ +void idMayaExport::CreateMesh( float scale ) { + size_t count; + idExportMesh *mesh; + MStatus status; + exportWeight_t weight; + unsigned int nGeoms; + + // Iterate through graph and search for skinCluster nodes + MItDependencyNodes iter( MFn::kSkinClusterFilter ); + count = 0; + for ( ; !iter.isDone(); iter.next() ) { + MObject object = iter.item(); + + count++; + + // For each skinCluster node, get the list of influence objects + MFnSkinCluster skinCluster( object, &status ); + if ( !status ) { + MayaError( "%s: Error getting skin cluster (%s)", object.apiTypeStr(), status.errorString().asChar() ); + } + + mesh = CopyMesh( skinCluster, scale ); + if ( !mesh ) { + continue; + } + + MDagPathArray infs; + unsigned int nInfs = skinCluster.influenceObjects(infs, &status); + if ( !status ) { + MayaError( "Mesh '%s': Error getting influence objects (%s)", mesh->name.c_str(), status.errorString().asChar() ); + } + + if ( 0 == nInfs ) { + MayaError( "Mesh '%s': No influence objects found", mesh->name.c_str() ); + } + + // loop through the geometries affected by this cluster + nGeoms = skinCluster.numOutputConnections(); + for (size_t ii = 0; ii < nGeoms; ++ii) { + unsigned int index = skinCluster.indexForOutputConnection(ii,&status); + + if ( !status ) { + MayaError( "Mesh '%s': Error getting geometry index (%s)", mesh->name.c_str(), status.errorString().asChar() ); + } + + // get the dag path of the ii'th geometry + MDagPath skinPath; + status = skinCluster.getPathAtIndex(index,skinPath); + if ( !status ) { + MayaError( "Mesh '%s': Error getting geometry path (%s)", mesh->name.c_str(), status.errorString().asChar() ); + } + + // iterate through the components of this geometry + MItGeometry gIter( skinPath ); + + // print out the influence objects + idList joints; + idExportJoint *joint; + exportVertex_t *vert; + + joints.Resize( nInfs ); + for (size_t kk = 0; kk < nInfs; ++kk) { + const char *c; + MString s; + + s = infs[kk].partialPathName(); + c = s.asChar(); + joint = model.FindJointReal( c ); + if ( !joint ) { + MayaError( "Mesh '%s': joint %s not found", mesh->name.c_str(), c ); + } + + joints.Append( joint ); + } + + for ( /* nothing */ ; !gIter.isDone(); gIter.next() ) { + MObject comp = gIter.component( &status ); + if ( !status ) { + MayaError( "Mesh '%s': Error getting component (%s)", mesh->name.c_str(), status.errorString().asChar() ); + } + + // Get the weights for this vertex (one per influence object) + MFloatArray wts; + unsigned infCount; + status = skinCluster.getWeights(skinPath,comp,wts,infCount); + if ( !status ) { + MayaError( "Mesh '%s': Error getting weights (%s)", mesh->name.c_str(), status.errorString().asChar() ); + } + if (0 == infCount) { + MayaError( "Mesh '%s': Error: 0 influence objects.", mesh->name.c_str() ); + } + + int num = gIter.index(); + vert = &mesh->verts[ num ]; + vert->startweight = mesh->weights.Num(); + + float totalweight = 0.0f; + + // copy the weight data for this vertex + int numNonZeroWeights = 0; + int jj; + for ( jj = 0; jj < (int)infCount ; ++jj ) { + float w = ( float )wts[ jj ]; + if ( w > 0.0f ) { + numNonZeroWeights++; + } + if ( w > options.jointThreshold ) { + weight.joint = joints[ jj ]; + weight.jointWeight = wts[ jj ]; + + if ( !options.ignoreScale ) { + weight.joint->bindmat.ProjectVector( vert->pos - ( weight.joint->bindpos * weight.joint->invscale ), weight.offset ); + weight.offset *= weight.joint->scale; + } else { + weight.joint->bindmat.ProjectVector( vert->pos - weight.joint->bindpos, weight.offset ); + } + mesh->weights.Append( weight ); + totalweight += weight.jointWeight; + } + } + + vert->numWeights = mesh->weights.Num() - vert->startweight; + if ( !vert->numWeights ) { + if ( numNonZeroWeights ) { + MayaError( "Error on mesh '%s': Vertex %d doesn't have any joint weights exceeding jointThreshold (%f).", mesh->name.c_str(), num, options.jointThreshold ); + } else { + MayaError( "Error on mesh '%s': Vertex %d doesn't have any joint weights.", mesh->name.c_str(), num ); + } + } else if ( !totalweight ) { + MayaError( "Error on mesh '%s': Combined weight of 0 on vertex %d.", mesh->name.c_str(), num ); + } + //if ( numNonZeroWeights ) { + // common->Printf( "Mesh '%s': skipped %d out of %d weights on vertex %d\n", mesh->name.c_str(), numNonZeroWeights, numNonZeroWeights + vert->numWeights, num ); + //} + + // normalize the joint weights + for( jj = 0; jj < vert->numWeights; jj++ ) { + mesh->weights[ vert->startweight + jj ].jointWeight /= totalweight; + } + } + break; + } + } + + if ( !count && !options.ignoreMeshes ) { + MayaError( "CreateMesh: No skinClusters found in this scene.\n" ); + } +} + +/* +=============== +idMayaExport::CombineMeshes + +combine surfaces with the same shader. +=============== +*/ +void idMayaExport::CombineMeshes( void ) { + int i, j; + int count; + idExportMesh *mesh; + idExportMesh *combine; + idList oldmeshes; + + oldmeshes = model.meshes; + model.meshes.Clear(); + + count = 0; + for( i = 0; i < oldmeshes.Num(); i++ ) { + mesh = oldmeshes[ i ]; + if ( !mesh->keep ) { + delete mesh; + continue; + } + + combine = NULL; + for( j = 0; j < model.meshes.Num(); j++ ) { + if ( model.meshes[ j ]->shader == mesh->shader ) { + combine = model.meshes[ j ]; + break; + } + } + + if ( combine ) { + combine->Merge( mesh ); + delete mesh; + count++; + } else { + model.meshes.Append( mesh ); + } + } + + // share verts + for( i = 0; i < model.meshes.Num(); i++ ) { + model.meshes[ i ]->ShareVerts(); + } + + common->Printf( "Merged %d meshes\n", count ); +} + +/* +=============== +idMayaExport::GetAlignment +=============== +*/ +void idMayaExport::GetAlignment( idStr &alignName, idMat3 &align, float rotate, int startframe ) { + idVec3 pos; + idExportJoint *joint; + idAngles ang( 0, rotate, 0 ); + idMat3 mat; + + align.Identity(); + + if ( alignName.Length() ) { + SetFrame( 0 ); + + joint = model.FindJoint( alignName ); + if ( !joint ) { + MayaError( "could not find joint '%s' to align model to.\n", alignName.c_str() ); + } + + // found it + GetWorldTransform( joint, pos, mat, 1.0f ); + align[ 0 ][ 0 ] = mat[ 2 ][ 0 ]; + align[ 0 ][ 1 ] = -mat[ 2 ][ 2 ]; + align[ 0 ][ 2 ] = mat[ 2 ][ 1 ]; + + align[ 1 ][ 0 ] = mat[ 0 ][ 0 ]; + align[ 1 ][ 1 ] = -mat[ 0 ][ 2 ]; + align[ 1 ][ 2 ] = mat[ 0 ][ 1 ]; + + align[ 2 ][ 0 ] = mat[ 1 ][ 0 ]; + align[ 2 ][ 1 ] = -mat[ 1 ][ 2 ]; + align[ 2 ][ 2 ] = mat[ 1 ][ 1 ]; + + if ( rotate ) { + align *= ang.ToMat3(); + } + } else if ( rotate ) { + align = ang.ToMat3(); + } + + align.TransposeSelf(); +} + +/* +=============== +idMayaExport::GetObjectType + +return the type of the object +=============== +*/ +const char *idMayaExport::GetObjectType( MObject object ) { + if( object.isNull() ) { + return "(Null)"; + } + + MStatus stat; + MFnDependencyNode dgNode; + MString typeName; + + stat = dgNode.setObject( object ); + typeName = dgNode.typeName( &stat ); + if( MS::kSuccess != stat ) { + // can not get the type name of this object + return "(Unknown)"; + } + + return typeName.asChar(); +} + +/* +=============== +idMayaExport::GetCameraFov +=============== +*/ +float idMayaExport::GetCameraFov( idExportJoint *joint ) { + int childCount; + int j; + double horiz; + double focal; + MStatus status; + const char *n1, *n2; + MFnDagNode *dagnode; + float fov; + + dagnode = joint->dagnode; + + MObject cameraNode = dagnode->object(); + childCount = dagnode->childCount(); + + fov = 90.0f; + for( j = 0; j < childCount; j++ ) { + MObject childNode = dagnode->child( j ); + + n1 = GetObjectType( cameraNode ); + n2 = GetObjectType( childNode ); + if ( ( !strcmp( "transform", n1 ) ) && ( !strcmp( "camera", n2 ) ) ) { + MFnCamera camera( childNode ); + focal = camera.focalLength(); + horiz = camera.horizontalFilmAperture(); + fov = RAD2DEG( 2 * atan( ( horiz * 0.5 ) / ( focal / 25.4 ) ) ); + break; + } + } + + return fov; +} + +/* +=============== +idMayaExport::GetCameraFrame +=============== +*/ +void idMayaExport::GetCameraFrame( idExportJoint *camera, idMat3 &align, cameraFrame_t *cam ) { + idMat3 mat; + idMat3 axis; + idVec3 pos; + + // get the worldspace positions of the joint + GetWorldTransform( camera, pos, axis, 1.0f ); + + // convert to id coordinates + cam->t = ConvertToIdSpace( pos ) * align; + + // correct the orientation for the game + axis = ConvertToIdSpace( axis ) * align; + mat[ 0 ] = -axis[ 2 ]; + mat[ 1 ] = -axis[ 0 ]; + mat[ 2 ] = axis[ 1 ]; + cam->q = mat.ToQuat().ToCQuat(); + + // get it's fov + cam->fov = GetCameraFov( camera ); +} + +/* +=============== +idMayaExport::CreateCameraAnim +=============== +*/ +void idMayaExport::CreateCameraAnim( idMat3 &align ) { + float start, end; + MDagPath dagPath; + int frameNum; + short v; + MStatus status; + cameraFrame_t cam; + idExportJoint *refCam; + idExportJoint *camJoint; + idStr currentCam; + idStr newCam; + MPlug plug; + MFnEnumAttribute cameraAttribute; + + start = TimeForFrame( options.startframe ); + end = TimeForFrame( options.endframe ); + +#if 0 + options.framerate = 60; + model.numFrames = ( int )( ( end - start ) * ( float )options.framerate ) + 1; + model.frameRate = options.framerate; +#else + model.numFrames = options.endframe + 1 - options.startframe; + model.frameRate = options.framerate; +#endif + + common->Printf( "start frame = %d\n end frame = %d\n", options.startframe, options.endframe ); + common->Printf( " start time = %f\n end time = %f\n total time = %f\n", start, end, end - start ); + + if ( start > end ) { + MayaError( "Start frame is greater than end frame." ); + } + + refCam = model.FindJoint( "refcam" ); + if ( refCam == NULL ) { + currentCam = MAYA_DEFAULT_CAMERA; + } else { + MObject cameraNode = refCam->dagnode->object(); + MFnDependencyNode cameraDG( cameraNode, &status ); + if( MS::kSuccess != status ) { + MayaError( "Can't find 'refcam' dependency node." ); + return; + } + + MObject attr = cameraDG.attribute( MString( "Camera" ), &status ); + if( MS::kSuccess != status ) { + MayaError( "Can't find 'Camera' attribute on 'refcam'." ); + return; + } + + plug = MPlug( cameraNode, attr ); + status = cameraAttribute.setObject( attr ); + if( MS::kSuccess != status ) { + MayaError( "Bad 'Camera' attribute on 'refcam'." ); + return; + } + + model.camera.Clear(); + model.cameraCuts.Clear(); + + SetFrame( 0 ); + status = plug.getValue( v ); + currentCam = cameraAttribute.fieldName( v, &status ).asChar(); + if( MS::kSuccess != status ) { + MayaError( "Error getting camera name on frame %d", GetMayaFrameNum( 0 ) ); + } + } + + camJoint = model.FindJoint( currentCam ); + if ( !camJoint ) { + MayaError( "Couldn't find camera '%s'", currentCam.c_str() ); + } + + for( frameNum = 0; frameNum < model.numFrames; frameNum++ ) { + common->Printf( "\rFrame %d/%d...", options.startframe + frameNum, options.endframe ); + +#if 0 + MTime time; + time.setUnit( MTime::kSeconds ); + time.setValue( start + ( ( float )frameNum / ( float )options.framerate ) ); + MGlobal::viewFrame( time ); +#else + SetFrame( frameNum ); +#endif + + // get the position for this frame + GetCameraFrame( camJoint, align, &model.camera.Alloc() ); + + if ( refCam != NULL ) { + status = plug.getValue( v ); + newCam = cameraAttribute.fieldName( v, &status ).asChar(); + if( MS::kSuccess != status ) { + MayaError( "Error getting camera name on frame %d", GetMayaFrameNum( frameNum ) ); + } + + if ( newCam != currentCam ) { + // place a cut at our last frame + model.cameraCuts.Append( model.camera.Num() - 1 ); + + currentCam = newCam; + camJoint = model.FindJoint( currentCam ); + if ( !camJoint ) { + MayaError( "Couldn't find camera '%s'", currentCam.c_str() ); + } + + // get the position for this frame + GetCameraFrame( camJoint, align, &model.camera.Alloc() ); + } + } + } + + common->Printf( "\n" ); +} + +/* +=============== +idMayaExport::GetDefaultPose +=============== +*/ +void idMayaExport::GetDefaultPose( idMat3 &align ) { + float start; + MDagPath dagPath; + idMat3 jointaxis; + idVec3 jointpos; + idExportJoint *joint, *parent; + idBounds bnds; + idBounds meshBounds; + idList frame; + + start = TimeForFrame( options.startframe ); + + common->Printf( "default pose frame = %d\n", options.startframe ); + common->Printf( " default pose time = %f\n", start ); + + frame.SetNum( model.joints.Num() ); + SetFrame( 0 ); + + // convert joints into local coordinates and save in channels + for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + if ( !joint->dagnode ) { + // custom origin joint + joint->idwm.Identity(); + joint->idt.Zero(); + frame[ joint->index ].t.Zero(); + frame[ joint->index ].q.Set( 0.0f, 0.0f, 0.0f ); + continue; + } + + // get the worldspace positions of the joint + GetWorldTransform( joint, jointpos, jointaxis, options.scale ); + + // convert to id coordinates + jointaxis = ConvertToIdSpace( jointaxis ) * align; + jointpos = ConvertToIdSpace( jointpos ) * align; + + // save worldspace position of joint for children + joint->idwm = jointaxis; + joint->idt = jointpos; + + parent = joint->exportNode.GetParent(); + if ( parent ) { + // convert to local coordinates + jointpos = ( jointpos - parent->idt ) * parent->idwm.Transpose(); + jointaxis = jointaxis * parent->idwm.Transpose(); + } else if ( joint->name == "origin" ) { + if ( options.clearOrigin ) { + jointpos.Zero(); + } + if ( options.clearOriginAxis ) { + jointaxis.Identity(); + } + } + + frame[ joint->index ].t = jointpos; + frame[ joint->index ].q = jointaxis.ToQuat().ToCQuat(); + } + + // relocate origin to start at 0, 0, 0 for first frame + joint = model.FindJoint( "origin" ); + if ( joint ) { + frame[ joint->index ].t.Zero(); + } + + // transform the hierarchy + for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + jointpos = frame[ joint->index ].t; + jointaxis = frame[ joint->index ].q.ToQuat().ToMat3(); + + parent = joint->exportNode.GetParent(); + if ( parent ) { + joint->idwm = jointaxis * parent->idwm; + joint->idt = parent->idt + jointpos * parent->idwm; + } else { + joint->idwm = jointaxis; + joint->idt = jointpos; + } + + joint->bindmat = joint->idwm; + joint->bindpos = joint->idt; + } + + common->Printf( "\n" ); +} + +/* +=============== +idMayaExport::CreateAnimation +=============== +*/ +void idMayaExport::CreateAnimation( idMat3 &align ) { + int i; + float start, end; + MDagPath dagPath; + idMat3 jointaxis; + idVec3 jointpos; + int frameNum; + idExportJoint *joint, *parent; + idBounds bnds; + idBounds meshBounds; + jointFrame_t *frame; + int cycleStart; + idVec3 totalDelta; + idList copyFrames; + + start = TimeForFrame( options.startframe ); + end = TimeForFrame( options.endframe ); + + model.numFrames = options.endframe + 1 - options.startframe; + model.frameRate = options.framerate; + + common->Printf( "start frame = %d\n end frame = %d\n", options.startframe, options.endframe ); + common->Printf( " start time = %f\n end time = %f\n total time = %f\n", start, end, end - start ); + + if ( start > end ) { + MayaError( "Start frame is greater than end frame." ); + } + + model.bounds.SetNum( model.numFrames ); + model.jointFrames.SetNum( model.numFrames * model.joints.Num() ); + model.frames.SetNum( model.numFrames ); + for( i = 0; i < model.numFrames; i++ ) { + model.frames[ i ] = &model.jointFrames[ model.joints.Num() * i ]; + } + + // *sigh*. cyclestart doesn't work nicely with the anims. + // may just want to not do it in SetTime anymore. + cycleStart = options.cycleStart; + options.cycleStart = options.startframe; + + for( frameNum = 0; frameNum < model.numFrames; frameNum++ ) { + common->Printf( "\rFrame %d/%d...", options.startframe + frameNum, options.endframe ); + + frame = model.frames[ frameNum ]; + SetFrame( frameNum ); + + // convert joints into local coordinates and save in channels + for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + if ( !joint->dagnode ) { + // custom origin joint + joint->idwm.Identity(); + joint->idt.Zero(); + frame[ joint->index ].t.Zero(); + frame[ joint->index ].q.Set( 0.0f, 0.0f, 0.0f ); + continue; + } + + // get the worldspace positions of the joint + GetWorldTransform( joint, jointpos, jointaxis, options.scale ); + + // convert to id coordinates + jointaxis = ConvertToIdSpace( jointaxis ) * align; + jointpos = ConvertToIdSpace( jointpos ) * align; + + // save worldspace position of joint for children + joint->idwm = jointaxis; + joint->idt = jointpos; + + parent = joint->exportNode.GetParent(); + if ( parent ) { + // convert to local coordinates + jointpos = ( jointpos - parent->idt ) * parent->idwm.Transpose(); + jointaxis = jointaxis * parent->idwm.Transpose(); + } else if ( joint->name == "origin" ) { + if ( options.clearOrigin ) { + jointpos.Zero(); + } + if ( options.clearOriginAxis ) { + jointaxis.Identity(); + } + } + + frame[ joint->index ].t = jointpos; + frame[ joint->index ].q = jointaxis.ToQuat().ToCQuat(); + } + } + + options.cycleStart = cycleStart; + totalDelta.Zero(); + + joint = model.FindJoint( "origin" ); + if ( joint ) { + frame = model.frames[ 0 ]; + idVec3 origin = frame[ joint->index ].t; + + frame = model.frames[ model.numFrames - 1 ]; + totalDelta = frame[ joint->index ].t - origin; + } + + // shift the frames when cycleStart is used + if ( options.cycleStart > options.startframe ) { + copyFrames = model.jointFrames; + for( i = 0; i < model.numFrames; i++ ) { + bool shiftorigin = false; + frameNum = i + ( options.cycleStart - options.startframe ); + if ( frameNum >= model.numFrames ) { + // wrap around, skipping the first frame, since it's a dupe of the last frame + frameNum -= model.numFrames - 1; + shiftorigin = true; + } + + memcpy( &model.jointFrames[ model.joints.Num() * i ], ©Frames[ model.joints.Num() * frameNum ], model.joints.Num() * sizeof( copyFrames[ 0 ] ) ); + + if ( joint && shiftorigin ) { + model.frames[ i ][ joint->index ].t += totalDelta; + } + } + } + + if ( joint ) { + // relocate origin to start at 0, 0, 0 for first frame + frame = model.frames[ 0 ]; + idVec3 origin = frame[ joint->index ].t; + for( i = 0; i < model.numFrames; i++ ) { + frame = model.frames[ i ]; + frame[ joint->index ].t -= origin; + } + } + + // get the bounds for each frame + for( frameNum = 0; frameNum < model.numFrames; frameNum++ ) { + frame = model.frames[ frameNum ]; + + // transform the hierarchy + for( joint = model.exportHead.GetNext(); joint != NULL; joint = joint->exportNode.GetNext() ) { + jointpos = frame[ joint->index ].t; + jointaxis = frame[ joint->index ].q.ToQuat().ToMat3(); + + parent = joint->exportNode.GetParent(); + if ( parent ) { + joint->idwm = jointaxis * parent->idwm; + joint->idt = parent->idt + jointpos * parent->idwm; + } else { + joint->idwm = jointaxis; + joint->idt = jointpos; + } + } + + // get bounds for this frame + bnds.Clear(); + for( i = 0; i < model.meshes.Num(); i++ ) { + if ( model.meshes[ i ]->keep ) { + model.meshes[ i ]->GetBounds( meshBounds ); + bnds.AddBounds( meshBounds ); + } + } + model.bounds[ frameNum ][ 0 ] = bnds[ 0 ]; + model.bounds[ frameNum ][ 1 ] = bnds[ 1 ]; + } + + common->Printf( "\n" ); +} + +/* +=============== +idMayaExport::ConvertModel +=============== +*/ +void idMayaExport::ConvertModel( void ) { + MStatus status; + idMat3 align; + + common->Printf( "Converting %s to %s...\n", options.src.c_str(), options.dest.c_str() ); + + // see if the destination file exists + FILE *file = fopen( options.dest, "r" ); + if ( file ) { + fclose( file ); + + // make sure we can write to the destination + FILE *file = fopen( options.dest, "r+" ); + if ( !file ) { + MayaError( "Unable to write to the file '%s'", options.dest.c_str() ); + } + fclose( file ); + } + + MString filename( options.src ); + MFileIO::newFile( true ); + + // Load the file into Maya + common->Printf( "Loading file...\n" ); + status = MFileIO::open( filename, NULL, true ); + if ( !status ) { + MayaError( "Error loading '%s': '%s'\n", filename.asChar(), status.errorString().asChar() ); + } + + // force Maya to update the frame. When using references, sometimes + // the model is posed the way it is in the source. Since Maya only + // updates it when the frame time changes to a value other than the + // current, just setting the time to the min time doesn't guarantee + // that the model gets updated. + MGlobal::viewFrame( MAnimControl::maxTime() ); + MGlobal::viewFrame( MAnimControl::minTime() ); + + if ( options.startframe < 0 ) { + options.startframe = MAnimControl::minTime().as( MTime::kFilm ); + } + + if ( options.endframe < 0 ) { + options.endframe = MAnimControl::maxTime().as( MTime::kFilm ); + } + if ( options.cycleStart < 0 ) { + options.cycleStart = options.startframe; + } else if ( ( options.cycleStart < options.startframe ) || ( options.cycleStart > options.endframe ) ) { + MayaError( "cycleStart (%d) out of frame range (%d to %d)\n", options.cycleStart, options.startframe, options.endframe ); + } else if ( options.cycleStart == options.endframe ) { + // end frame is a duplicate of the first frame in cycles, so just disable cycleStart + options.cycleStart = options.startframe; + } + + // create a list of the transform nodes that will make up our heirarchy + common->Printf( "Creating joints...\n" ); + CreateJoints( options.scale ); + if ( options.type != WRITE_CAMERA ) { + common->Printf( "Creating meshes...\n" ); + CreateMesh( options.scale ); + common->Printf( "Renaming joints...\n" ); + RenameJoints( options.renamejoints, options.prefix ); + common->Printf( "Remapping parents...\n" ); + RemapParents( options.remapjoints ); + common->Printf( "Pruning joints...\n" ); + PruneJoints( options.keepjoints, options.prefix ); + common->Printf( "Combining meshes...\n" ); + CombineMeshes(); + } + + common->Printf( "Align model...\n" ); + GetAlignment( options.align, align, options.rotate, 0 ); + + switch( options.type ) { + case WRITE_MESH : + common->Printf( "Grabbing default pose:\n" ); + GetDefaultPose( align ); + common->Printf( "Writing file...\n" ); + if ( !model.WriteMesh( options.dest, options ) ) { + MayaError( "error writing to '%s'", options.dest.c_str() ); + } + break; + + case WRITE_ANIM : + common->Printf( "Creating animation frames:\n" ); + CreateAnimation( align ); + common->Printf( "Writing file...\n" ); + if ( !model.WriteAnim( options.dest, options ) ) { + MayaError( "error writing to '%s'", options.dest.c_str() ); + } + break; + + case WRITE_CAMERA : + common->Printf( "Creating camera frames:\n" ); + CreateCameraAnim( align ); + + common->Printf( "Writing file...\n" ); + if ( !model.WriteCamera( options.dest, options ) ) { + MayaError( "error writing to '%s'", options.dest.c_str() ); + } + break; + } + + common->Printf( "done\n\n" ); +} + +/* +=============== +idMayaExport::ConvertToMD3 +=============== +*/ +void idMayaExport::ConvertToMD3( void ) { +#if 0 + int i, j; + md3Header_t *pinmodel; + md3Frame_t *frame; + md3Surface_t *surf; + md3Shader_t *shader; + md3Triangle_t *tri; + md3St_t *st; + md3XyzNormal_t *xyz; + md3Tag_t *tag; + int version; + int size; + + //model_t *mod, int lod, void *buffer, const char *mod_name + + pinmodel = (md3Header_t *)buffer; + + version = LittleLong (pinmodel->version); + if (version != MD3_VERSION) { + common->Printf( "R_LoadMD3: %s has wrong version (%i should be %i)\n", + mod_name, version, MD3_VERSION); + return qfalse; + } + + mod->type = MOD_MESH; + size = LittleLong(pinmodel->ofsEnd); + mod->dataSize += size; + mod->md3[lod] = ri.Hunk_Alloc( size ); + + memcpy (mod->md3[lod], buffer, LittleLong(pinmodel->ofsEnd) ); + + LL(mod->md3[lod]->ident); + LL(mod->md3[lod]->version); + LL(mod->md3[lod]->numFrames); + LL(mod->md3[lod]->numTags); + LL(mod->md3[lod]->numSurfaces); + LL(mod->md3[lod]->ofsFrames); + LL(mod->md3[lod]->ofsTags); + LL(mod->md3[lod]->ofsSurfaces); + LL(mod->md3[lod]->ofsEnd); + + if ( mod->md3[lod]->numFrames < 1 ) { + common->Printf( "R_LoadMD3: %s has no frames\n", mod_name ); + return qfalse; + } + + // swap all the frames + frame = (md3Frame_t *) ( (byte *)mod->md3[lod] + mod->md3[lod]->ofsFrames ); + for ( i = 0 ; i < mod->md3[lod]->numFrames ; i++, frame++) { + frame->radius = LittleFloat( frame->radius ); + for ( j = 0 ; j < 3 ; j++ ) { + frame->bounds[0][j] = LittleFloat( frame->bounds[0][j] ); + frame->bounds[1][j] = LittleFloat( frame->bounds[1][j] ); + frame->localOrigin[j] = LittleFloat( frame->localOrigin[j] ); + } + } + + // swap all the tags + tag = (md3Tag_t *) ( (byte *)mod->md3[lod] + mod->md3[lod]->ofsTags ); + for ( i = 0 ; i < mod->md3[lod]->numTags * mod->md3[lod]->numFrames ; i++, tag++) { + for ( j = 0 ; j < 3 ; j++ ) { + tag->origin[j] = LittleFloat( tag->origin[j] ); + tag->axis[0][j] = LittleFloat( tag->axis[0][j] ); + tag->axis[1][j] = LittleFloat( tag->axis[1][j] ); + tag->axis[2][j] = LittleFloat( tag->axis[2][j] ); + } + } + + // swap all the surfaces + surf = (md3Surface_t *) ( (byte *)mod->md3[lod] + mod->md3[lod]->ofsSurfaces ); + for ( i = 0 ; i < mod->md3[lod]->numSurfaces ; i++) { + + LL(surf->ident); + LL(surf->flags); + LL(surf->numFrames); + LL(surf->numShaders); + LL(surf->numTriangles); + LL(surf->ofsTriangles); + LL(surf->numVerts); + LL(surf->ofsShaders); + LL(surf->ofsSt); + LL(surf->ofsXyzNormals); + LL(surf->ofsEnd); + + if ( surf->numVerts > SHADER_MAX_VERTEXES ) { + ri.Error (ERR_DROP, "R_LoadMD3: %s has more than %i verts on a surface (%i)", + mod_name, SHADER_MAX_VERTEXES, surf->numVerts ); + } + if ( surf->numTriangles*3 > SHADER_MAX_INDEXES ) { + ri.Error (ERR_DROP, "R_LoadMD3: %s has more than %i triangles on a surface (%i)", + mod_name, SHADER_MAX_INDEXES / 3, surf->numTriangles ); + } + + // change to surface identifier + surf->ident = SF_MD3; + + // lowercase the surface name so skin compares are faster + Q_strlwr( surf->name ); + + // strip off a trailing _1 or _2 + // this is a crutch for q3data being a mess + j = strlen( surf->name ); + if ( j > 2 && surf->name[j-2] == '_' ) { + surf->name[j-2] = 0; + } + + // register the shaders + shader = (md3Shader_t *) ( (byte *)surf + surf->ofsShaders ); + for ( j = 0 ; j < surf->numShaders ; j++, shader++ ) { + shader_t *sh; + + sh = R_FindShader( shader->name, LIGHTMAP_NONE, qtrue ); + if ( sh->defaultShader ) { + shader->shaderIndex = 0; + } else { + shader->shaderIndex = sh->index; + } + } + + // swap all the triangles + tri = (md3Triangle_t *) ( (byte *)surf + surf->ofsTriangles ); + for ( j = 0 ; j < surf->numTriangles ; j++, tri++ ) { + LL(tri->indexes[0]); + LL(tri->indexes[1]); + LL(tri->indexes[2]); + } + + // swap all the ST + st = (md3St_t *) ( (byte *)surf + surf->ofsSt ); + for ( j = 0 ; j < surf->numVerts ; j++, st++ ) { + st->st[0] = LittleFloat( st->st[0] ); + st->st[1] = LittleFloat( st->st[1] ); + } + + // swap all the XyzNormals + xyz = (md3XyzNormal_t *) ( (byte *)surf + surf->ofsXyzNormals ); + for ( j = 0 ; j < surf->numVerts * surf->numFrames ; j++, xyz++ ) + { + xyz->xyz[0] = LittleShort( xyz->xyz[0] ); + xyz->xyz[1] = LittleShort( xyz->xyz[1] ); + xyz->xyz[2] = LittleShort( xyz->xyz[2] ); + + xyz->normal = LittleShort( xyz->normal ); + } + + + // find the next surface + surf = (md3Surface_t *)( (byte *)surf + surf->ofsEnd ); + } + return true; +#endif +} + +/* +============================================================================== + +dll setup + +============================================================================== +*/ + +/* +=============== +Maya_Shutdown +=============== +*/ +void Maya_Shutdown( void ) { + if ( initialized ) { + errorMessage.Clear(); + initialized = false; + + // This shuts down the entire app somehow, so just ignore it. + //MLibrary::cleanup(); + } +} + +/* +=============== +Maya_ConvertModel +=============== +*/ +const char *Maya_ConvertModel( const char *ospath, const char *commandline ) { + + errorMessage = "Ok"; + + try { + idExportOptions options( commandline, ospath ); + idMayaExport export( options ); + + export.ConvertModel(); + } + + catch( idException &exception ) { + errorMessage = exception.error; + } + + return errorMessage; +} + +/* +=============== +dllEntry +=============== +*/ +bool dllEntry( int version, idCommon *common, idSys *sys ) { + + if ( !common || !sys ) { + return false; + } + + ::common = common; + ::sys = sys; + ::cvarSystem = NULL; + + idLib::sys = sys; + idLib::common = common; + idLib::cvarSystem = NULL; + idLib::fileSystem = NULL; + + idLib::Init(); + + if ( version != MD5_VERSION ) { + common->Printf( "Error initializing Maya exporter: DLL version %d different from .exe version %d\n", MD5_VERSION, version ); + return false; + } + + if ( !initialized ) { + MStatus status; + + status = MLibrary::initialize( GAME_NAME, true ); + if ( !status ) { + common->Printf( "Error calling MLibrary::initialize (%s)\n", status.errorString().asChar() ); + return false; + } + + initialized = true; + } + + return true; +} + +// Force type checking on the interface functions to help ensure that they match the ones in the .exe +const exporterDLLEntry_t ValidateEntry = &dllEntry; +const exporterInterface_t ValidateConvert = &Maya_ConvertModel; +const exporterShutdown_t ValidateShutdown = &Maya_Shutdown; diff --git a/src/MayaImport/maya_main.h b/src/MayaImport/maya_main.h new file mode 100644 index 0000000..af94187 --- /dev/null +++ b/src/MayaImport/maya_main.h @@ -0,0 +1,18 @@ + +#ifndef __MAYA_MAIN_H__ +#define __MAYA_MAIN_H__ + +/* +============================================================== + + Maya Import + +============================================================== +*/ + + +typedef bool ( *exporterDLLEntry_t )( int version, idCommon *common, idSys *sys ); +typedef const char *( *exporterInterface_t )( const char *ospath, const char *commandline ); +typedef void ( *exporterShutdown_t )( void ); + +#endif /* !__MAYA_MAIN_H__ */ diff --git a/src/MayaImport/mayaimport.def b/src/MayaImport/mayaimport.def new file mode 100644 index 0000000..adb9528 --- /dev/null +++ b/src/MayaImport/mayaimport.def @@ -0,0 +1,4 @@ +EXPORTS + dllEntry + Maya_ConvertModel + Maya_Shutdown diff --git a/src/PREY.sln b/src/PREY.sln new file mode 100644 index 0000000..019bcdf --- /dev/null +++ b/src/PREY.sln @@ -0,0 +1,38 @@ +Microsoft Visual Studio Solution File, Format Version 9.00 +# Visual Studio 2005 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Game", "2005game.vcproj", "{49BEC5C6-B964-417A-851E-808886B57430}" + ProjectSection(ProjectDependencies) = postProject + {49BEC5C6-B964-417A-851E-808886B57400} = {49BEC5C6-B964-417A-851E-808886B57400} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "idLib", "2005idlib.vcproj", "{49BEC5C6-B964-417A-851E-808886B57400}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug with inlines and memory log|Win32 = Debug with inlines and memory log|Win32 + Debug with inlines|Win32 = Debug with inlines|Win32 + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {49BEC5C6-B964-417A-851E-808886B57430}.Debug with inlines and memory log|Win32.ActiveCfg = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57430}.Debug with inlines and memory log|Win32.Build.0 = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57430}.Debug with inlines|Win32.ActiveCfg = Debug with inlines|Win32 + {49BEC5C6-B964-417A-851E-808886B57430}.Debug with inlines|Win32.Build.0 = Debug with inlines|Win32 + {49BEC5C6-B964-417A-851E-808886B57430}.Debug|Win32.ActiveCfg = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57430}.Debug|Win32.Build.0 = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57430}.Release|Win32.ActiveCfg = Release|Win32 + {49BEC5C6-B964-417A-851E-808886B57430}.Release|Win32.Build.0 = Release|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Debug with inlines and memory log|Win32.ActiveCfg = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Debug with inlines and memory log|Win32.Build.0 = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Debug with inlines|Win32.ActiveCfg = Debug with inlines|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Debug with inlines|Win32.Build.0 = Debug with inlines|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Debug|Win32.ActiveCfg = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Debug|Win32.Build.0 = Debug|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Release|Win32.ActiveCfg = Release|Win32 + {49BEC5C6-B964-417A-851E-808886B57400}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/Prey/ai_Navigator.cpp b/src/Prey/ai_Navigator.cpp new file mode 100644 index 0000000..9a2cd71 --- /dev/null +++ b/src/Prey/ai_Navigator.cpp @@ -0,0 +1,268 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + + +/* +================= +hhNavigator::hhNavigator() +================= +*/ +hhNavigator::hhNavigator(void) { + + + hhSelf = NULL; + +// followStateFunction = FollowAllyStay; + +} //. hhNavigator::hhNavigator(void) + + +/* +======================== +hhNavigator::Spawn(void) +======================== +*/ + +// Required for CLASS_DECLARATION +void hhNavigator::Spawn(void) { + + +} //. hhNavigator::hhNavigator(void) + + +/* +======================================= +hhNavigator::SetOwner(idAI *owner) +======================================= +*/ +void hhNavigator::SetOwner(idAI *owner) { + + idNavigator::SetOwner(owner); + + if (owner->IsType(hhAI::Type)) { + hhSelf = static_cast(owner); + } + +} //. hhNavigator::SetOwner(idAI *) + +/* JRM- Removed because brain stuff handles this logic now +// +//=============================== +//hhNavigator::SetAlly(idActor *) +//=============================== +// +void hhNavigator::SetAlly(idActor *ally) { + + if (ally != NULL) { + FollowAlly(ally); + } + +} //. hhNavigator::SetAlly(idActor *) + + +// +//============================= +//hhNavigator::FollowAlly(ally) +//============================= +// +void hhNavigator::FollowAlly(idActor *ally) { + + // If we aren't on an hhAI, we can't follow, so leave + if (hhSelf == NULL) { + return; + } + + + + (this->*followStateFunction)(ally); + + + +} //. hhNavigator::FollowAlly(idActor *ally) +*/ + + + + + +/* HUMANHEAD JRM - REMOVED +// +//=================================== +//hhNavigator::FollowAllyStay(void) +//=================================== +// +void hhNavigator::FollowAllyStay(idActor *ally) { + static bool first = true; + + + if (first) { + if (ai_debug->integer > 0) { + gameLocal.Printf("in FollowAllyStay\n"); + } + first = false; + } + + if (hhSelf->AI_ALLY_FAR || + !hhSelf->AI_ALLY_VISIBLE) { + followStateFunction = FollowAllyFollow; + first = true; + } + else if (hhSelf->AI_ALLY_TOUCHED) { + followStateFunction = FollowAllyLead; + first = true; + } + else { // Just stay still + moveType = MOVE_NONE; + } + + +} //. hhNavigator::FollowAllyStay(void) + + +// +//=================================== +//hhNavigator::FollowAllyFollow(void) +//=================================== +// +void hhNavigator::FollowAllyFollow(idActor *ally) { + static bool first = true; + + + hhSelf->AI_FOLLOW_ALLY = true; + + if (first) { + if (ai_debug->integer > 0) { + gameLocal.Printf("in FollowAllyFollow\n"); + } + first = false; + } + + if (hhSelf->AI_ALLY_NEAR) { + StopMove(); + + followStateFunction = FollowAllyStay; + hhSelf->AI_FOLLOW_ALLY = false; + first = true; + } + // In case the min_dist is too close + else if (hhSelf->AI_ALLY_TOUCHED) { + followStateFunction = FollowAllyLead; + hhSelf->AI_FOLLOW_ALLY = false; + first = true; + } + else { + moveDest = ally->GetFloorPos(); + goal = ally; + moveType = MOVE_TO_ALLY; + + if (aas) { + toAreaNum = aas->PointReachableAreaNum( moveDest ); + toAreaEnemy = false; + } + } //. Follow the player + +} //. hhNavigator::FollowAllyFollow(void) + + +// +//=================================== +//hhNavigator::FollowAllyLead(void) +//=================================== +// +void hhNavigator::FollowAllyLead(idActor *ally) { + static bool first = true; + static int nextUpdateTime; + idVec3 leadPosition; + + + hhSelf->AI_LEAD_ALLY = true; + + if (first) { + if (ai_debug->integer > 0) { + gameLocal.Printf("in FollowAllyLead\n"); + } + first = false; + } + + // Reset this variable, as it was used to get here. + if (hhSelf->AI_ALLY_TOUCHED) { + hhSelf->AI_ALLY_TOUCHED = false; + nextUpdateTime = 0; + } + + if (gameLocal.time * 1000.0f >= nextUpdateTime) { + //if (nextUpdateTime == 0) { + + leadPosition = FindNewLeadPosition(ally); + + if (leadPosition != vec3_origin) { + moveDest = leadPosition; + moveType = MOVE_TO_ALLY; + goal = NULL; + + if (aas) { + toAreaNum = aas->PointReachableAreaNum(moveDest); + toAreaEnemy = false; + } + + nextUpdateTime = gameLocal.time * 1000.0f + + hhSelf->follow_lead_update_time; + } + + } //. Time to update position + + if (!hhSelf->AI_ALLY_NEAR) { + followStateFunction = FollowAllyStay; + hhSelf->AI_LEAD_ALLY = false; + first = true; + } + + +} //. hhNavigator::FollowAllyLead(void) + + +// +//================================ +//hhNavigator::FindNewLeadPosition +//================================ +// +idVec3 hhNavigator::FindNewLeadPosition(idActor *ally) { + idVec3 direction; + aasTrace_t trace; + + + direction = hhSelf->GetFloorPos() - ally->GetFloorPos(); + direction.Normalize(); + direction *= hhSelf->follow_min_dist; + + if (aas) { + idVec3 bestPos; + + bestPos = aas->FindNearestPoint(ally->GetFloorPos(), + hhSelf->GetFloorPos(), + hhSelf->follow_min_dist); + if (bestPos != vec3_origin) { + return(bestPos); + } + + // Didn't find a good point + + // Just move as far as we can in the direction nudged + aas->Trace(trace, hhSelf->GetPhysics()->GetOrigin(), + hhSelf->GetPhysics()->GetOrigin() + direction); + + //? How does this work on slopes? + //! Do we want to use the floor position? + return(ally->GetFloorPos() + direction * trace.fraction); + } + + return(ally->GetFloorPos() + direction); + + + +} //. hhNavigator::FindNewLeadPosition(idActor *) +*/ diff --git a/src/Prey/ai_Navigator.h b/src/Prey/ai_Navigator.h new file mode 100644 index 0000000..a379653 --- /dev/null +++ b/src/Prey/ai_Navigator.h @@ -0,0 +1,41 @@ + +#ifndef __PREY_AI_NAVIGATOR_H__ +#define __PREY_AI_NAVIGATOR_H__ + +// Forward declaration for hhAI +class hhAI; + +class hhNavigator : public idNavigator { + +public: + + hhNavigator(void); + + void Spawn(void); + + //virtual void SetAlly(idActor *ally); JRM removed + + virtual void SetOwner(idAI *owner); + + virtual boolean IsNearDest( void ); + +protected: + + hhAI * hhSelf; + + //virtual void FollowAlly(idActor *ally); JRM removed + + /* JRM removed + void FollowAllyStay(idActor *ally); + void FollowAllyFollow(idActor *ally); + void FollowAllyLead(idActor *ally); + */ + +// void (hhNavigator::* followStateFunction) (idActor *ally); JRM removed + + //idVec3 FindNewLeadPosition(idActor *ally); JRM removed + +}; + + +#endif /* __PREY_AI_NAVIGATOR_H__ */ diff --git a/src/Prey/ai_centurion.cpp b/src/Prey/ai_centurion.cpp new file mode 100644 index 0000000..5d04c40 --- /dev/null +++ b/src/Prey/ai_centurion.cpp @@ -0,0 +1,529 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef AI_CenturionLaunch("centurionFire", "d"); //called from anims to handle firing for centurion +const idEventDef AI_CenturionRoar("centurionRoar"); //called from level-script to cause centurion to roar +const idEventDef AI_CenturionArmChop("centurionArmChop"); //called from level-script to actually cause centurion to loose arm +const idEventDef AI_CenturionLooseArm("centurionLooseArm"); //internal: used to actually 'loose' arm +const idEventDef AI_CenturionForceFieldNotify("preForcefieldNotify"); +const idEventDef AI_CenturionForceFieldToggle("forcefieldToggle", "d"); +const idEventDef AI_CenturionInTunnel("playerInBox", "dE"); +const idEventDef AI_CenturionReachedTunnel("reachedTunnel"); +const idEventDef AI_CenturionMoveToTunnel("moveToTunnel", NULL, 'd'); +const idEventDef AI_CheckForObstructions("checkForObstructions", "d", 'd'); +const idEventDef AI_DestroyObstruction("destroyObstruction"); +const idEventDef AI_MoveToObstruction("moveToObstruction"); +const idEventDef AI_ReachedObstruction("reachedObstruction"); +const idEventDef AI_CloseToObstruction("closeToObstruction", 0, 'd'); +const idEventDef AI_BackhandImpulse("", "e"); +const idEventDef AI_EnemyCloseToObstruction("enemyCloseToObstruction", NULL, 'd'); +const idEventDef AI_TakeDamage("takeDamage", "d"); +const idEventDef AI_FindNearbyEnemy("findNearbyEnemy", "f", 'e'); + +CLASS_DECLARATION( hhMonsterAI, hhCenturion ) + EVENT( AI_CenturionLaunch, hhCenturion::Event_CenturionLaunch ) + EVENT( AI_CenturionRoar, hhCenturion::Event_ScriptedRoar ) + EVENT( AI_CenturionArmChop, hhCenturion::Event_ScriptedArmChop ) + EVENT( AI_CenturionLooseArm, hhCenturion::Event_CenturionLooseArm ) + EVENT( AI_CenturionInTunnel, hhCenturion::Event_PlayerInTunnel ) + EVENT( AI_CenturionReachedTunnel, hhCenturion::Event_ReachedTunnel ) + EVENT( AI_CenturionMoveToTunnel, hhCenturion::Event_MoveToTunnel ) + EVENT( AI_CenturionForceFieldNotify, hhCenturion::Event_ForceFieldNotify ) + EVENT( AI_CenturionForceFieldToggle, hhCenturion::Event_ForceFieldToggle ) + EVENT( AI_CheckForObstructions, hhCenturion::Event_CheckForObstruction ) + EVENT( AI_MoveToObstruction, hhCenturion::Event_MoveToObstruction ) + EVENT( AI_DestroyObstruction, hhCenturion::Event_DestroyObstruction ) + EVENT( AI_ReachedObstruction, hhCenturion::Event_ReachedObstruction ) + EVENT( AI_CloseToObstruction, hhCenturion::Event_CloseToObstruction ) + EVENT( AI_BackhandImpulse, hhCenturion::Event_BackhandImpulse ) + EVENT( AI_EnemyCloseToObstruction, hhCenturion::Event_EnemyCloseToObstruction ) + EVENT( AI_TakeDamage, hhCenturion::Event_TakeDamage ) + EVENT( AI_FindNearbyEnemy, hhCenturion::Event_FindNearbyEnemy ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +void hhCenturion::Spawn() { + AI_CENTURION_ARM_MISSING = false; + AI_CENTURION_REQUIRE_ROAR = false; + AI_CENTURION_ARM_TUNNEL = false; + AI_CENTURION_SCRIPTED_ROAR = 0; + AI_CENTURION_FORCEFIELD_WAIT = false; + AI_CENTURION_SCRIPTED_TUNNEL = 0; +} + +void hhCenturion::Event_PostSpawn( void ) { + hhMonsterAI::Event_PostSpawn(); + + const idKeyValue *kv = spawnArgs.MatchPrefix( "aasObstacle" ); + idEntityPtr ent; + while ( kv ) { + ent = gameLocal.FindEntity( kv->GetValue() ); + if ( ent.IsValid() ) { + obstacles.AddUnique( ent ); + } + kv = spawnArgs.MatchPrefix( "aasObstacle", kv ); + } +} + +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhCenturion::LinkScriptVariables( void ) { + hhMonsterAI::LinkScriptVariables(); + + LinkScriptVariable(AI_CENTURION_ARM_MISSING); + LinkScriptVariable(AI_CENTURION_REQUIRE_ROAR); + LinkScriptVariable(AI_CENTURION_ARM_TUNNEL); + LinkScriptVariable(AI_CENTURION_SCRIPTED_ROAR); + LinkScriptVariable(AI_CENTURION_SCRIPTED_TUNNEL); + LinkScriptVariable(AI_CENTURION_FORCEFIELD_WAIT); +} + + +void hhCenturion::Event_ForceFieldNotify() { + if( !armchop_Target.GetEntity() ) { + return; //must not be in arm-chop mode, so we don't care yet + } + AI_CENTURION_REQUIRE_ROAR = true; + AI_CENTURION_FORCEFIELD_WAIT = true; +} + +void hhCenturion::Event_ForceFieldToggle( int toggle ) { + if( !armchop_Target.GetEntity() ) { + return; + } + AI_CENTURION_FORCEFIELD_WAIT = false; + if (!toggle) { + AI_CENTURION_REQUIRE_ROAR = false; + } else if( AI_CENTURION_ARM_TUNNEL ) { + Event_ScriptedArmChop(); + return; + } +} + +void hhCenturion::Event_PlayerInTunnel( int toggle, idEntity* ent ) { + if( AI_CENTURION_ARM_MISSING ) { //don't care if the player goes back in tunnel once we already lost our arm + return; + } + + if( toggle ) { + if( !ent ) { + gameLocal.DWarning( "entity needs to be present for tunnel event" ); + return; + } + armchop_Target = ent; + AI_CENTURION_SCRIPTED_TUNNEL = 1; + } + else { + armchop_Target = NULL; + AI_CENTURION_SCRIPTED_TUNNEL = 0; + } +} + +void hhCenturion::Event_MoveToTunnel() { + if (!armchop_Target.IsValid()) { + idThread::ReturnInt(0); + return; + } + + StopMove(MOVE_STATUS_DONE); + MoveToPosition(armchop_Target->GetOrigin()); + idThread::ReturnInt(1); +} + +void hhCenturion::Event_ReachedTunnel() { + if( armchop_Target.IsValid() ) { + idAngles faceAngles = armchop_Target->GetAxis()[0].ToAngles(); + ideal_yaw = faceAngles.yaw; + current_yaw = faceAngles.yaw; + SetAxis( armchop_Target->GetAxis() ); + } +} + +void hhCenturion::Event_ScriptedRoar() { + if ( AI_CENTURION_SCRIPTED_ROAR > 0 ) { + gameLocal.Warning( "centurionRoar() called more than once!\n"); + } else { + AI_CENTURION_SCRIPTED_ROAR = 1; + } +} + +void hhCenturion::Event_ScriptedArmChop() { + const function_t* newstate = NULL; + newstate = GetScriptFunction( "state_ScriptedArmChop" ); + if( newstate ) { + SetState( newstate ); + } + else { + gameLocal.Warning( "Unable to find 'state_ScriptedArmChop' on centurion" ); + } +} + +void hhCenturion::Event_CenturionLooseArm() { + idDict args; + idEntity* ent = NULL; + idVec3 jointLoc; + idMat3 jointAxis; + + animator.GetJointTransform( spawnArgs.GetString("arm_severjoint", ""), gameLocal.time, jointLoc, jointAxis ); + jointLoc = renderEntity.origin + jointLoc * renderEntity.axis; + args.SetVector( "origin", jointLoc ); + args.SetMatrix( "rotation", renderEntity.axis ); + args.SetBool( "spin", 0 ); + args.SetFloat( "triggersize", 48.f ); + args.SetBool( "enablePickup", true ); + args.SetFloat( "respawn", 0.f ); + ent = gameLocal.SpawnObject( spawnArgs.GetString("def_arm_weaponclass", ""), &args ); + + SetSkinByName( spawnArgs.GetString( "skin_arm_gone" ) ); + + hhFxInfo fx; + fx.SetEntity( this ); + fx.RemoveWhenDone( true ); + SpawnFxLocal( spawnArgs.GetString( "fx_armchop" ), jointLoc, mat3_identity, &fx ); + + //drop any stuck arrows + idEntity *next; + for( ent = teamChain; ent != NULL; ent = next ) { + next = ent->GetTeamChain(); + if ( ent && ent->IsType( hhProjectile::Type ) ) { + ent->Unbind(); + ent->Hide(); + ent->PostEventSec( &EV_Remove, 5 ); + next = teamChain; + } + } + + AI_CENTURION_ARM_MISSING = true; +} + +void hhCenturion::Event_CenturionLaunch( const idList* parmList ) { + const idDict* projDef = NULL; + const idSoundShader* soundShader = NULL; + + // parms: joint, projectileDef, sound, fx + if( !parmList || parmList->Num() != 5 ) { + gameLocal.Warning( "Incorrect paramater number" ); + return; + } +//Rbarrel_A projectile_centurion_autocannon snd_fire fx_muzzleFlash + const char* jointName = (*parmList)[ 0 ].c_str(); + const char* projectileDefName = (*parmList)[ 1 ].c_str(); + const char* soundName = (*parmList)[ 2 ].c_str(); + const char* fxName = (*parmList)[ 3 ].c_str(); + int autoAim = atoi( (*parmList)[ 4 ].c_str() ); + + if( AI_CENTURION_ARM_MISSING ) { //skip firing if joint is on the severed arm + if( !idStr::Icmp(jointName, spawnArgs.GetString("severed_jointA", "")) || !idStr::Icmp(jointName, spawnArgs.GetString("severed_jointB", "")) ) { + return; + } + } + + projDef = gameLocal.FindEntityDefDict( projectileDefName, false ); + HH_ASSERT( !shootTarget.IsValid() ); + // If autoAim is true and we're not blending, and we're facing the enemy, auto-aim + if ( ( autoAim == 1 || ( autoAim == 2 && gameLocal.random.RandomInt(100) < 50 ) ) && torsoAnim.animBlendFrames == 0 && FacingEnemy( 5.0f ) ) { + AimedAttackMissile( jointName, projDef ); + } else { // Otherwise do a normal missile attack + Event_AttackMissile( jointName, projDef, 1 ); + } + + if(idStr::Cmpn( soundName, "snd_", 4)) { + soundShader = declManager->FindSound( soundName ); + if( soundShader->GetState() == DS_DEFAULTED ) { + gameLocal.Warning( "Sound '%s' not found", soundName ); + } + StartSoundShader( soundShader, SND_CHANNEL_WEAPON, 0, false, NULL ); + } + else { + if( !StartSound(soundName, SND_CHANNEL_WEAPON, 0, false, NULL) ) { + gameLocal.Warning( "Framecommand 'centurionFire' on entity '%s' could not find sound '%s'", GetName(), soundName ); + } + } + BroadcastFxInfoAlongBone( spawnArgs.GetString(fxName), jointName ); +} + + +void hhCenturion::Event_DestroyObstruction() { + if( pillarEntity.IsValid() ) { + pillarEntity->PostEventMS( &EV_Activate, 0.f, this ); + } +} + +void hhCenturion::Event_CheckForObstruction( int checkPathToPillar ) { + bool obstacle = false; + predictedPath_t path; + if( enemy.IsValid() ) { + idVec3 end = enemy->GetPhysics()->GetOrigin(); + if ( !checkPathToPillar ) { + trace_t tr; + idVec3 toPos, eye = GetEyePosition(); + + if ( enemy->IsType( idActor::Type ) ) { + toPos = ( ( idActor * )enemy.GetEntity() )->GetEyePosition(); + } else { + toPos = enemy->GetPhysics()->GetOrigin(); + } + + gameLocal.clip.TracePoint( tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, this ); + + idEntity *traceEnt = gameLocal.GetTraceEntity( tr ); + if ( traceEnt && ( tr.fraction < 1.0f || traceEnt != enemy.GetEntity() ) ) { + //check to see if the other object is an pillar... + if( traceEnt->spawnArgs.GetInt("centurion_pillar", "0") == 1 ) { + pillarEntity = traceEnt; + obstacle = true; + } + } + + } else if ( pillarEntity.IsValid() ) { + idAI::PredictPath( this, this->aas, physicsObj.GetOrigin(), enemy->GetPhysics()->GetOrigin() - physicsObj.GetOrigin(), 1000, 1000, ( move.moveType == MOVETYPE_FLY ) ? SE_BLOCKED : (SE_ENTER_OBSTACLE | SE_BLOCKED | SE_ENTER_LEDGE_AREA ), path ); + if( path.endEvent != 0 && path.blockingEntity ) { + //check to see if the other object is an pillar... + if( path.blockingEntity->spawnArgs.GetInt("centurion_pillar", "0") == 1 ) { + //check to see if we can path to the obstacle clearly + pillarEntity = path.blockingEntity; + //idAI::PredictPath( this, this->aas, physicsObj.GetOrigin(), pillarEntity->GetPhysics()->GetOrigin() - physicsObj.GetOrigin(), 1000, 1000, (SE_ENTER_OBSTACLE | SE_BLOCKED | SE_ENTER_LEDGE_AREA), path ); + //if( path.endEvent != 0 && path.blockingEntity == pillarEntity.GetEntity() ) { + obstacle = true; + //} + } + } + } + } + idThread::ReturnInt( (int)obstacle ); +} + +void hhCenturion::Event_CloseToObstruction() { + if( physicsObj.GetAbsBounds().IntersectsBounds(pillarEntity->GetPhysics()->GetAbsBounds()) ) { + idThread::ReturnInt( 1 ); + return; + } + idThread::ReturnInt( 0 ); +} + +void hhCenturion::Event_ReachedObstruction() { + FaceEntity( pillarEntity.GetEntity() ); +} + +void hhCenturion::Event_MoveToObstruction() { + idVec3 temp; + if ( aas ) { + int toAreaNum = PointReachableAreaNum( pillarEntity.GetEntity()->GetOrigin() ); + temp = pillarEntity.GetEntity()->GetOrigin(); + aas->PushPointIntoAreaNum( toAreaNum, temp ); + MoveToPosition( temp ); + } else { + gameLocal.Warning( "Centurion has no aas for MoveToObstruction\n" ); + } +} + +void hhCenturion::Think() { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { + return; + } + + hhMonsterAI::Think(); + + if(ai_debugBrain.GetInteger() > 0 && state) { + if ( enemy.IsValid() && enemy->GetHealth() > 0 ) { + float dist = ( GetPhysics()->GetOrigin() - enemy->GetPhysics()->GetOrigin() ).LengthFast(); + gameRenderWorld->DrawText( va("%f", dist), this->GetEyePosition() + idVec3(0.0f, 0.0f, 40.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + gameRenderWorld->DrawText(state->Name(), this->GetEyePosition() + idVec3(0.0f, 0.0f, 40.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + gameRenderWorld->DrawText(torsoAnim.state, this->GetEyePosition() + idVec3(0.0f, 0.0f, 20.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + gameRenderWorld->DrawText(legsAnim.state, this->GetEyePosition() + idVec3(0.0f, 0.0f, 0.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } +} + +bool hhCenturion::AttackMelee( const char *meleeDefName ) { + const idDict *meleeDef; + idActor *enemyEnt = enemy.GetEntity(); + const char *p; + const idSoundShader *shader; + + meleeDef = gameLocal.FindEntityDefDict( meleeDefName, false ); + if ( !meleeDef ) { + gameLocal.Error( "Unknown melee '%s'", meleeDefName ); + } + + if ( !enemyEnt ) { + p = meleeDef->GetString( "snd_miss" ); + if ( p && *p ) { + shader = declManager->FindSound( p ); + StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL ); + } + return false; + } + + // make sure the trace can actually hit the enemy + if ( !TestMelee() ) { + // missed + p = meleeDef->GetString( "snd_miss" ); + if ( p && *p ) { + shader = declManager->FindSound( p ); + StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL ); + } + return false; + } + + // + // do the damage + // + p = meleeDef->GetString( "snd_hit" ); + if ( p && *p ) { + shader = declManager->FindSound( p ); + StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL ); + } + + idVec3 kickDir; + meleeDef->GetVector( "kickDir", "0 0 0", kickDir ); + + idVec3 globalKickDir; + globalKickDir = ( viewAxis * physicsObj.GetGravityAxis() ) * kickDir; + + enemyEnt->Damage( this, this, globalKickDir, meleeDefName, 1.0f, INVALID_JOINT ); + if ( enemyEnt->IsActiveAF() && !enemyEnt->IsType( idPlayer::Type ) ) { + PostEventMS( &AI_BackhandImpulse, 0, enemyEnt ); + } + + lastAttackTime = gameLocal.time; + + return true; +} + +void hhCenturion::Event_BackhandImpulse( idEntity* ent ) { + const idDict *meleeDef = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_impulse_damage"), false ); + if ( !meleeDef || !ent ) { + return; + } + idVec3 kickDir; + meleeDef->GetVector( "kickDir", "0 0 0", kickDir ); + idVec3 globalKickDir = ( viewAxis * physicsObj.GetGravityAxis() ) * kickDir; + globalKickDir *= ent->spawnArgs.GetFloat( "kick_scale", "1.0" ); + ent->ApplyImpulse( this, 0, ent->GetOrigin(), meleeDef->GetFloat( "push_ragdoll" ) * globalKickDir ); +} + +/* +===================== +hhCenturion::Save +===================== +*/ +void hhCenturion::Save( idSaveGame *savefile ) const { + armchop_Target.Save( savefile ); + pillarEntity.Save( savefile ); +} + +/* +===================== +hhCenturion::Restore +===================== +*/ +void hhCenturion::Restore( idRestoreGame *savefile ) { + armchop_Target.Restore( savefile ); + pillarEntity.Restore( savefile ); + + // Restore the obstacle list + const idKeyValue *kv = spawnArgs.MatchPrefix( "aasObstacle" ); + idEntityPtr ent; + while ( kv ) { + ent = gameLocal.FindEntity( kv->GetValue() ); + if ( ent.IsValid() ) { + obstacles.AddUnique( ent ); + } + kv = spawnArgs.MatchPrefix( "aasObstacle", kv ); + } +} + +void hhCenturion::Event_EnemyCloseToObstruction(void) { + bool close = false; + if ( pillarEntity.IsValid() && enemy.IsValid() ) { + float obs_dist = ( pillarEntity->GetOrigin() - GetOrigin() ).Length(); + float dist = ( enemy->GetOrigin() - GetOrigin() ).Length(); + + // If the enemy is further from me than the pillar... + if ( dist > obs_dist) { + dist = ( pillarEntity->GetOrigin() - enemy->GetOrigin() ).Length(); + // If the enemy is less than 400 units from the pillar, then he's 'close' + if ( dist < 400.0f ) { + close = true; + } + } + } + idThread::ReturnInt( (int) close ); +} + +void hhCenturion::AimedAttackMissile( const char *jointname, const idDict *projDef) { + idProjectile *proj; + idVec3 target, origin = GetOrigin(); + bool inShuttle = false; + + if ( shootTarget.IsValid() ) { + target = shootTarget->GetOrigin(); + } else if ( enemy.IsValid() ) { + target = enemy->GetOrigin(); + if ( enemy->IsType( idActor::Type ) ) { + target.z += enemy->EyeHeight() / 4.0f; + } + } else { + // No target? Do the default attack + Event_AttackMissile( jointname, projDef, 1 ); + return; + } + + // If target is too close do a non-aimed attack + if ( fabsf( origin.x - target.x ) < 256 && + fabsf( origin.y - target.y ) < 256 ) { + Event_AttackMissile( jointname, projDef, 1 ); + return; + } + + idVec3 dist = origin - target; + + if ( shootTarget.IsValid() ) { + proj = LaunchProjectile( jointname, shootTarget.GetEntity(), true, projDef ); + } else { + proj = LaunchProjectile( jointname, enemy.GetEntity(), true, projDef ); + } +} + +void hhCenturion::Event_TakeDamage(int takeDamage) { + // Set the takedamage flag + fl.takedamage = (takeDamage != 0); +} + +void hhCenturion::Event_FindNearbyEnemy( float distance ) { + // Search for the monster nearest to us + idAI *nearest = NULL; + float dist, nearDist = idMath::INFINITY; + idAI *ai = reinterpret_cast (gameLocal.FindEntityOfType( idAI::Type, NULL )); + while (ai) { + if (ai != this) { // Don't target yourself + dist = (ai->GetOrigin() - GetOrigin()).Length(); + if (dist < nearDist) { + nearDist = dist; + nearest = ai; + } + } + ai = reinterpret_cast (gameLocal.FindEntityOfType( idAI::Type, ai )); + } + + // If we found one near us and it's near enough, return it + if (nearest && nearDist < distance) { + idThread::ReturnEntity(nearest); + } else { + idThread::ReturnEntity(NULL); + } +} + +void hhCenturion::Event_Touch( idEntity *other, trace_t *trace ) { + if ( (!enemy.GetEntity() || other->IsType( hhPlayer::Type )) && !other->fl.notarget && ( ReactionTo( other ) & ATTACK_ON_ACTIVATE ) ) { + Activate( other ); + SetEnemy( static_cast ( other ) ); + } + AI_PUSHED = true; +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_centurion.h b/src/Prey/ai_centurion.h new file mode 100644 index 0000000..b640f64 --- /dev/null +++ b/src/Prey/ai_centurion.h @@ -0,0 +1,84 @@ +#ifndef __PREY_AI_CENTURION_H__ +#define __PREY_AI_CENTURION_H__ + +/*********************************************************************** + hhCenturion. + Centurion AI. +***********************************************************************/ +class hhCenturion : public hhMonsterAI { + +public: + CLASS_PROTOTYPE(hhCenturion); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_CenturionLaunch( const idList* parmList ) {}; + void Event_ScriptedRoar() {}; + void Event_ScriptedArmChop() {}; + void Event_CenturionLooseArm() {}; + void Event_ForceFieldNotify() {}; + void Event_ForceFieldToggle( int toggle ) {}; + void Event_PlayerInTunnel( int toggle, idEntity* ent ) {}; + void Event_ReachedTunnel() {}; + void Event_MoveToTunnel() {}; + + bool AttackMelee( const char *meleeDefName ) { return true; }; + void Event_CheckForObstruction( int checkPathToPillar ) {}; + void Event_MoveToObstruction() {}; + void Event_DestroyObstruction() {}; + void Event_ReachedObstruction() {}; + void Event_CloseToObstruction() {}; + void Event_EnemyCloseToObstruction() {}; + void Event_BackhandImpulse( idEntity* ent ) {}; + virtual void Event_PostSpawn( void ) {}; + void Event_TakeDamage( int takeDamage ) {}; + void Event_FindNearbyEnemy( float distance ) {}; + virtual void Event_Touch( idEntity *other, trace_t *trace ) {}; +#else + +public: + void Spawn(); + void Think(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); +protected: + void Event_CenturionLaunch( const idList* parmList ); + void Event_ScriptedRoar(); + void Event_ScriptedArmChop(); + void Event_CenturionLooseArm(); + void Event_ForceFieldNotify(); + void Event_ForceFieldToggle( int toggle ); + void Event_PlayerInTunnel( int toggle, idEntity* ent ); + void Event_ReachedTunnel(); + void Event_MoveToTunnel(); + + void LinkScriptVariables( void ); + bool AttackMelee( const char *meleeDefName ); + void Event_CheckForObstruction( int checkPathToPillar ); + void Event_MoveToObstruction(); + void Event_DestroyObstruction(); + void Event_ReachedObstruction(); + void Event_CloseToObstruction(); + void Event_EnemyCloseToObstruction(); + void Event_BackhandImpulse( idEntity* ent ); + virtual void Event_PostSpawn( void ); + void Event_TakeDamage( int takeDamage ); + void Event_FindNearbyEnemy( float distance ); + virtual void Event_Touch( idEntity *other, trace_t *trace ); + + void AimedAttackMissile( const char *jointname, const idDict *projDef ); + + idEntityPtr armchop_Target; + idEntityPtr pillarEntity; + + idScriptBool AI_CENTURION_ARM_MISSING; + idScriptBool AI_CENTURION_REQUIRE_ROAR; + idScriptBool AI_CENTURION_ARM_TUNNEL; + idScriptBool AI_CENTURION_FORCEFIELD_WAIT; + idScriptFloat AI_CENTURION_SCRIPTED_ROAR; + idScriptFloat AI_CENTURION_SCRIPTED_TUNNEL; + + idList< idEntityPtr > obstacles; +#endif +}; + +#endif //__PREY_AI_CENTURION_H__ \ No newline at end of file diff --git a/src/Prey/ai_crawler.cpp b/src/Prey/ai_crawler.cpp new file mode 100644 index 0000000..e367a87 --- /dev/null +++ b/src/Prey/ai_crawler.cpp @@ -0,0 +1,115 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +CLASS_DECLARATION( hhMonsterAI, hhCrawler ) + EVENT( EV_Touch, hhCrawler::Event_Touch ) +END_CLASS + + +void hhCrawler::Spawn() { + if( spawnArgs.FindKey("def_pickup") ) { + if ( gameLocal.isMultiplayer ) { + GetPhysics()->SetContents( CONTENTS_TRIGGER | CONTENTS_MONSTERCLIP ); + } else { + GetPhysics()->SetContents( CONTENTS_TRIGGER ); + } + } +} + +void hhCrawler::Event_Touch( idEntity *other, trace_t *trace ) { + hhPlayer *player; + + if( !spawnArgs.FindKey("def_pickup") || !other->IsType(idPlayer::Type) ) { + return; + } + + player = static_cast(other); + + Pickup( player ); +} + +void hhCrawler::Pickup( hhPlayer *player ) { + + if( !GiveToPlayer( player ) ) { + return; + } + + // play pickup sound + StartSoundShader( refSound.shader, SND_CHANNEL_ITEM, false ); // play what's defined in the entity + + // trigger our targets + ActivateTargets( player ); + + // clear our contents so the object isn't picked up twice + GetPhysics()->SetContents( 0 ); + + // hide the model + Hide(); + + if (player->hud) { + player->hud->SetStateInt("item", 1); + player->hud->SetStateString("itemtext", spawnArgs.GetString("inv_name")); + player->hud->SetStateString("itemicon", spawnArgs.GetString("inv_icon")); + } + + idStr str; + spawnArgs.GetString("inv_name", "Item", str); + + if (player == gameLocal.GetClientByNum(gameLocal.localClientNum)) { + gameLocal.Printf("Picked up a %s\n", str.c_str()); + } + + + PostEventMS( &EV_Remove, 2000 ); +} + +bool hhCrawler::GiveToPlayer( hhPlayer* player ) { + const char *pickupName = spawnArgs.GetString("def_pickup", NULL); + bool pickedUp = false; + + if ( player && pickupName ) { + idEntity *ent = gameLocal.SpawnObject(pickupName, NULL); + if (ent->IsType(hhItem::Type)) { + pickedUp = player->GiveItem( static_cast(ent) ); + } + ent->Hide(); + ent->PostEventMS(&EV_Remove, 2000); + } + + return pickedUp; +} + +void hhCrawler::Think( void ) { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { //HUMANHEAD rww + return; + } + + if ( thinkFlags & TH_THINK ) { + current_yaw += deltaViewAngles.yaw; + ideal_yaw = idMath::AngleNormalize180( ideal_yaw + deltaViewAngles.yaw ); + deltaViewAngles.Zero(); + viewAxis = idAngles( 0, current_yaw, 0 ).ToMat3(); + + // HUMANHEAD NLA + physicsObj.ResetNumTouchEnt(0); + // HUMANHEAD END + // animation based movement + UpdateAIScript(); + AnimMove(); + } else if ( thinkFlags & TH_PHYSICS ) { + RunPhysics(); + } + + UpdateAnimation(); + Present(); + LinkCombat(); +} + +bool hhCrawler::UpdateAnimationControllers() { + //do nothing + return false; +} \ No newline at end of file diff --git a/src/Prey/ai_crawler.h b/src/Prey/ai_crawler.h new file mode 100644 index 0000000..1fc7178 --- /dev/null +++ b/src/Prey/ai_crawler.h @@ -0,0 +1,24 @@ +#ifndef __PREY_AI_CRAWLER_H__ +#define __PREY_AI_CRAWLER_H__ + +/*********************************************************************** + hhCrawler. + Crawler AI. +***********************************************************************/ +class hhCrawler : public hhMonsterAI { + +public: + CLASS_PROTOTYPE(hhCrawler); + +public: + void Spawn(); + +protected: + void Event_Touch( idEntity *other, trace_t *trace ); + void Pickup( hhPlayer *player ); + bool GiveToPlayer( hhPlayer* player ); + void Think(); + bool UpdateAnimationControllers(); +}; + +#endif //__PREY_AI_CRAWLER_H__ \ No newline at end of file diff --git a/src/Prey/ai_creaturex.cpp b/src/Prey/ai_creaturex.cpp new file mode 100644 index 0000000..b6787ff --- /dev/null +++ b/src/Prey/ai_creaturex.cpp @@ -0,0 +1,1669 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef CX_LaserOn("sunBeamOn", NULL, 'd'); +const idEventDef CX_LaserOff("sunBeamOff"); +const idEventDef MA_UpdateLasers(""); +const idEventDef MA_AssignLeftMuzzleFx( "", "e" ); +const idEventDef MA_AssignRightMuzzleFx( "", "e" ); +const idEventDef MA_AssignLeftImpactFx( "", "e" ); +const idEventDef MA_AssignRightImpactFx( "", "e" ); +const idEventDef MA_AssignLeftRechargeFx( "", "e" ); +const idEventDef MA_AssignRightRechargeFx( "", "e" ); +const idEventDef MA_SetGunOffset( "setGunOffset", "v" ); +const idEventDef MA_EndLeftBeams( "" ); +const idEventDef MA_EndRightBeams( "" ); +const idEventDef MA_HudEvent( "hudEvent", "s" ); +const idEventDef MA_GunRecharge( "gunRecharge", "d" ); +const idEventDef MA_EndRecharge( "endRecharge" ); +const idEventDef MA_ResetRechargeBeam( "" ); +const idEventDef MA_SparkLeft( "" ); +const idEventDef MA_SparkRight( "" ); +const idEventDef MA_LeftGunDeath( "" ); +const idEventDef MA_RightGunDeath( "" ); +const idEventDef MA_StartRechargeBeams( "" ); + +CLASS_DECLARATION(hhMonsterAI, hhCreatureX) + EVENT(CX_LaserOn, hhCreatureX::Event_LaserOn ) + EVENT(CX_LaserOff, hhCreatureX::Event_LaserOff ) + EVENT(MA_UpdateLasers, hhCreatureX::Event_UpdateLasers ) + EVENT(MA_AssignRightMuzzleFx, hhCreatureX::Event_AssignRightMuzzleFx ) + EVENT(MA_AssignLeftMuzzleFx, hhCreatureX::Event_AssignLeftMuzzleFx ) + EVENT(MA_AssignRightImpactFx, hhCreatureX::Event_AssignRightImpactFx ) + EVENT(MA_AssignLeftImpactFx, hhCreatureX::Event_AssignLeftImpactFx ) + EVENT(MA_SetGunOffset, hhCreatureX::Event_SetGunOffset ) + EVENT(MA_EndLeftBeams, hhCreatureX::Event_EndLeftBeams ) + EVENT(MA_EndRightBeams, hhCreatureX::Event_EndRightBeams ) + EVENT(MA_HudEvent, hhCreatureX::Event_HudEvent ) + EVENT(MA_GunRecharge, hhCreatureX::Event_GunRecharge ) + EVENT(MA_EndRecharge, hhCreatureX::Event_EndRecharge ) + EVENT(MA_ResetRechargeBeam, hhCreatureX::Event_ResetRechargeBeam ) + EVENT(MA_SparkLeft, hhCreatureX::Event_SparkLeft ) + EVENT(MA_SparkRight, hhCreatureX::Event_SparkRight ) + EVENT(MA_LeftGunDeath, hhCreatureX::Event_LeftGunDeath ) + EVENT(MA_RightGunDeath, hhCreatureX::Event_RightGunDeath ) + EVENT(MA_StartRechargeBeams, hhCreatureX::Event_StartRechargeBeams ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +void hhCreatureX::Spawn() { + idVec3 boneOrigin; + idMat3 boneAxis; + targetStart_L = vec3_zero; + targetStart_R = vec3_zero; + targetEnd_L = vec3_zero; + targetEnd_R = vec3_zero; + targetCurrent_L = vec3_zero; + targetCurrent_R = vec3_zero; + bLaserLeftActive = false; + bLaserRightActive = false; + nextBeamTime = 0; + nextLeftZapTime = 0; + nextRightZapTime = 0; + nextLaserLeft = 0; + nextLaserRight = 0; + nextHealthTick = 0; + rightGunLives = spawnArgs.GetInt( "gun_lives", "3" ); + leftGunLives = spawnArgs.GetInt( "gun_lives", "3" ); + rightGunHealth = spawnArgs.GetInt( "gun_health", "20" ); + leftGunHealth = spawnArgs.GetInt( "gun_health", "20" ); + bScripted = spawnArgs.GetBool( "scripted", "0" ); + numBurstBeams = 0; + + if ( bScripted ) { + //don't spawn any combat related entities if scripted + return; + } + + laserRight = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamLaser" ) ); + if( laserRight.IsValid() ) { + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + + idVec3 junkOrigin = boneOrigin + viewAxis[0] * 60; //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.z)); //Test for bad origin + + laserRight->SetOrigin( boneOrigin + viewAxis[0] * 60 ); + laserRight->Activate( false ); + } + laserLeft = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamLaser" ) ); + if( laserLeft.IsValid() ) { + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + + idVec3 junkOrigin = boneOrigin + viewAxis[0] * 60; //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.z)); //Test for bad origin + + laserLeft->SetOrigin( boneOrigin + viewAxis[0] * 60 ); + laserLeft->Activate( false ); + } + preLaserRight = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamLaser" ) ); + if( preLaserRight.IsValid() ) { + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + + idVec3 junkOrigin = boneOrigin; //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.z)); //Test for bad origin + + preLaserRight->SetOrigin( boneOrigin ); + preLaserRight->BindToJoint( this, spawnArgs.GetString("laser_bone_right"), false ); + preLaserRight->Activate( false ); + } + preLaserLeft = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamLaser" ) ); + if( preLaserLeft.IsValid() ) { + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + + idVec3 junkOrigin = boneOrigin; //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(junkOrigin.z)); //Test for bad origin + + preLaserLeft->SetOrigin( boneOrigin ); + preLaserLeft->BindToJoint( this, spawnArgs.GetString("laser_bone_left"), false ); + preLaserLeft->Activate( false ); + } + PostEventSec( &MA_UpdateLasers, spawnArgs.GetFloat( "laser_freq", "0.1" ) ); + + leftBeamList.Clear(); + rightBeamList.Clear(); + leftRechargeBeam.Clear(); + rightRechargeBeam.Clear(); + leftRecharger.Clear(); + rightRecharger.Clear(); + if ( spawnArgs.GetBool( "use_recharge" ) ) { + numBurstBeams = spawnArgs.GetInt( "num_burst_beams", "4" ); + for ( int i = 0; i < numBurstBeams; i ++ ) { + leftBeamList.Append( hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamBurst" ) ) ); + if( leftBeamList[i].IsValid() ) { + leftBeamList[i]->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("damage_bone_left"), boneOrigin, boneAxis ); + leftBeamList[i]->SetOrigin( boneOrigin ); + leftBeamList[i]->BindToJoint( this, spawnArgs.GetString("damage_bone_left"), false ); + } + rightBeamList.Append( hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamBurst" ) ) ); + if( rightBeamList[i].IsValid() ) { + rightBeamList[i]->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("damage_bone_right"), boneOrigin, boneAxis ); + rightBeamList[i]->SetOrigin( boneOrigin ); + rightBeamList[i]->BindToJoint( this, spawnArgs.GetString("damage_bone_right"), false ); + } + } + + leftRechargeBeam = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamRecharge" ) ); + if( leftRechargeBeam.IsValid() ) { + leftRechargeBeam->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + leftRechargeBeam->SetOrigin( boneOrigin ); + leftRechargeBeam->BindToJoint( this, spawnArgs.GetString("laser_bone_left"), false ); + leftRechargeBeam->Hide(); + } + rightRechargeBeam = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamRecharge" ) ); + if( rightRechargeBeam.IsValid() ) { + rightRechargeBeam->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + rightRechargeBeam->SetOrigin( boneOrigin ); + rightRechargeBeam->BindToJoint( this, spawnArgs.GetString("laser_bone_right"), false ); + rightRechargeBeam->Hide(); + } + leftDamageBeam = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamDamage" ) ); + if( leftDamageBeam.IsValid() ) { + leftDamageBeam->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + leftDamageBeam->SetOrigin( boneOrigin ); + leftDamageBeam->BindToJoint( this, spawnArgs.GetString("laser_bone_left"), false ); + leftDamageBeam->Hide(); + } + rightDamageBeam = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamDamage" ) ); + if( rightDamageBeam.IsValid() ) { + rightDamageBeam->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + rightDamageBeam->SetOrigin( boneOrigin ); + rightDamageBeam->BindToJoint( this, spawnArgs.GetString("laser_bone_right"), false ); + rightDamageBeam->Hide(); + } + leftRetractBeam = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamRetract" ) ); + if( leftRetractBeam.IsValid() ) { + leftRetractBeam->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + leftRetractBeam->SetOrigin( boneOrigin ); + leftRetractBeam->BindToJoint( this, spawnArgs.GetString("laser_bone_left"), false ); + leftRetractBeam->Hide(); + } + rightRetractBeam = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamRetract" ) ); + if( rightRetractBeam.IsValid() ) { + rightRetractBeam->Activate( false ); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + rightRetractBeam->SetOrigin( boneOrigin ); + rightRetractBeam->BindToJoint( this, spawnArgs.GetString("laser_bone_right"), false ); + rightRetractBeam->Hide(); + } + + idEntity *ent; + idDict dict; + const idDict *rechargeDict = gameLocal.FindEntityDefDict(spawnArgs.GetString("def_recharger")); + if ( !rechargeDict ) { + gameLocal.Error( "Unknown def_recharger: %s\n", spawnArgs.GetString("def_recharger") ); + } + dict.Copy(*rechargeDict); + gameLocal.SpawnEntityDef( dict, &ent ); + if ( ent ) { + leftRecharger.Assign( ent ); + leftRecharger->GetPhysics()->DisableClip(); + leftRecharger->GetPhysics()->SetClipMask( 0 ); + leftRecharger->HideNoDormant(); + leftRecharger->SetOrigin( GetOrigin() ); + leftRecharger->SetEnemy( this ); + leftRecharger->spawnArgs.Set( "left_recharger", "1" ); //set key for left or right + } + ent = NULL; + gameLocal.SpawnEntityDef( dict, &ent ); + if ( ent ) { + rightRecharger.Assign( ent ); + rightRecharger->GetPhysics()->DisableClip(); + rightRecharger->GetPhysics()->SetClipMask( 0 ); + rightRecharger->HideNoDormant(); + rightRecharger->SetOrigin( GetOrigin() ); + rightRecharger->SetEnemy( this ); + rightRecharger->spawnArgs.Set( "right_recharger", "1" ); //set key for left or right + } + } +} + +hhCreatureX::~hhCreatureX() { + MuzzleLeftOff(); + MuzzleRightOff(); + + SAFE_REMOVE( laserRight ); + SAFE_REMOVE( laserLeft ); + SAFE_REMOVE( preLaserLeft ); + SAFE_REMOVE( preLaserRight ); +} + +void hhCreatureX::Event_LaserOn() { + if ( !enemy.IsValid() ) { + idThread::ReturnInt( false ); + return; + } + if ( !AI_RIGHT_DAMAGED ) { + bLaserRightActive = true; + } + if ( !AI_LEFT_DAMAGED ) { + bLaserLeftActive = true; + } + idVec3 toEnemy = GetEnemy()->GetOrigin() - GetOrigin(); + targetStart_L = GetOrigin() + spawnArgs.GetFloat( "test_1", "0.5" ) * toEnemy; + targetStart_L.z = GetOrigin().z; + float distToStart = (targetStart_L - GetOrigin()).LengthFast(); + if ( distToStart < 200 ) { + targetStart_L = GetOrigin() + toEnemy; + targetStart_L.z = GetOrigin().z; + } + targetStart_R = GetOrigin() + spawnArgs.GetFloat( "test_1", "0.5" ) * toEnemy; + targetStart_R.z = GetOrigin().z; + distToStart = (targetStart_R - GetOrigin()).LengthFast(); + if ( distToStart < 200 ) { + targetStart_R = GetOrigin() + toEnemy; + targetStart_R.z = GetOrigin().z; + } + targetEnd_L = GetOrigin() + spawnArgs.GetFloat( "test_2", "1.4" ) * toEnemy; + targetEnd_L.z = GetOrigin().z + spawnArgs.GetFloat( "test_4", "50" ); + targetEnd_R = GetOrigin() + spawnArgs.GetFloat( "test_2", "1.4" ) * toEnemy; + targetEnd_R.z = GetOrigin().z + spawnArgs.GetFloat( "test_4", "50" ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, targetStart_L, targetEnd_L, 10, 10000 ); + } + targetAlpha_L = 0.0f; + targetAlpha_R = 0.0f; + if ( !bLaserLeftActive && !bLaserRightActive ) { + idThread::ReturnInt( false ); + } + idThread::ReturnInt( true ); +} + +void hhCreatureX::Event_LaserOff() { + bLaserLeftActive = false; + bLaserRightActive = false; + targetStart_L = vec3_zero; + targetStart_R = vec3_zero; + targetEnd_L = vec3_zero; + targetEnd_R = vec3_zero; + MuzzleLeftOff(); + MuzzleRightOff(); +} + +void hhCreatureX::Event_UpdateLasers() { + if ( !enemy.IsValid() ) { + PostEventSec( &MA_UpdateLasers, spawnArgs.GetFloat( "laser_freq", "0.1" ) ); + return; + } + trace_t trace; + float dist = 0; + idVec3 boneOrigin, traceEnd; + idMat3 boneAxis; + idEntity *hitEntity = NULL; + int hitCount = 0; + if ( laserRight.IsValid() && bLaserRightActive ) { + MuzzleRightOn(); + traceEnd = targetCurrent_R + 2000 * (targetCurrent_R - laserRight->GetOrigin()).ToNormal(); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + gameLocal.clip.TracePoint( trace, laserRight->GetOrigin(), traceEnd, MASK_SHOT_RENDERMODEL, this ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, laserRight->GetOrigin(), traceEnd, 10, 200 ); + } + if ( trace.fraction < 1.0f ) { + dist = (trace.endpos - laserRight->GetOrigin()).Length(); + hitEntity = gameLocal.GetTraceEntity( trace ); + if ( hitEntity ) { + hitEntity->Damage( this, this, hitEntity->GetOrigin() - GetOrigin().ToNormal(), spawnArgs.GetString( "def_laserDamage" ), 1.0, 0 ); + } + } + } else { + MuzzleRightOff(); + } + if ( laserLeft.IsValid() && bLaserLeftActive ) { + MuzzleLeftOn(); + traceEnd = targetCurrent_L + 2000 * (targetCurrent_L - laserLeft->GetOrigin()).ToNormal(); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + gameLocal.clip.TracePoint( trace, laserLeft->GetOrigin(), traceEnd, MASK_SHOT_RENDERMODEL, this ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, laserLeft->GetOrigin(), traceEnd, 10, 200 ); + } + if ( trace.fraction < 1.0f ) { + dist = (trace.endpos - laserLeft->GetOrigin()).Length(); + hitEntity = gameLocal.GetTraceEntity( trace ); + if ( hitEntity ) { + hitEntity->Damage( this, this, hitEntity->GetOrigin() - GetOrigin().ToNormal(), spawnArgs.GetString( "def_laserDamage" ), 1.0, 0 ); + } + } + } else { + MuzzleLeftOff(); + } + PostEventSec( &MA_UpdateLasers, spawnArgs.GetFloat( "laser_freq", "0.1" ) ); +} + +void hhCreatureX::Event_ResetRechargeBeam() { + if ( !AI_RECHARGING ) { + return; + } + if ( leftRecharger.IsValid() ) { + leftRecharger->SetShaderParm( 6, 0.0f ); + } + if ( rightRecharger.IsValid() ) { + rightRecharger->SetShaderParm( 6, 0.0f ); + } + if ( rechargeLeftFx.IsValid() ) { + rechargeLeftFx->SetParticleShaderParm( 6, 0.0f ); + } + if ( rechargeRightFx.IsValid() ) { + rechargeRightFx->SetParticleShaderParm( 6, 0.0f ); + } + if ( leftRechargeBeam.IsValid() ) { + leftRechargeBeam->Activate( true ); + leftRechargeBeam->Show(); + } + if ( rightRechargeBeam.IsValid() ) { + rightRechargeBeam->Activate( true ); + rightRechargeBeam->Show(); + } + if ( leftDamageBeam.IsValid() ) { + leftDamageBeam->Activate( false ); + } + if ( rightDamageBeam.IsValid() ) { + rightDamageBeam->Activate( false ); + } + if ( leftRetractBeam.IsValid() ) { + leftRetractBeam->Activate( false ); + } + if ( rightRetractBeam.IsValid() ) { + rightRetractBeam->Activate( false ); + } +} + +void hhCreatureX::Think() { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { + return; + } + + idVec3 pastEnemy; + hhMonsterAI::Think(); + if ( AI_RECHARGING ) { + bool damaged = false; + if ( leftRecharger.IsValid() ) { + //recharger damage. switch out beams + if ( leftRechargeBeam.IsValid() && leftRecharger->GetHealth() < leftRechargerHealth ) { + if ( leftRechargerHealth > 0 && !leftRecharger->IsDead() ) { + damaged = true; + } + leftRechargeBeam->Activate( false ); + leftDamageBeam->Show(); + leftDamageBeam->Activate( true ); + leftRecharger->SetShaderParm( 6, 1.0f ); + if ( rechargeLeftFx.IsValid() ) { + rechargeLeftFx->SetParticleShaderParm( 6, 1.0f ); + } + } + leftRechargerHealth = leftRecharger->GetHealth(); + } + if ( rightRecharger.IsValid() ) { + //recharger damage. switch out beams + if ( rightRechargeBeam.IsValid() && rightRecharger->GetHealth() < rightRechargerHealth ) { + if ( rightRechargerHealth > 0 && !rightRecharger->IsDead() ) { + damaged = true; + } + rightRechargeBeam->Activate( false ); + rightDamageBeam->Show(); + rightDamageBeam->Activate( true ); + rightRecharger->SetShaderParm( 6, 1.0f ); + if ( rechargeRightFx.IsValid() ) { + rechargeRightFx->SetParticleShaderParm( 6, 1.0f ); + } + } + rightRechargerHealth = rightRecharger->GetHealth(); + } + if ( damaged ) { + PostEventSec( &MA_ResetRechargeBeam, spawnArgs.GetFloat( "damage_beam_duration", "0.8" ) ); + } + if ( !AI_LEFT_DAMAGED && leftRecharger.IsValid() && leftRecharger->GetHealth() <= 0 ) { + //recharger has died so start retracting the beams and schedule the gun's death + StartSound( "snd_gun_predeath", SND_CHANNEL_ANY ); + SAFE_REMOVE( leftRechargeBeam ); + PostEventSec( &MA_LeftGunDeath, spawnArgs.GetFloat("retract_delay", "0.9") ); + const char *defName = spawnArgs.GetString( "fx_retract" ); + if ( defName && defName[0] && leftDamageBeam.IsValid() ) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + retractLeftFx = SpawnFxLocal( defName, leftDamageBeam->GetTargetLocation(), mat3_identity, &fxInfo, gameLocal.isClient ); + } + if ( leftRetractBeam.IsValid() ) { + leftRetractBeam->Show(); + leftRetractBeam->Activate( true ); + } + if ( leftDamageBeam.IsValid() ) { + leftDamageBeam->Activate( false ); + leftDamageBeam->Hide(); + } + AI_LEFT_DAMAGED = true; + } + if ( !AI_RIGHT_DAMAGED && rightRecharger.IsValid() && rightRecharger->GetHealth() <= 0 ) { + //recharger has died so start retracting the beams and schedule the gun's death + StartSound( "snd_gun_predeath", SND_CHANNEL_ANY ); + SAFE_REMOVE( rightRechargeBeam ); + PostEventSec( &MA_RightGunDeath, spawnArgs.GetFloat("retract_delay", "1") ); + const char *defName = spawnArgs.GetString( "fx_retract" ); + if ( defName && defName[0] && leftDamageBeam.IsValid() ) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + retractRightFx = SpawnFxLocal( defName, rightDamageBeam->GetTargetLocation(), mat3_identity, &fxInfo, gameLocal.isClient ); + } + if ( rightRetractBeam.IsValid() ) { + rightRetractBeam->Show(); + rightRetractBeam->Activate( true ); + } + if ( rightDamageBeam.IsValid() ) { + rightDamageBeam->Activate( false ); + rightDamageBeam->Hide(); + } + AI_RIGHT_DAMAGED = true; + } + if ( AI_HEALTH_TICK && gameLocal.time >= nextHealthTick ) { + health += spawnArgs.GetInt( "recharge_delta", "1" ); + nextHealthTick += spawnArgs.GetInt( "recharge_period" ); + if ( health > spawnArgs.GetInt( "health" ) ) { + AI_HEALTH_TICK = false; + health = spawnArgs.GetInt( "health" ); + } + } + } else { + if ( leftRechargeBeam.IsValid() ) { + leftRechargeBeam->Activate( false ); + } + if ( rightRechargeBeam.IsValid() ) { + rightRechargeBeam->Activate( false ); + } + } + + if ( AI_LEFT_DAMAGED && gameLocal.time > nextLeftZapTime ) { + nextLeftZapTime = gameLocal.time + SEC2MS(spawnArgs.GetFloat( "zap_period", "0.3" )); + for ( int i = 0; i < numBurstBeams; i ++ ) { + if ( leftBeamList[i].IsValid() && !leftBeamList[i]->IsActivated() ) { + leftBeamList[i]->Activate( true ); + leftBeamList[i]->SetTargetLocation( leftBeamList[i]->GetOrigin() + spawnArgs.GetFloat( "zap_length", "50" ) * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), 16.0f ) ); + PostEventSec(&MA_EndLeftBeams, spawnArgs.GetFloat( "beam_duration_1", "0.2" ) ); + break; + } + } + } + if ( AI_RIGHT_DAMAGED && gameLocal.time > nextRightZapTime ) { + nextRightZapTime = gameLocal.time + SEC2MS(spawnArgs.GetFloat( "zap_period", "0.3" )); + for ( int i = 0; i < numBurstBeams; i ++ ) { + if ( rightBeamList[i].IsValid() && !rightBeamList[i]->IsActivated() ) { + rightBeamList[i]->Activate( true ); + rightBeamList[i]->SetTargetLocation( rightBeamList[i]->GetOrigin() + spawnArgs.GetFloat( "zap_length", "50" ) * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), 16.0f ) ); + PostEventSec(&MA_EndRightBeams, spawnArgs.GetFloat( "beam_duration_1", "0.2" ) ); + break; + } + } + } + + if ( enemy.IsValid() ) { + //left laser + //HUMANHEAD jsh PCF 5/12/06 30hz issue with beam movement + targetAlpha_L += spawnArgs.GetFloat( "laser_move_delta", "0.1" ) * (60.0f * USERCMD_ONE_OVER_HZ); + if ( targetAlpha_L > 1.0f ) { + targetAlpha_L = 1.0f; + } + targetCurrent_L = targetStart_L + targetAlpha_L * ( targetEnd_L - targetStart_L ); + pastEnemy = GetEnemy()->GetOrigin() + idVec3( 0,0,30 ); + //HUMANHEAD jsh PCF 5/12/06 30hz issue with beam movement + targetEnd_L += spawnArgs.GetFloat( "laser_track_delta", "0.1" ) * ( pastEnemy - targetEnd_L ) * (60.0f * USERCMD_ONE_OVER_HZ); + + //right laser + //HUMANHEAD jsh PCF 5/12/06 30hz issue with beam movement + targetAlpha_R += spawnArgs.GetFloat( "laser_move_delta", "0.1" ) * (60.0f * USERCMD_ONE_OVER_HZ); + if ( targetAlpha_R > 1.0f ) { + targetAlpha_R = 1.0f; + } + targetCurrent_R = targetStart_R + targetAlpha_R * ( targetEnd_R - targetStart_R ); + pastEnemy = GetEnemy()->GetOrigin() + idVec3( 0,0,30 ); + //HUMANHEAD jsh PCF 5/12/06 30hz issue with beam movement + targetEnd_R += spawnArgs.GetFloat( "laser_track_delta", "0.1" ) * ( pastEnemy - targetEnd_R ) * (60.0f * USERCMD_ONE_OVER_HZ); + } + + if ( gameLocal.time > nextBeamTime ) { + nextBeamTime = gameLocal.time + SEC2MS(spawnArgs.GetFloat( "beam_period", "0.3" )); + for ( int i = 0; i < leftBeamList.Num(); i ++ ) { + if ( leftBeamList[i].IsValid() && leftBeamList[i]->IsActivated() ) { + leftBeamList[i]->SetTargetLocation( leftBeamList[i]->GetOrigin() + spawnArgs.GetFloat( "beam_length", "100" ) * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), 16.0f ) ); + } + } + for ( int i = 0; i < rightBeamList.Num(); i ++ ) { + if ( rightBeamList[i].IsValid() && rightBeamList[i]->IsActivated() ) { + rightBeamList[i]->SetTargetLocation( rightBeamList[i]->GetOrigin() + spawnArgs.GetFloat( "beam_length", "100" ) * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), 16.0f ) ); + } + } + } + + idVec3 boneOrigin, traceEnd; + idMat3 boneAxis; + trace_t trace; + memset(&trace, 0, sizeof(trace)); + if ( !enemy.IsValid() ) { + return; + } + + if ( laserRight.IsValid() && bLaserRightActive ) { + traceEnd = targetCurrent_R + 2000 * (targetCurrent_R - laserRight->GetOrigin()).ToNormal(); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + gameLocal.clip.TracePoint( trace, laserRight->GetOrigin(), traceEnd, MASK_SHOT_RENDERMODEL, this ); + if ( trace.fraction < 1.0f ) { + if ( preLaserRight.IsValid() ) { + if ( preLaserRight->IsHidden() ) { + preLaserRight->Show(); + } + preLaserRight->Activate( true ); + preLaserRight->SetTargetEntity( laserRight.GetEntity() ); + } + if ( laserRight->IsHidden() ) { + laserRight->Show(); + } + laserRight->SetOrigin( boneOrigin + viewAxis[0] * 60 ); + laserRight->Activate( true ); + laserRight->SetTargetLocation( trace.endpos - viewAxis[1] * 10 ); + } + float dist = (laserRight->GetTargetLocation() - laserRight->GetOrigin()).Length(); + if ( dist > 100.0f && impactRightFx.IsValid() ) { + impactRightFx->SetOrigin( laserRight->GetTargetLocation() ); + } + laserEndRight = trace.endpos; + } else { + if ( preLaserRight.IsValid() ) { + preLaserRight->Activate( false ); + } + if ( laserRight.IsValid() ) { + laserRight->Activate( false ); + } + } + + if ( laserLeft.IsValid() && bLaserLeftActive ) { + traceEnd = targetCurrent_L + 2000 * (targetCurrent_L - laserLeft->GetOrigin()).ToNormal(); + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + gameLocal.clip.TracePoint( trace, laserLeft->GetOrigin(), traceEnd, MASK_SHOT_RENDERMODEL, this ); + if ( trace.fraction < 1.0f ) { + if ( preLaserLeft.IsValid() ) { + if ( preLaserLeft->IsHidden() ) { + preLaserLeft->Show(); + } + preLaserLeft->Activate( true ); + preLaserLeft->SetTargetEntity( laserLeft.GetEntity() ); + } + if ( laserLeft->IsHidden() ) { + laserLeft->Show(); + } + laserLeft->SetOrigin( boneOrigin + viewAxis[0] * 60 ); + laserLeft->Activate( true ); + laserLeft->SetTargetLocation( trace.endpos + viewAxis[1] * 10 ); + } + float dist = (laserLeft->GetTargetLocation() - laserLeft->GetOrigin()).Length(); + if ( dist > 100.0f && impactLeftFx.IsValid() ) { + impactLeftFx->SetOrigin( laserLeft->GetTargetLocation() ); + } + laserEndLeft = trace.endpos; + } else { + if ( preLaserLeft.IsValid() ) { + preLaserLeft->Activate( false ); + } + if ( laserLeft.IsValid() ) { + laserLeft->Activate( false ); + } + } +} + +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhCreatureX::LinkScriptVariables() { + hhMonsterAI::LinkScriptVariables(); + LinkScriptVariable( AI_GUN_TRACKING ); + LinkScriptVariable( AI_RECHARGING ); + LinkScriptVariable( AI_LEFT_FIRE ); + LinkScriptVariable( AI_RIGHT_FIRE ); + LinkScriptVariable( AI_LEFT_DAMAGED ); + LinkScriptVariable( AI_RIGHT_DAMAGED ); + LinkScriptVariable( AI_FIRING_LASER ); + LinkScriptVariable( AI_HEALTH_TICK ); + LinkScriptVariable( AI_GUN_EXPLODE ); +} + +void hhCreatureX::Event_RightGunDeath() { + bool explodeGun = false; + if ( rightRetractBeam.IsValid() ) { + rightRetractBeam->Show(); + rightRetractBeam->Activate( true ); + + //retract the damage beam a little bit + idVec3 bonePos; + idMat3 boneAxis; + GetJointWorldTransform( spawnArgs.GetString( "gun_bone_right" ), bonePos, boneAxis ); + idVec3 lastTargetLocation = rightRetractBeam->GetTargetLocation(); + rightRetractBeam->SetTargetEntity( NULL ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, lastTargetLocation, bonePos, 10, 1 ); + } + rightRetractBeam->SetTargetLocation( lastTargetLocation + spawnArgs.GetFloat("retract_speed", "50") * (bonePos - lastTargetLocation).ToNormal() ); + if ( retractRightFx.IsValid() ) { + retractRightFx->SetOrigin( rightRetractBeam->GetTargetLocation() ); + } + if ( (lastTargetLocation - bonePos).LengthFast() < 30.0f ) { + explodeGun = true; + rightRetractBeam->Activate( false ); + } else { + PostEventSec( &MA_RightGunDeath, 0.01f ); + } + } + + if ( explodeGun || !rightRetractBeam.IsValid() ) { + CancelEvents( &MA_ResetRechargeBeam ); + SAFE_REMOVE( rightRechargeBeam ); + SAFE_REMOVE( retractRightFx ); + for ( int i=0;iActivate( true ); + rightBeamList[i]->SetTargetLocation( rightBeamList[i]->GetOrigin() + spawnArgs.GetFloat( "beam_length", "100" ) * hhUtils::RandomSpreadDir( GetEnemy()->GetOrigin().ToMat3(), 16.0f ) ); + } + } + StartSound( "snd_jenny_scream", SND_CHANNEL_ANY ); + PostEventSec(&MA_EndRightBeams, spawnArgs.GetFloat( "beam_duration_3", "1.0" ) ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_smoke"), spawnArgs.GetString("damage_bone_right"), NULL ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_gun_death"), spawnArgs.GetString("damage_bone_right"), NULL ); + AI_RIGHT_DAMAGED = true; + PostEventSec(&MA_SparkLeft, spawnArgs.GetFloat( "spark_freq", "1" ) + gameLocal.random.RandomFloat() ); + if ( AI_LEFT_DAMAGED ) { + SetSkinByName( spawnArgs.GetString( "skin_nogun_both" ) ); + } else { + SetSkinByName( spawnArgs.GetString( "skin_nogun_right" ) ); + } + } +} + +void hhCreatureX::Event_LeftGunDeath() { + bool explodeGun = false; + if ( leftRetractBeam.IsValid() ) { + leftRetractBeam->Show(); + leftRetractBeam->Activate( true ); + + //retract the damage beam a little bit + idVec3 bonePos; + idMat3 boneAxis; + GetJointWorldTransform( spawnArgs.GetString( "gun_bone_left" ), bonePos, boneAxis ); + idVec3 lastTargetLocation = leftRetractBeam->GetTargetLocation(); + leftRetractBeam->SetTargetEntity( NULL ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, lastTargetLocation, bonePos, 10, 1 ); + } + leftRetractBeam->SetTargetLocation( lastTargetLocation + spawnArgs.GetFloat("retract_speed", "50") * (bonePos - lastTargetLocation).ToNormal() ); + if ( retractLeftFx.IsValid() ) { + retractLeftFx->SetOrigin( leftRetractBeam->GetTargetLocation() ); + } + if ( (lastTargetLocation - bonePos).LengthFast() < 30.0f ) { + explodeGun = true; + leftRetractBeam->Activate( false ); + } else { + PostEventSec( &MA_LeftGunDeath, 0.01f ); + } + } + + if ( explodeGun || !leftRetractBeam.IsValid() ) { + CancelEvents( &MA_ResetRechargeBeam ); + SAFE_REMOVE( leftRechargeBeam ); + SAFE_REMOVE( retractLeftFx ); + for ( int i=0;iActivate( true ); + leftBeamList[i]->SetTargetLocation( leftBeamList[i]->GetOrigin() + spawnArgs.GetFloat( "beam_length", "100" ) * hhUtils::RandomSpreadDir( GetEnemy()->GetOrigin().ToMat3(), 16.0f ) ); + } + } + StartSound( "snd_jenny_scream", SND_CHANNEL_ANY ); + PostEventSec(&MA_EndLeftBeams, spawnArgs.GetFloat( "beam_duration_3", "1.0" ) ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_smoke"), spawnArgs.GetString("damage_bone_left"), NULL ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_gun_death"), spawnArgs.GetString("damage_bone_left"), NULL ); + AI_LEFT_DAMAGED = true; + PostEventSec(&MA_SparkLeft, spawnArgs.GetFloat( "spark_freq", "1" ) + gameLocal.random.RandomFloat() ); + if ( AI_RIGHT_DAMAGED ) { + SetSkinByName( spawnArgs.GetString( "skin_nogun_both" ) ); + } else { + SetSkinByName( spawnArgs.GetString( "skin_nogun_left" ) ); + } + } +} + + +bool hhCreatureX::UpdateAnimationControllers( void ) { + idVec3 local; + idVec3 focusPos; + idVec3 left; + idVec3 dir; + idVec3 orientationJointPos; + idVec3 localDir; + idAngles newLookAng; + idAngles diff; + idMat3 mat; + idMat3 axis; + idMat3 orientationJointAxis; + idAFAttachment *headEnt = head.GetEntity(); + idVec3 eyepos; + idVec3 pos; + int i; + idAngles jointAng; + float orientationJointYaw; + + if ( AI_DEAD ) { + + for (int i = 0; i < jawFlapList.Num(); i++) { + jawFlapInfo_t &flapInfo = jawFlapList[i]; + animator.ClearJoint( flapInfo.bone ); + } + animator.ClearJoint( leftEyeJoint ); + animator.ClearJoint( rightEyeJoint ); + + return idActor::UpdateAnimationControllers(); + } + + if ( orientationJoint == INVALID_JOINT ) { + orientationJointAxis = viewAxis; + orientationJointPos = physicsObj.GetOrigin(); + orientationJointYaw = current_yaw; + } else { + GetJointWorldTransform( orientationJoint, gameLocal.time, orientationJointPos, orientationJointAxis ); + orientationJointYaw = orientationJointAxis[ 2 ].ToYaw(); + orientationJointAxis = idAngles( 0.0f, orientationJointYaw, 0.0f ).ToMat3(); + } + + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorCyan, orientationJointPos, orientationJointPos + orientationJointAxis[0] * 64.0, 10, 1 ); + } + + if ( focusJoint != INVALID_JOINT ) { + if ( headEnt ) { + headEnt->GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis ); + } else { + // JRMMERGE_GRAVAXIS - What about GetGravAxis() are we still using/needing that? + GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis ); + } + eyeOffset.z = eyepos.z - physicsObj.GetOrigin().z; + } else { + eyepos = GetEyePosition(); + } + + if ( headEnt ) { + CopyJointsFromBodyToHead(); + } + + // Update the IK after we've gotten all the joint positions we need, but before we set any joint positions. + // Getting the joint positions causes the joints to be updated. The IK gets joint positions itself (which + // are already up to date because of getting the joints in this function) and then sets their positions, which + // forces the heirarchy to be updated again next time we get a joint or present the model. If IK is enabled, + // or if we have a seperate head, we end up transforming the joints twice per frame. Characters with no + // head entity and no ik will only transform their joints once. Set g_debuganim to the current entity number + // in order to see how many times an entity transforms the joints per frame. + idActor::UpdateAnimationControllers(); + + idEntity *focusEnt = focusEntity.GetEntity(); + //HUMANHEAD jsh allow eyefocus independent from allowJointMod + if ( ( !allowJointMod && !allowEyeFocus ) || ( gameLocal.time >= focusTime && focusTime != -1 ) || GetPhysics()->GetGravityNormal() != idVec3( 0,0,-1) ) { + focusPos = GetEyePosition() + orientationJointAxis[ 0 ] * 512.0f; + } else if ( focusEnt == NULL ) { + // keep looking at last position until focusTime is up + focusPos = currentFocusPos; + } else if ( focusEnt == enemy.GetEntity() ) { + focusPos = lastVisibleEnemyPos + lastVisibleEnemyEyeOffset - eyeVerticalOffset * enemy.GetEntity()->GetPhysics()->GetGravityNormal(); + } else if ( focusEnt->IsType( idActor::Type ) ) { + focusPos = static_cast( focusEnt )->GetEyePosition() - eyeVerticalOffset * focusEnt->GetPhysics()->GetGravityNormal(); + } else { + focusPos = focusEnt->GetPhysics()->GetOrigin(); + } + + currentFocusPos = currentFocusPos + ( focusPos - currentFocusPos ) * eyeFocusRate; + + // determine yaw from origin instead of from focus joint since joint may be offset, which can cause us to bounce between two angles + dir = focusPos - orientationJointPos; + newLookAng.yaw = idMath::AngleNormalize180( dir.ToYaw() - orientationJointYaw ); + newLookAng.roll = 0.0f; + newLookAng.pitch = 0.0f; + + newLookAng += lookOffset; + +#if 0 + gameRenderWorld->DebugLine( colorRed, orientationJointPos, focusPos, gameLocal.msec ); + gameRenderWorld->DebugLine( colorYellow, orientationJointPos, orientationJointPos + orientationJointAxis[ 0 ] * 32.0f, gameLocal.msec ); + gameRenderWorld->DebugLine( colorGreen, orientationJointPos, orientationJointPos + newLookAng.ToForward() * 48.0f, gameLocal.msec ); +#endif + +//JRMMERGE_GRAVAXIS: This changed to much to merge, see if you can get your monsters on planets changes back in here. I'll leave both versions +#if OLD_CODE + GetGravViewAxis().ProjectVector( dir, localDir ); // HUMANHEAD JRM: VIEWAXIS_TO_GETGRAVVIEWAXIS + lookAng.yaw = idMath::AngleNormalize180( localDir.ToYaw() ); + lookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ); + lookAng.roll = 0.0f; +#else + // determine pitch from joint position + dir = focusPos - eyepos; + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorYellow, eyepos, eyepos + dir, 10, 1 ); + } + dir.NormalizeFast(); + orientationJointAxis.ProjectVector( dir, localDir ); + newLookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ) + lookOffset.pitch; + newLookAng.roll = 0.0f; +#endif + + diff = newLookAng - lookAng; + + if ( eyeAng != diff ) { + eyeAng = diff; + eyeAng.Clamp( eyeMin, eyeMax ); + idAngles angDelta = diff - eyeAng; + if ( !angDelta.Compare( ang_zero, 0.1f ) ) { + alignHeadTime = gameLocal.time; + } else { + alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime; + } + } + + if ( idMath::Fabs( newLookAng.yaw ) < 0.1f ) { + alignHeadTime = gameLocal.time; + } + + if ( ( gameLocal.time >= alignHeadTime ) || ( gameLocal.time < forceAlignHeadTime ) ) { + alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime; + destLookAng = newLookAng; + destLookAng.Clamp( lookMin, lookMax ); + } + + diff = destLookAng - lookAng; + if ( ( lookMin.pitch == -180.0f ) && ( lookMax.pitch == 180.0f ) ) { + if ( ( diff.pitch > 180.0f ) || ( diff.pitch <= -180.0f ) ) { + diff.pitch = 360.0f - diff.pitch; + } + } + if ( ( lookMin.yaw == -180.0f ) && ( lookMax.yaw == 180.0f ) ) { + if ( diff.yaw > 180.0f ) { + diff.yaw -= 360.0f; + } else if ( diff.yaw <= -180.0f ) { + diff.yaw += 360.0f; + } + } + lookAng = lookAng + diff * headFocusRate; + lookAng.Normalize180(); + + jointAng.roll = 0.0f; + if ( allowJointMod ) { + for( i = 0; i < lookJoints.Num(); i++ ) { + jointAng.pitch = lookAng.pitch * lookJointAngles[ i ].pitch; + jointAng.yaw = lookAng.yaw * lookJointAngles[ i ].yaw; + animator.SetJointAxis( lookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() ); + } + } + + if ( move.moveType == MOVETYPE_FLY ) { + // lean into turns + AdjustFlyingAngles(); + } + + if ( headEnt ) { + idAnimator *headAnimator = headEnt->GetAnimator(); + + // HUMANHEAD pdm: Added support for look joints in head entities + if ( allowJointMod ) { + for( i = 0; i < headLookJoints.Num(); i++ ) { + jointAng.pitch = lookAng.pitch * headLookJointAngles[ i ].pitch; + jointAng.yaw = lookAng.yaw * headLookJointAngles[ i ].yaw; + headAnimator->SetJointAxis( headLookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() ); + } + } + // HUMANHEAD END + + if ( allowEyeFocus ) { + idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3(); idMat3 headTranspose = headEnt->GetPhysics()->GetAxis().Transpose(); + axis = eyeAxis * orientationJointAxis; + left = axis[ 1 ] * eyeHorizontalOffset; + eyepos -= headEnt->GetPhysics()->GetOrigin(); + headAnimator->SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose ); + headAnimator->SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f - left ) * headTranspose ); + + //if ( ai_debugMove.GetBool() ) { + // gameRenderWorld->DebugLine( colorRed, orientationJointPos, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose, gameLocal.msec ); + //} + } else { + headAnimator->ClearJoint( leftEyeJoint ); + headAnimator->ClearJoint( rightEyeJoint ); + } + } else { + if ( allowEyeFocus ) { + idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3(); + axis = eyeAxis * orientationJointAxis; + left = axis[ 1 ] * eyeHorizontalOffset; + eyepos += axis[ 0 ] * 64.0f - physicsObj.GetOrigin(); + animator.SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + left ); + animator.SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos - left ); + } else { + animator.ClearJoint( leftEyeJoint ); + animator.ClearJoint( rightEyeJoint ); + } + } + + //HUMANHEAD pdm jawflap + hhAnimator *theAnimator; + if (head.IsValid()) { + theAnimator = head->GetAnimator(); + } + else { + theAnimator = GetAnimator(); + } + JawFlap(theAnimator); + //END HUMANHEAD + + //update guns + //HUMANHEAD jsh PCF 4/27/06 no gun tracking + //if ( enemy.IsValid() ) { + // if ( AI_GUN_TRACKING && !AI_DEAD && !AI_RECHARGING ) { + // idVec3 focusPos, bonePos; + // idMat3 boneAxis; + + // idAngles ang = ang_zero; + // ang.pitch = (bonePos - enemy->GetOrigin()).ToPitch(); + // ang.pitch += gunShake.pitch; + // ang.yaw = (targetCurrent_L-bonePos).ToYaw() - viewAxis.ToAngles().yaw; + // ang.yaw += gunShake.yaw; + // GetJointWorldTransform( spawnArgs.GetString( "gun_bone_left" ), bonePos, boneAxis ); + // animator.SetJointAxis( animator.GetJointHandle( spawnArgs.GetString( "gun_bone_left" ) ), JOINTMOD_WORLD, idAngles( (bonePos - targetCurrent_L).ToPitch(), (targetCurrent_L-bonePos).ToYaw() - viewAxis.ToAngles().yaw, 0.0f ).ToMat3() ); + + // ang.yaw = (targetCurrent_R-bonePos).ToYaw() - viewAxis.ToAngles().yaw; + // ang.yaw += gunShake.yaw; + // GetJointWorldTransform( spawnArgs.GetString( "gun_bone_right" ), bonePos, boneAxis ); + // animator.SetJointAxis( animator.GetJointHandle( spawnArgs.GetString( "gun_bone_right" ) ), JOINTMOD_WORLD, idAngles( (bonePos - targetCurrent_R).ToPitch(), (targetCurrent_R-bonePos).ToYaw() - viewAxis.ToAngles().yaw, 0.0f ).ToMat3() ); + // } else { + // animator.SetJointAxis( animator.GetJointHandle( spawnArgs.GetString( "gun_bone_left" ) ), JOINTMOD_WORLD, mat3_identity ); + // animator.SetJointAxis( animator.GetJointHandle( spawnArgs.GetString( "gun_bone_right" ) ), JOINTMOD_WORLD, mat3_identity ); + // } + //} + + return true; +} + +void hhCreatureX::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + HandleNoGore(); + + //stop sparking upon death + CancelEvents( &MA_SparkLeft ); + CancelEvents( &MA_SparkRight ); + SAFE_REMOVE( preLaserLeft ); + SAFE_REMOVE( preLaserRight ); + AI_LEFT_DAMAGED = false; + AI_RIGHT_DAMAGED = false; + + if ( laserLeft.IsValid() ) { + laserLeft->Hide(); + } + if ( laserRight.IsValid() ) { + laserRight->Hide(); + } + + if ( spawnArgs.GetBool( "use_death_point", "0" ) ) { + if ( !AI_DEAD ) { + fl.takedamage = false; + AI_DEAD = true; + state = GetScriptFunction( "state_Predeath" ); + SetState( state ); + SetWaitState( "" ); + return; + } + } + + if ( AI_DEAD ) { + AI_DAMAGE = true; + return; + } + + fl.takedamage = false; + idAngles ang; + const char *modelDeath; + + // make sure the monster is activated + EndAttack(); + + if ( g_debugDamage.GetBool() ) { + gameLocal.Printf( "Damage: joint: '%s', zone '%s'\n", animator.GetJointName( ( jointHandle_t )location ), + GetDamageGroup( location ) ); + } + + if ( inflictor ) { + AI_SPECIAL_DAMAGE = inflictor->spawnArgs.GetInt( "special_damage" ); + } else { + AI_SPECIAL_DAMAGE = 0; + } + + if ( AI_DEAD ) { + AI_PAIN = true; + AI_DAMAGE = true; + return; + } + + // stop all voice sounds + StopSound( SND_CHANNEL_VOICE, false ); + if ( head.GetEntity() ) { + head.GetEntity()->StopSound( SND_CHANNEL_VOICE, false ); + head.GetEntity()->GetAnimator()->ClearAllAnims( gameLocal.time, 100 ); + } + + disableGravity = false; + move.moveType = MOVETYPE_DEAD; + af_push_moveables = false; + + physicsObj.UseFlyMove( false ); + physicsObj.ForceDeltaMove( false ); + + // end our looping ambient sound + StopSound( SND_CHANNEL_AMBIENT, false ); + + if ( attacker && attacker->IsType( idActor::Type ) ) { + gameLocal.AlertAI( ( idActor * )attacker ); + } + + // activate targets + ActivateTargets( attacker ); + + RemoveAttachments(); + RemoveProjectile(); + StopMove( MOVE_STATUS_DONE ); + + ClearEnemy(); + AI_DEAD = true; + + // HUMANHEAD jsh commented out for girlfriendx + // make monster nonsolid + if ( spawnArgs.GetBool( "boss" ) ) { + physicsObj.SetContents( 0 ); + physicsObj.GetClipModel()->Unlink(); + } + + Unbind(); + + // spawn death clip model + if ( spawnArgs.GetBool( "boss" ) ) { + idDict dict; + const idDict *torsoDict = gameLocal.FindEntityDefDict(spawnArgs.GetString("def_deathclip")); + dict.Copy(*torsoDict); + dict.SetVector("origin", GetOrigin() + modelOffset * GetAxis() ); + dict.SetMatrix("rotation", GetAxis()); + idEntity *e; + gameLocal.SpawnEntityDef(dict, &e); + if ( e ) { + e->GetPhysics()->SetContents( CONTENTS_PLAYERCLIP ); + } + } + + if ( StartRagdoll() ) { + StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL ); + // HUMANHEAD JRM - some monsters are removed, but always need to play sound + } else if(spawnArgs.GetBool("death_sound_always","0")) { + StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL ); + } + // HUMANHEAD JRM - end + + if ( spawnArgs.GetString( "model_death", "", &modelDeath ) ) { + // lost soul is only case that does not use a ragdoll and has a model_death so get the death sound in here + StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL ); + renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.time ); + SetModel( modelDeath ); + physicsObj.SetLinearVelocity( vec3_zero ); + physicsObj.PutToRest(); + physicsObj.DisableImpact(); + } + + restartParticles = false; + + state = GetScriptFunction( "state_Killed" ); + SetState( state ); + SetWaitState( "" ); + + if ( attacker && attacker->IsType( idPlayer::Type ) ) { + static_cast< idPlayer* >( attacker )->AddAIKill(); + } + + // General non-item dropping (for monsters, souls, etc.) + const idKeyValue *kv = NULL; + kv = spawnArgs.MatchPrefix( "def_drops", NULL ); + while ( kv ) { + + idStr drops = kv->GetValue(); + idDict args; + + idStr last5 = kv->GetKey().Right(5); + if ( drops.Length() && idStr::Icmp( last5, "Joint" ) != 0) { + args.Set( "classname", drops ); + + // HUMANHEAD pdm: specify monster so souls can call back to remove body when picked up + args.Set("monsterSpawnedBy", name.c_str()); + + idVec3 origin; + idMat3 axis; + idStr jointKey = kv->GetKey() + idStr("Joint"); + idStr jointName = spawnArgs.GetString( jointKey ); + idStr joint2JointKey = kv->GetKey() + idStr("Joint2Joint"); + idStr j2jName = spawnArgs.GetString( joint2JointKey ); + + idEntity *newEnt = NULL; + gameLocal.SpawnEntityDef( args, &newEnt ); + HH_ASSERT(newEnt != NULL); + + if(jointName.Length()) { + jointHandle_t joint = GetAnimator()->GetJointHandle( jointName ); + if (!GetAnimator()->GetJointTransform( joint, gameLocal.time, origin, axis ) ) { + gameLocal.Printf( "%s refers to invalid joint '%s' on entity '%s'\n", (const char*)jointKey.c_str(), (const char*)jointName, (const char*)name ); + origin = renderEntity.origin; + axis = renderEntity.axis; + } + axis *= renderEntity.axis; + origin = renderEntity.origin + origin * renderEntity.axis; + newEnt->SetAxis(axis); + newEnt->SetOrigin(origin); + } + else { + + newEnt->SetAxis(viewAxis); + newEnt->SetOrigin(GetOrigin()); + } + + } + + kv = spawnArgs.MatchPrefix( "def_drops", kv ); + } +} + +/* +===================== +hhCreatureX::Save +===================== +*/ +void hhCreatureX::Save( idSaveGame *savefile ) const { + laserRight.Save( savefile ); + laserLeft.Save( savefile ); + savefile->WriteBool( bLaserLeftActive ); + savefile->WriteBool( bLaserRightActive ); + + int i, num = leftBeamList.Num(); + savefile->WriteInt( num ); + for ( i = 0; i < num; i++ ) { + leftBeamList[i].Save( savefile ); + } + + num = rightBeamList.Num(); + savefile->WriteInt( num ); + for ( i = 0; i < num; i++ ) { + rightBeamList[i].Save( savefile ); + } + + savefile->WriteInt( numBurstBeams ); + savefile->WriteInt( leftGunHealth ); + savefile->WriteInt( rightGunHealth ); + savefile->WriteAngles( gunShake ); + savefile->WriteInt( nextBeamTime ); + savefile->WriteInt( nextLeftZapTime ); + savefile->WriteInt( nextRightZapTime ); + savefile->WriteInt( leftGunLives ); + savefile->WriteInt( rightGunLives ); + + savefile->WriteVec3( targetStart_L ); + savefile->WriteVec3( targetEnd_L ); + savefile->WriteVec3( targetCurrent_L ); + savefile->WriteVec3( targetStart_R ); + savefile->WriteVec3( targetEnd_R ); + savefile->WriteVec3( targetCurrent_R ); + + savefile->WriteFloat( targetAlpha_L ); + savefile->WriteFloat( targetAlpha_R ); + + savefile->WriteVec3( laserEndLeft ); + savefile->WriteVec3( laserEndRight ); + + savefile->WriteInt( nextLaserLeft ); + savefile->WriteInt( nextLaserRight ); + savefile->WriteInt( nextHealthTick ); + + leftRecharger.Save( savefile ); + rightRecharger.Save( savefile ); + + leftRechargeBeam.Save( savefile ); + rightRechargeBeam.Save( savefile ); + + leftDamageBeam.Save( savefile ); + rightDamageBeam.Save( savefile ); + + leftRetractBeam.Save( savefile ); + rightRetractBeam.Save( savefile ); + + preLaserLeft.Save( savefile ); + preLaserRight.Save( savefile ); + + muzzleLeftFx.Save( savefile ); + muzzleRightFx.Save( savefile ); + impactLeftFx.Save( savefile ); + impactRightFx.Save( savefile ); + rechargeLeftFx.Save( savefile ); + rechargeRightFx.Save( savefile ); + retractLeftFx.Save( savefile ); + retractRightFx.Save( savefile ); + + savefile->WriteInt( leftRechargerHealth ); + savefile->WriteInt( rightRechargerHealth ); + savefile->WriteBool( bScripted ); +} + +/* +===================== +hhCreatureX::Restore +===================== +*/ +void hhCreatureX::Restore( idRestoreGame *savefile ) { + laserRight.Restore( savefile ); + laserLeft.Restore( savefile ); + savefile->ReadBool( bLaserLeftActive ); + savefile->ReadBool( bLaserRightActive ); + + int i, num; + savefile->ReadInt( num ); + leftBeamList.SetNum( num ); + for ( i = 0; i < num; i++ ) { + leftBeamList[i].Restore( savefile ); + } + + savefile->ReadInt( num ); + rightBeamList.SetNum( num ); + for ( int i = 0; i < num; i++ ) { + rightBeamList[i].Restore( savefile ); + } + + savefile->ReadInt( numBurstBeams ); + savefile->ReadInt( leftGunHealth ); + savefile->ReadInt( rightGunHealth ); + savefile->ReadAngles( gunShake ); + savefile->ReadInt( nextBeamTime ); + savefile->ReadInt( nextLeftZapTime ); + savefile->ReadInt( nextRightZapTime ); + savefile->ReadInt( leftGunLives ); + savefile->ReadInt( rightGunLives ); + + savefile->ReadVec3( targetStart_L ); + savefile->ReadVec3( targetEnd_L ); + savefile->ReadVec3( targetCurrent_L ); + savefile->ReadVec3( targetStart_R ); + savefile->ReadVec3( targetEnd_R ); + savefile->ReadVec3( targetCurrent_R ); + + savefile->ReadFloat( targetAlpha_L ); + savefile->ReadFloat( targetAlpha_R ); + + savefile->ReadVec3( laserEndLeft ); + savefile->ReadVec3( laserEndRight ); + + savefile->ReadInt( nextLaserLeft ); + savefile->ReadInt( nextLaserRight ); + savefile->ReadInt( nextHealthTick ); + + leftRecharger.Restore( savefile ); + rightRecharger.Restore( savefile ); + + leftRechargeBeam.Restore( savefile ); + rightRechargeBeam.Restore( savefile ); + + leftDamageBeam.Restore( savefile ); + rightDamageBeam.Restore( savefile ); + + leftRetractBeam.Restore( savefile ); + rightRetractBeam.Restore( savefile ); + + preLaserLeft.Restore( savefile ); + preLaserRight.Restore( savefile ); + + muzzleLeftFx.Restore( savefile ); + muzzleRightFx.Restore( savefile ); + impactLeftFx.Restore( savefile ); + impactRightFx.Restore( savefile ); + rechargeLeftFx.Restore( savefile ); + rechargeRightFx.Restore( savefile ); + retractLeftFx.Restore( savefile ); + retractRightFx.Restore( savefile ); + + savefile->ReadInt( leftRechargerHealth ); + savefile->ReadInt( rightRechargerHealth ); + savefile->ReadBool( bScripted ); +} + + +void hhCreatureX::MuzzleLeftOn() { + if ( !muzzleLeftFx.IsValid() ) { + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_muzzle"), spawnArgs.GetString("muzzle_bone_left"), NULL, &MA_AssignLeftMuzzleFx, false ); + } + if ( !impactLeftFx.IsValid() ) { + BroadcastFxInfo( spawnArgs.GetString("fx_impact"), GetOrigin(), GetAxis(), NULL, &MA_AssignLeftImpactFx, false ); + } +} + +void hhCreatureX::MuzzleLeftOff() { + SAFE_REMOVE( muzzleLeftFx ); + SAFE_REMOVE( impactLeftFx ); +} + +void hhCreatureX::Event_AssignLeftMuzzleFx( hhEntityFx* fx ) { + muzzleLeftFx = fx; +} + +void hhCreatureX::Event_AssignRightMuzzleFx( hhEntityFx* fx ) { + muzzleRightFx = fx; +} + +void hhCreatureX::Event_AssignRightImpactFx( hhEntityFx* fx ) { + impactRightFx = fx; +} + +void hhCreatureX::Event_AssignLeftImpactFx( hhEntityFx* fx ) { + impactLeftFx = fx; +} + + +void hhCreatureX::Event_AssignLeftRechargeFx( hhEntityFx* fx ) { + rechargeLeftFx = fx; +} + +void hhCreatureX::Event_AssignRightRechargeFx( hhEntityFx* fx ) { + rechargeRightFx= fx; +} + +void hhCreatureX::MuzzleRightOn() { + if ( !muzzleRightFx.IsValid() ) { + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_muzzle"), spawnArgs.GetString("muzzle_bone_right"), NULL, &MA_AssignRightMuzzleFx, false ); + } + if ( !impactRightFx.IsValid() ) { + BroadcastFxInfo( spawnArgs.GetString("fx_impact"), GetOrigin(), GetAxis(), NULL, &MA_AssignRightImpactFx, false ); + } +} + +void hhCreatureX::MuzzleRightOff() { + SAFE_REMOVE( muzzleRightFx ); + SAFE_REMOVE( impactRightFx ); +} + +void hhCreatureX::Event_SetGunOffset( const idAngles &ang ) { + gunShake = ang; +} + +void hhCreatureX::Event_EndLeftBeams() { + for ( int i = 0; i < numBurstBeams; i ++ ) { + if( leftBeamList[i].IsValid() ) { + leftBeamList[i]->Activate( false ); + } + } +} + +void hhCreatureX::Event_EndRightBeams() { + for ( int i = 0; i < numBurstBeams; i ++ ) { + if( rightBeamList[i].IsValid() ) { + rightBeamList[i]->Activate( false ); + } + } +} + +void hhCreatureX::Event_StartRechargeBeams() { + StartSound( "snd_recharge_beam_start", SND_CHANNEL_ANY ); + if ( leftRecharger.IsValid() ) { + const char *defName = spawnArgs.GetString( "fx_recharge_beam_start" ); + if (defName && defName[0]) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + idEntityFx *rechargeFx = SpawnFxLocal( defName, leftRecharger->GetOrigin(), leftRecharger->GetAxis(), &fxInfo, gameLocal.isClient ); + if ( rechargeFx ) { + rechargeFx->Bind( leftRecharger.GetEntity(), true ); + } + } + if ( leftRechargeBeam.IsValid() ) { + leftRechargeBeam->Activate( true ); + leftRechargeBeam->SetTargetEntity( leftRecharger.GetEntity(), 0, leftRecharger->spawnArgs.GetVector( "offsetModel" ) ); + } + } + if ( rightRecharger.IsValid() ) { + const char *defName = spawnArgs.GetString( "fx_recharge_beam_start" ); + if (defName && defName[0]) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + idEntityFx *rechargeFx = SpawnFxLocal( defName, rightRecharger->GetOrigin(), rightRecharger->GetAxis(), &fxInfo, gameLocal.isClient ); + if ( rechargeFx ) { + rechargeFx->Bind( rightRecharger.GetEntity(), true ); + } + } + + if ( rightRechargeBeam.IsValid() ) { + rightRechargeBeam->Activate( true ); + rightRechargeBeam->SetTargetEntity( rightRecharger.GetEntity(), 0, rightRecharger->spawnArgs.GetVector( "offsetModel" ) ); + } + } +} + +void hhCreatureX::Event_GunRecharge( int onOff ) { + if ( onOff ) { + idVec3 boneOrigin; + idMat3 boneAxis; + GetJointWorldTransform( spawnArgs.GetString("laser_bone_right"), boneOrigin, boneAxis ); + BroadcastFxInfo( spawnArgs.GetString("fx_recharger_enter"), boneOrigin, boneAxis, NULL, NULL, false ); + + GetJointWorldTransform( spawnArgs.GetString("laser_bone_left"), boneOrigin, boneAxis ); + BroadcastFxInfo( spawnArgs.GetString("fx_recharger_enter"), boneOrigin, boneAxis, NULL, NULL, false ); + + PostEventSec( &MA_StartRechargeBeams, spawnArgs.GetFloat( "recharge_delay", "0.9" ) ); + if ( leftRecharger.IsValid() ) { + if ( leftRetractBeam.IsValid() ) { + leftRetractBeam->SetTargetEntity( leftRecharger.GetEntity(), 0, leftRecharger->spawnArgs.GetVector( "offsetModel" ) ); + } + leftRechargerHealth = leftRecharger->GetHealth(); + if ( leftDamageBeam.IsValid() ) { + leftDamageBeam->SetTargetEntity( leftRecharger.GetEntity(), 0, leftRecharger->spawnArgs.GetVector( "offsetModel" ) ); + } + leftRecharger->Show(); + leftRecharger->SetOrigin( GetOrigin() ); + idVec3 offset = spawnArgs.GetVector( "healer_offset", "0 80 60" ); + leftRecharger->MoveToPosition( GetOrigin() + viewAxis * offset ); + leftRecharger->SetState( leftRecharger->GetScriptFunction( "state_Healer" ) ); + leftRecharger->SetWaitState( "" ); + } + if ( rightRecharger.IsValid() ) { + if ( rightRetractBeam.IsValid() ) { + rightRetractBeam->SetTargetEntity( rightRecharger.GetEntity(), 0, rightRecharger->spawnArgs.GetVector( "offsetModel" ) ); + } + rightRechargerHealth = rightRecharger->GetHealth(); + if ( rightDamageBeam.IsValid() ) { + rightDamageBeam->SetTargetEntity( rightRecharger.GetEntity(), 0, rightRecharger->spawnArgs.GetVector( "offsetModel" ) ); + } + rightRecharger->Show(); + idVec3 offset = spawnArgs.GetVector( "healer_offset", "0 80 60" ); + offset.y *= -1; + rightRecharger->SetOrigin( GetOrigin() ); + rightRecharger->MoveToPosition( GetOrigin() + viewAxis * offset ); + rightRecharger->SetState( rightRecharger->GetScriptFunction( "state_Healer" ) ); + rightRecharger->SetWaitState( "" ); + } + if ( !AI_LEFT_DAMAGED ) { + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_weakpoint"), spawnArgs.GetString("recharge_bone_left"), NULL, &MA_AssignLeftRechargeFx, false ); + } + if ( !AI_RIGHT_DAMAGED ) { + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_weakpoint"), spawnArgs.GetString("recharge_bone_right"), NULL, &MA_AssignRightRechargeFx, false ); + } + } else { + SAFE_REMOVE( rechargeLeftFx ); + SAFE_REMOVE( rechargeRightFx ); + } +} + +void hhCreatureX::Event_EndRecharge() { + if ( leftRechargeBeam.IsValid() ) { + leftRechargeBeam->Activate( false ); + } + if ( leftRecharger.IsValid() ) { + leftRecharger->SetState( leftRecharger->GetScriptFunction( "state_Return" ) ); + } + if ( rightRechargeBeam.IsValid() ) { + rightRechargeBeam->Activate( false ); + } + if ( rightRecharger.IsValid() ) { + rightRecharger->SetState( rightRecharger->GetScriptFunction( "state_Return" ) ); + } + if ( rightDamageBeam.IsValid() ) { + rightDamageBeam->Activate( false ); + } + if ( leftDamageBeam.IsValid() ) { + leftDamageBeam->Activate( false ); + } + CancelEvents( &MA_ResetRechargeBeam ); + CancelEvents( &MA_StartRechargeBeams ); +} + +//HUMANHEAD jsh PCF 4/27/06 initialized proj and made sure ReturnEntity is called +void hhCreatureX::Event_AttackMissile( const char *jointname, const idDict *projDef, int boneDir ) { + idProjectile *proj = NULL; + + // Bonedir launch? + if((BOOL)boneDir) { + proj = hhProjectile::SpawnProjectile(projDef); + if ( proj ) { + idMat3 axis; + idVec3 muzzle; + GetMuzzle( jointname, muzzle, axis ); + proj->Create(this, muzzle, axis); + proj->Launch(muzzle, axis, vec3_zero); + } + } + else { + if ( shootTarget.IsValid() ) { + proj = LaunchProjectile( jointname, shootTarget.GetEntity(), false, projDef ); //HUMANHEAD mdc - pass projDef on for multiple proj support + } else { + proj = LaunchProjectile( jointname, enemy.GetEntity(), false, projDef ); //HUMANHEAD mdc - pass projDef on for multiple proj support + } + } + + idThread::ReturnEntity( proj ); +} + +void hhCreatureX::Event_SparkLeft() { + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_spark"), spawnArgs.GetString("damage_bone_left"), NULL ); + PostEventSec(&MA_SparkLeft, spawnArgs.GetFloat( "spark_freq", "1" ) + gameLocal.random.RandomFloat() ); +} + +void hhCreatureX::Event_SparkRight() { + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_spark"), spawnArgs.GetString("damage_bone_right"), NULL ); + PostEventSec(&MA_SparkRight, spawnArgs.GetFloat( "spark_freq", "1" ) + gameLocal.random.RandomFloat() ); +} + +void hhCreatureX::Event_HudEvent( const char* eventName ) { + idPlayer* player = gameLocal.GetLocalPlayer(); + if ( player && player->hud ) { + gameLocal.GetLocalPlayer()->hud->HandleNamedEvent( eventName ); + gameLocal.GetLocalPlayer()->hud->StateChanged(gameLocal.time); + } +} + +void hhCreatureX::Activate( idEntity *activator ) { + if ( preLaserLeft.IsValid() ) { + HH_ASSERT(!FLOAT_IS_NAN(preLaserLeft->GetOrigin().x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(preLaserLeft->GetOrigin().y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(preLaserLeft->GetOrigin().z)); //Test for bad origin + } + if ( preLaserRight.IsValid() ) { + HH_ASSERT(!FLOAT_IS_NAN(preLaserRight->GetOrigin().x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(preLaserRight->GetOrigin().y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(preLaserRight->GetOrigin().z)); //Test for bad origin + } + if ( laserLeft.IsValid() ) { + HH_ASSERT(!FLOAT_IS_NAN(laserLeft->GetOrigin().x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(laserLeft->GetOrigin().y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(laserLeft->GetOrigin().z)); //Test for bad origin + } + if ( laserRight.IsValid() ) { + HH_ASSERT(!FLOAT_IS_NAN(laserRight->GetOrigin().x)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(laserRight->GetOrigin().y)); //Test for bad origin + HH_ASSERT(!FLOAT_IS_NAN(laserRight->GetOrigin().z)); //Test for bad origin + } + + hhMonsterAI::Activate( activator ); +} + +void hhCreatureX::Show() { + hhMonsterAI::Show(); + if ( preLaserLeft.IsValid() ) { + preLaserLeft->Hide(); + preLaserLeft->Activate( false ); + } + if ( preLaserRight.IsValid() ) { + preLaserRight->Hide(); + preLaserRight->Activate( false ); + } + if ( laserLeft.IsValid() ) { + laserLeft->Hide(); + laserLeft->Activate( false ); + } + if ( laserRight.IsValid() ) { + laserRight->Hide(); + laserRight->Activate( false ); + } + if ( leftRechargeBeam.IsValid() ) { + leftRechargeBeam->Hide(); + leftRechargeBeam->Activate( false ); + } + if ( rightRechargeBeam.IsValid() ) { + rightRechargeBeam->Hide(); + rightRechargeBeam->Activate( false ); + } + if ( leftDamageBeam.IsValid() ) { + leftDamageBeam->Hide(); + leftDamageBeam->Activate( false ); + } + if ( rightDamageBeam.IsValid() ) { + rightDamageBeam->Hide(); + rightDamageBeam->Activate( false ); + } + if ( leftRetractBeam.IsValid() ) { + leftRetractBeam->Hide(); + leftRetractBeam->Activate( false ); + } + if ( rightRetractBeam.IsValid() ) { + rightRetractBeam->Hide(); + rightRetractBeam->Activate( false ); + } +} + +void hhCreatureX::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if ( spawnArgs.GetBool( "boss" ) ) { + if ( attacker && attacker->IsType( idPlayer::Type ) && idStr::Icmp( damageDefName, spawnArgs.GetString( "def_damageTelefrag", "damage_telefrag" ) ) == 0 ) { + //telefragged so move somewhere nice + SetState( GetScriptFunction( "state_Telefragged" ) ); + return; + } + } + + if ( !fl.takedamage ) { + return; + } + //check for splash damage or jenny-specific damage based on location + if ( AI_DEAD && spawnArgs.GetBool( "boss" ) ) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( ( damageDef && damageDef->GetFloat( "radius" ) > 0 ) || + ( location && strcmp( GetDamageGroup( location ), "jenny" ) == 0 ) ) { + SetState( GetScriptFunction( "state_RealDeath" ) ); + fl.takedamage = false; + } + } + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_creaturex.h b/src/Prey/ai_creaturex.h new file mode 100644 index 0000000..af4b5b7 --- /dev/null +++ b/src/Prey/ai_creaturex.h @@ -0,0 +1,139 @@ + +#ifndef __PREY_AI_CREATURE_H__ +#define __PREY_AI_CREATURE_H__ + + +class hhCreatureX : public hhMonsterAI { + +public: + + CLASS_PROTOTYPE(hhCreatureX); +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_GunRecharge( int onOff ) {}; + void Event_LaserOn() {}; + void Event_LaserOff() {}; + void Event_UpdateLasers() {}; + void Event_SetGunOffset( const idAngles &ang ) {}; + void Event_EndLeftBeams() {}; + void Event_EndRightBeams() {}; + void Event_AssignRightMuzzleFx( hhEntityFx* fx ) {}; + void Event_AssignLeftMuzzleFx( hhEntityFx* fx ) {}; + void Event_AssignRightImpactFx( hhEntityFx* fx ) {}; + void Event_AssignLeftImpactFx( hhEntityFx* fx ) {}; + void Event_AssignLeftRechargeFx( hhEntityFx* fx ) {}; + void Event_AssignRightRechargeFx( hhEntityFx* fx ) {}; + void Event_AttackMissile( const char *jointname, const idDict *projDef, int boneDir ) {}; + void Event_HudEvent( const char *eventName ) {}; + void Event_RechargeHealth() {}; + void Event_EndRecharge() {}; + void Event_ResetRechargeBeam() {}; + void Event_SparkLeft() {}; + void Event_SparkRight() {}; + void Event_LeftGunDeath() {}; + void Event_RightGunDeath() {}; + void Event_StartRechargeBeams() {}; +#else + + ~hhCreatureX(); + void Spawn(); + void LinkScriptVariables( void ); + void Think(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + + idScriptBool AI_GUN_TRACKING; + idScriptBool AI_RECHARGING; + idScriptBool AI_LEFT_FIRE; + idScriptBool AI_RIGHT_FIRE; + idScriptBool AI_LEFT_DAMAGED; + idScriptBool AI_RIGHT_DAMAGED; + idScriptBool AI_FIRING_LASER; + idScriptBool AI_HEALTH_TICK; + idScriptBool AI_GUN_EXPLODE; +protected: + void Event_GunRecharge( int onOff ); + void Event_LaserOn(); + void Event_LaserOff(); + void Event_UpdateLasers(); + void Event_SetGunOffset( const idAngles &ang ); + void Event_EndLeftBeams(); + void Event_EndRightBeams(); + void Event_AssignRightMuzzleFx( hhEntityFx* fx ); + void Event_AssignLeftMuzzleFx( hhEntityFx* fx ); + void Event_AssignRightImpactFx( hhEntityFx* fx ); + void Event_AssignLeftImpactFx( hhEntityFx* fx ); + void Event_AssignLeftRechargeFx( hhEntityFx* fx ); + void Event_AssignRightRechargeFx( hhEntityFx* fx ); + void Event_AttackMissile( const char *jointname, const idDict *projDef, int boneDir ); + void Event_HudEvent( const char *eventName ); + void Event_RechargeHealth(); + void Event_EndRecharge(); + void Event_ResetRechargeBeam(); + void Event_SparkLeft(); + void Event_SparkRight(); + void Event_LeftGunDeath(); + void Event_RightGunDeath(); + void Event_StartRechargeBeams(); + + void MuzzleLeftOn(); + void MuzzleRightOn(); + void MuzzleLeftOff(); + void MuzzleRightOff(); + bool UpdateAnimationControllers( void ); + virtual void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + void Show(); + void Activate( idEntity *activator ); + idEntityPtr laserRight; + idEntityPtr laserLeft; + idEntityPtr preLaserRight; + idEntityPtr preLaserLeft; + idList< idEntityPtr > leftBeamList; + idList< idEntityPtr > rightBeamList; + idEntityPtr leftRechargeBeam; + idEntityPtr rightRechargeBeam; + idEntityPtr leftDamageBeam; + idEntityPtr rightDamageBeam; + idEntityPtr leftRetractBeam; + idEntityPtr rightRetractBeam; + idEntityPtr muzzleLeftFx; + idEntityPtr muzzleRightFx; + idEntityPtr impactLeftFx; + idEntityPtr impactRightFx; + idEntityPtr rechargeLeftFx; + idEntityPtr rechargeRightFx; + idEntityPtr retractLeftFx; + idEntityPtr retractRightFx; + int numBurstBeams; + bool bLaserLeftActive; + bool bLaserRightActive; + idVec3 targetStart_L; + idVec3 targetEnd_L; + idVec3 targetCurrent_L; + idVec3 targetStart_R; + idVec3 targetEnd_R; + idVec3 targetCurrent_R; + float targetAlpha_L; + float targetAlpha_R; + int leftGunHealth; + int rightGunHealth; + int leftGunLives; + int rightGunLives; + idAngles gunShake; + int nextBeamTime; + int nextLeftZapTime; + int nextRightZapTime; + idVec3 laserEndLeft; + idVec3 laserEndRight; + int nextLaserLeft; + int nextLaserRight; + int nextHealthTick; + int leftRechargerHealth; + int rightRechargerHealth; + idEntityPtr leftRecharger; + idEntityPtr rightRecharger; + bool bScripted; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif /* __PREY_AI_CREATURE_H__ */ diff --git a/src/Prey/ai_droid.cpp b/src/Prey/ai_droid.cpp new file mode 100644 index 0000000..a183212 --- /dev/null +++ b/src/Prey/ai_droid.cpp @@ -0,0 +1,660 @@ + + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_StartBurst("startBurst"); +const idEventDef EV_EndBurst("endBurst"); +const idEventDef EV_StartStaticBeam("startStaticBeam"); +const idEventDef EV_EndStaticBeam("endStaticBeam"); +const idEventDef EV_StartChargeShot("startChargeShot"); +const idEventDef EV_EndChargeShot("endChargeShot"); +const idEventDef EV_StartPathing("startPathing"); +const idEventDef EV_EndPathing("endPathing"); +const idEventDef EV_EndZipBeams(""); +const idEventDef EV_HealerReset(""); + +CLASS_DECLARATION(hhMonsterAI, hhDroid) + EVENT(EV_StartStaticBeam, hhDroid::Event_StartStaticBeam) + EVENT(EV_EndStaticBeam, hhDroid::Event_EndStaticBeam) + EVENT(EV_StartBurst, hhDroid::Event_StartBurst) + EVENT(EV_EndBurst, hhDroid::Event_EndBurst) + EVENT(EV_StartChargeShot, hhDroid::Event_StartChargeShot) + EVENT(EV_EndChargeShot, hhDroid::Event_EndChargeShot) + EVENT(EV_StartPathing, hhDroid::Event_StartPathing) + EVENT(EV_EndPathing, hhDroid::Event_EndPathing) + EVENT(EV_EndZipBeams, hhDroid::Event_EndZipBeams) + EVENT(EV_HealerReset, hhDroid::Event_HealerReset) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +hhDroid::hhDroid() { + burstLength = 0.0; + burstSpread = 0.0; + burstDuration = 0.0; + staticDuration = 0.0; + staticRange = 0.0; + numBurstBeams = 0; + beamOffset = vec3_zero; + chargeShotSize = 0.0; + old_fly_bob_strength = 0.0; + spinAngle = 0.0f; +} + +hhDroid::~hhDroid() { + for ( int i = 0; i < numBurstBeams; i ++ ) { + SAFE_REMOVE( beamBurstList[i] ); + } +} + +void hhDroid::Show( void ) { + hhMonsterAI::Show(); + + PostEventSec(&EV_StartStaticBeam, gameLocal.random.RandomFloat() * spawnArgs.GetFloat( "staticFreq", "2.5" ) ); +} + +void hhDroid::Spawn(void) +{ + Event_SetMoveType(MOVETYPE_FLY); + lives = spawnArgs.GetInt( "lives", "3" ); + numBurstBeams = spawnArgs.GetInt( "numBurstBeams", "5" ); + burstLength = spawnArgs.GetFloat( "burstLength", "250" ); + staticDuration = spawnArgs.GetFloat( "staticDuration", "2.0" ); + burstDuration = spawnArgs.GetFloat( "burstDuration", "2.0" ); + beamOffset = spawnArgs.GetVector( "beamOffset", "0 0 0" ); + burstSpread = spawnArgs.GetFloat( "burstSpread", "1.0" ); + enemyRange = spawnArgs.GetFloat( "enemy_range", "3000" ); + flyDampening = spawnArgs.GetFloat( "fly_dampening", "0.01" ); + bHealer = spawnArgs.GetBool( "healer" ); + + beamZip = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamZip" ) ); + if( beamZip.IsValid() ) { + beamZip->Activate( false ); + beamZip->SetOrigin( GetPhysics()->GetOrigin() + beamOffset ); + beamZip->Bind( this, false ); + } + + for ( int i = 0; i < numBurstBeams; i ++ ) { + if ( gameLocal.random.RandomFloat() > 0.5f ) { + beamBurstList.Append( hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamBurst1" ) ) ); + } else { + beamBurstList.Append( hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamBurst2" ) ) ); + } + if( beamBurstList[i].IsValid() ) { + beamBurstList[i]->Activate( false ); + beamBurstList[i]->SetOrigin( GetPhysics()->GetOrigin() + beamOffset ); + beamBurstList[i]->Bind( this, false ); + } + } + + PostEventSec(&EV_StartStaticBeam, gameLocal.random.RandomFloat() * spawnArgs.GetFloat( "staticFreq", "2.5" ) ); +} + +bool hhDroid::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + bool bPain = hhMonsterAI::Pain( inflictor, attacker, damage, dir, location ); + if ( !bPain ) { + return bPain; + } + + //activate some beams for damage fx + for ( int i = 0; i < beamBurstList.Num(); i ++ ) { + if ( beamBurstList[i].IsValid() ) { + beamBurstList[i]->Activate( true ); + beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( dir.ToMat3(), burstSpread ) ); + PostEventSec(&EV_EndBurst, staticDuration); + } + } + return bPain; +} + +void hhDroid::Think() { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { + return; + } + + hhMonsterAI::Think(); + + //update burst beams + idMat3 dir; + if ( enemy.IsValid() ) { + dir = (enemy->GetPhysics()->GetOrigin() - GetPhysics()->GetOrigin()).ToMat3(); + } else { + dir = GetPhysics()->GetAxis(); + } + for ( int i = 0; i < beamBurstList.Num(); i ++ ) { + if ( beamBurstList[i].IsValid() && beamBurstList[i]->IsActivated() ) { + beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), burstSpread ) ); + } + } + + if ( staticPoint.IsValid() && beamBurstList[0].IsValid() ) { + beamBurstList[0]->SetTargetLocation( staticPoint->GetPhysics()->GetOrigin() ); + } + + if ( chargeShot.IsValid() ) { + chargeShot->SetShaderParm( 9, 1 ); + chargeShot->SetShaderParm( 10, chargeShotSize ); + chargeShotSize += spawnArgs.GetFloat( "chargeSizeDelta" ); + chargeShot->GetPhysics()->DisableClip(); + } +} + +void hhDroid::Event_StartChargeShot(void) { + //spawn chargeshot projectile + const idDict *projectileDef = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_chargeshot") ); + if ( projectileDef ) { + chargeShot = hhProjectile::SpawnProjectile( projectileDef ); + if ( chargeShot.IsValid() ) { + chargeShotSize = spawnArgs.GetFloat( "chargeSizeStart", "1.0" ); + idVec3 launchStart = GetPhysics()->GetOrigin() + spawnArgs.GetVector( "chargeOffset", "15 0 0" ) * GetRenderEntity()->axis; + chargeShot->Create(this, launchStart, GetRenderEntity()->axis); + chargeShot->Launch(chargeShot->GetPhysics()->GetOrigin(), chargeShot->GetPhysics()->GetAxis(), vec3_zero ); + chargeShot->Bind(this, true); + } + } +} + +void hhDroid::Event_EndChargeShot(void) { + //launch projectile + if ( chargeShot.IsValid() ) { + chargeShot->Unbind(); + chargeShot->StartTracking(); + chargeShot.Clear(); + } +} + +void hhDroid::Event_StartBurst(void) { + gameLocal.RadiusDamage( GetPhysics()->GetOrigin(), this, this, this, this, spawnArgs.GetString("def_burstdamage") ); + + //burst effect + idMat3 dir; + if ( enemy.IsValid() ) { + dir = (enemy->GetPhysics()->GetOrigin() - GetPhysics()->GetOrigin()).ToMat3(); + } else { + dir = GetPhysics()->GetAxis(); + } + for ( int i = 0; i < beamBurstList.Num(); i++ ) { + if ( beamBurstList[i].IsValid() ) { + beamBurstList[i]->Activate( true ); + beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( hhUtils::RandomVector().ToMat3(), burstSpread ) ); + } + } + PostEventSec(&EV_EndBurst, burstDuration); +} + +void hhDroid::Event_EndBurst(void) { + for ( int i = 0; i < beamBurstList.Num(); i ++ ) { + if ( beamBurstList[i].IsValid() ) { + beamBurstList[i]->Activate( false ); + } + } +} + +void hhDroid::Event_EndStaticBeam(void) { + if ( beamBurstList[0].IsValid() ) { + beamBurstList[0]->Activate( false ); + } + if ( staticPoint.IsValid() ) { + staticPoint.Clear(); + } + + //wait random time until next static beam + if ( !IsHidden() && !AI_DEAD ) { + PostEventSec(&EV_StartStaticBeam, gameLocal.random.RandomFloat() * spawnArgs.GetFloat( "staticFreq", "2.5" ) ); + } +} + +void hhDroid::Event_StartStaticBeam(void) { + idVec3 targetPoint; + idEntity *entityList[ MAX_GENTITIES ]; + + if ( IsHidden() || AI_DEAD ) { + return; + } + + //find nearest staticPoint + float radius = spawnArgs.GetFloat( "staticRange" ); + int listedEntities = gameLocal.EntitiesWithinRadius( GetPhysics()->GetOrigin(), radius, entityList, MAX_GENTITIES ); + for( int i = 0; i < listedEntities; i++ ) { + if ( entityList[ i ] && entityList[ i ]->spawnArgs.GetInt( "droidBeam", "0" ) ) { + staticPoint = entityList[ i ]; + break; + } + } + + //activate one beam + if ( staticPoint.IsValid() && beamBurstList[0].IsValid() ) { + StartSound( "snd_staticBeam", SND_CHANNEL_BODY, 0, true, NULL ); + beamBurstList[0]->Activate( true ); + beamBurstList[0]->SetTargetLocation( beamBurstList[0]->GetOrigin() + burstLength * hhUtils::RandomVector() ); + staticPoint->ActivateTargets( this ); + PostEventSec(&EV_EndStaticBeam, staticDuration); + } +} + +void hhDroid::FlyTurn( void ) { + if ( AI_PATHING ) { + hhMonsterAI::FlyTurn(); + return; + } + if ( AI_ENEMY_VISIBLE || move.moveCommand == MOVE_FACE_ENEMY ) { + TurnToward( lastVisibleEnemyPos ); + } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + } else if ( move.speed > 0.0f ) { + const idVec3 &vel = physicsObj.GetLinearVelocity(); + if ( vel.ToVec2().LengthSqr() > 0.1f ) { + TurnToward( vel.ToYaw() ); + } + } + Turn(); +} + +void hhDroid::Event_StartPathing(void) { + AI_PATHING = true; + old_fly_bob_strength = fly_bob_strength; + fly_bob_strength = 0.0; + ignore_obstacles = true; +} + +void hhDroid::Event_EndPathing(void) { + AI_PATHING = false; + fly_bob_strength = old_fly_bob_strength; + ignore_obstacles = false; +} + +void hhDroid::FlyMove( void ) { + idVec3 goalPos; + idVec3 oldorigin; + idVec3 newDest; + + AI_BLOCKED = false; + if ( ( move.moveCommand != MOVE_NONE ) && ReachedPos( move.moveDest, move.moveCommand ) ) { + if ( AI_PATHING ) { + physicsObj.SetLinearVelocity( idVec3(0,0,0) ); + } + StopMove( MOVE_STATUS_DONE ); + } + + if ( move.moveCommand != MOVE_TO_POSITION_DIRECT ) { + idVec3 vel = physicsObj.GetLinearVelocity(); + + if ( GetMovePos( goalPos ) ) { + move.obstacle = NULL; + } + + if ( move.speed ) { + FlySeekGoal( vel, goalPos ); + } + + // add in bobbing + AddFlyBob( vel ); + + if ( enemy.GetEntity() && ( move.moveCommand != MOVE_TO_POSITION ) ) { + float dist = ( GetPhysics()->GetOrigin() - enemy->GetPhysics()->GetOrigin() ).LengthFast(); + if ( dist < enemyRange && enemyRange > 0 ) { + AdjustFlyHeight( vel, goalPos ); + } + } + + AdjustFlySpeed( vel ); + + physicsObj.SetLinearVelocity( vel ); + } + + // turn + FlyTurn(); + + // run the physics for this frame + oldorigin = physicsObj.GetOrigin(); + physicsObj.UseFlyMove( true ); + physicsObj.UseVelocityMove( false ); + physicsObj.SetDelta( vec3_zero ); + physicsObj.ForceDeltaMove( disableGravity ); + RunPhysics(); + + monsterMoveResult_t moveResult = physicsObj.GetMoveResult(); + if ( !af_push_moveables && attack.Length() && TestMelee() ) { + DirectDamage( attack, enemy.GetEntity() ); + } else { + idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); + if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + } else if ( moveResult == MM_BLOCKED ) { + move.blockTime = gameLocal.time + 500; + AI_BLOCKED = true; + } + } + + idVec3 org = physicsObj.GetOrigin(); + if ( oldorigin != org ) { + TouchTriggers(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 4000 ); + gameRenderWorld->DebugBounds( colorOrange, physicsObj.GetBounds(), org, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); + gameRenderWorld->DebugLine( colorRed, org, org + physicsObj.GetLinearVelocity(), gameLocal.msec, true ); + gameRenderWorld->DebugLine( colorBlue, org, goalPos, gameLocal.msec, true ); + gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); + DrawRoute(); + } +} + +idProjectile *hhDroid::LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { + //jsh overridden to allow per-projectile accuracy + idVec3 muzzle; + idVec3 dir; + idVec3 start; + trace_t tr; + idBounds projBounds; + float distance; + const idClipModel *projClip; + float attack_accuracy; + float attack_cone; + float projectile_spread; + float diff; + float angle; + float spin; + idAngles ang; + int num_projectiles; + int i; + idMat3 axis; + idVec3 tmp; + idProjectile *lastProjectile; + + //HUMANHEAD mdc - added to support multiple projectiles + if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile + int projIndex = FindProjectileInfo( desiredProjectileDef ); + if( projIndex >= 0 ) { + SetCurrentProjectile( projIndex ); + } + } + //HUMANHEAD END + + if ( !projectileDef ) { + gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); + return NULL; + } + + if ( projectileDef->GetFloat( "attack_accuracy" ) ) { + attack_accuracy = projectileDef->GetFloat( "attack_accuracy", "7" ); + } else { + attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); + } + attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); + if ( projectileDef->GetFloat( "projectile_spread" ) ) { + projectile_spread = projectileDef->GetFloat( "projectile_spread", "0" ); + } else { + projectile_spread = spawnArgs.GetFloat( "projectile_spread", "0" ); + } + if ( projectileDef->GetFloat( "num_projectiles" ) ) { + num_projectiles = projectileDef->GetFloat( "num_projectiles", "1" ); + } else { + num_projectiles = spawnArgs.GetFloat( "num_projectiles", "1" ); + } + + + GetMuzzle( jointname, muzzle, axis ); + + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, axis[ 0 ] ); + } + + lastProjectile = projectile.GetEntity(); + + if ( target != NULL ) { + tmp = target->GetPhysics()->GetAbsBounds().GetCenter() - muzzle; + tmp.Normalize(); + axis = tmp.ToMat3(); + } else { + axis = viewAxis; + } + + // rotate it because the cone points up by default + tmp = axis[2]; + axis[2] = axis[0]; + axis[0] = -tmp; + + // make sure the projectile starts inside the monster bounding box + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + projClip = lastProjectile->GetPhysics()->GetClipModel(); + projBounds = projClip->GetBounds().Rotate( axis ); + + // check if the owner bounds is bigger than the projectile bounds + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { + start = muzzle + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); + muzzle = tr.endpos; + + // set aiming direction + GetAimDir( muzzle, target, this, dir ); + ang = dir.ToAngles(); + + // adjust his aim so it's not perfect. uses sine based movement so the tracers appear less random in their spread. + float t = MS2SEC( gameLocal.time + entityNumber * 497 ); + ang.pitch += idMath::Sin16( t * 5.1 ) * attack_accuracy; + ang.yaw += idMath::Sin16( t * 6.7 ) * attack_accuracy; + + if ( !AI_WALLWALK && clampToAttackCone ) { + // clamp the attack direction to be within monster's attack cone so he doesn't do + // things like throw the missile backwards if you're behind him + diff = idMath::AngleDelta( ang.yaw, current_yaw ); + if ( diff > attack_cone ) { + ang.yaw = current_yaw + attack_cone; + } else if ( diff < -attack_cone ) { + ang.yaw = current_yaw - attack_cone; + } + } + + axis = ang.ToMat3(); + + float spreadRad = DEG2RAD( projectile_spread ); + for( i = 0; i < num_projectiles; i++ ) { + // spread the projectiles out + angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); + spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); + dir = axis[ 0 ] + axis[ 2 ] * ( angle * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir.Normalize(); + + // launch the projectile + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, dir ); + } + lastProjectile = projectile.GetEntity(); + lastProjectile->Launch( muzzle, dir, vec3_origin ); + projectile = NULL; + } + + TriggerWeaponEffects( muzzle, axis ); + + lastAttackTime = gameLocal.time; + +//HUMANHEAD mdc - added to support multiple projectiles + projectile = NULL; + SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side +//HUMANHEAD END + + return lastProjectile; +} + +void hhDroid::Event_FlyZip() { + idVec3 old_origin = GetOrigin(); + hhMonsterAI::Event_FlyZip(); + if ( old_origin != GetOrigin() ) { + if ( beamZip.IsValid() ) { + beamZip->SetTargetLocation( old_origin ); + } + if ( beamZip.IsValid() ) { + beamZip->Activate( true ); + } + PostEventSec( &EV_EndZipBeams, spawnArgs.GetFloat( "zip_beam_duration", "1.0" ) ); + } +} + +void hhDroid::Event_EndZipBeams() { + if ( beamZip.IsValid() ) { + beamZip->Activate( false ); + } +} + +void hhDroid::Event_HealerReset() { + fl.takedamage = true; +} + +void hhDroid::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if ( bHealer ) { + lives--; + if ( lives <= 0 ) { + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + } else { + const char *defName = spawnArgs.GetString( "fx_pain" ); + if (defName && defName[0]) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + idEntityFx *painFx = SpawnFxLocal( defName, GetOrigin(), GetAxis(), &fxInfo, gameLocal.isClient ); + if ( painFx ) { + painFx->Bind( this, true ); + } + } + health = spawnArgs.GetInt( "health" ); + fl.takedamage = false; + PostEventSec( &EV_HealerReset, spawnArgs.GetFloat( "reset_delay", "0.8" ) ); + } + return; + } + if ( AI_DEAD ) { + AI_DAMAGE = true; + return; + } + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + for ( int i = 0; i < beamBurstList.Num(); i ++ ) { + if ( beamBurstList[i].IsValid() ) { + beamBurstList[i]->Activate( true ); + beamBurstList[i]->SetTargetLocation( beamBurstList[i]->GetOrigin() + burstLength * hhUtils::RandomSpreadDir( dir.ToMat3(), burstSpread ) ); + PostEventSec(&EV_EndBurst, staticDuration); + } + } +} + +void hhDroid::AdjustFlySpeed( idVec3 &vel ) { + float speed; + + // apply dampening + vel -= vel * flyDampening * MS2SEC( gameLocal.msec ); + + // gradually speed up/slow down to desired speed + speed = vel.Normalize(); + speed += ( move.speed - speed ) * MS2SEC( gameLocal.msec ); + if ( speed < 0.0f ) { + speed = 0.0f; + } else if ( move.speed && ( speed > move.speed ) ) { + speed = move.speed; + } + + vel *= speed; +} + +void hhDroid::UpdateModelTransform( void ) { + idVec3 origin; + idMat3 axis; + + if ( GetPhysicsToVisualTransform( origin, axis ) ) { + renderEntity.axis = axis * GetPhysics()->GetAxis(); + renderEntity.origin = GetPhysics()->GetOrigin() + origin * renderEntity.axis; + + if ( bHealer ) { + renderEntity.axis = idAngles( 0, 0, spinAngle ).ToMat3() * renderEntity.axis; + spinAngle += 10; + if ( spinAngle > 360.0f ) { + spinAngle = 0.0f; + } + } + } else { + renderEntity.axis = GetPhysics()->GetAxis(); + renderEntity.origin = GetPhysics()->GetOrigin(); + } +} + +/* +===================== +hhDroid::Save +===================== +*/ +void hhDroid::Save( idSaveGame *savefile ) const { + int num = beamBurstList.Num(); + savefile->WriteInt( num ); + for ( int i = 0; i < num; i++ ) { + beamBurstList[i].Save( savefile ); + } + + staticPoint.Save( savefile ); + chargeShot.Save( savefile ); + beamZip.Save( savefile ); + + savefile->WriteVec3( savedGravity ); + savefile->WriteFloat( chargeShotSize ); + savefile->WriteFloat( burstLength ); + savefile->WriteFloat( burstSpread ); + savefile->WriteFloat( burstDuration ); + savefile->WriteFloat( staticDuration ); + savefile->WriteFloat( staticRange ); + savefile->WriteInt( numBurstBeams ); + savefile->WriteVec3( beamOffset ); + savefile->WriteFloat( old_fly_bob_strength ); + savefile->WriteFloat( enemyRange ); + savefile->WriteFloat( flyDampening ); + savefile->WriteFloat( spinAngle ); + savefile->WriteBool( bHealer ); + savefile->WriteInt( lives ); +} + +/* +===================== +hhDroid::Restore +===================== +*/ +void hhDroid::Restore( idRestoreGame *savefile ) { + int num; + savefile->ReadInt( num ); + beamBurstList.SetNum( num ); + for ( int i = 0; i < num; i++ ) { + beamBurstList[i].Restore( savefile ); + } + + staticPoint.Restore( savefile ); + chargeShot.Restore( savefile ); + beamZip.Restore( savefile ); + + savefile->ReadVec3( savedGravity ); + savefile->ReadFloat( chargeShotSize ); + savefile->ReadFloat( burstLength ); + savefile->ReadFloat( burstSpread ); + savefile->ReadFloat( burstDuration ); + savefile->ReadFloat( staticDuration ); + savefile->ReadFloat( staticRange ); + savefile->ReadInt( numBurstBeams ); + savefile->ReadVec3( beamOffset ); + savefile->ReadFloat( old_fly_bob_strength ); + savefile->ReadFloat( enemyRange ); + savefile->ReadFloat( flyDampening ); + savefile->ReadFloat( spinAngle ); + savefile->ReadBool( bHealer ); + savefile->ReadInt( lives ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_droid.h b/src/Prey/ai_droid.h new file mode 100644 index 0000000..d483ef8 --- /dev/null +++ b/src/Prey/ai_droid.h @@ -0,0 +1,73 @@ + +#ifndef __PREY_AI_DROID_H__ +#define __PREY_AI_DROID_H__ + +class hhDroid : public hhMonsterAI { + +public: + + CLASS_PROTOTYPE(hhDroid); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_StartBurst(void) {}; + void Event_EndBurst(void) {}; + void Event_StartStaticBeam(void) {}; + void Event_EndStaticBeam(void) {}; + void Event_StartChargeShot(void) {}; + void Event_EndChargeShot(void) {}; + void Event_StartPathing(void) {}; + void Event_EndPathing(void) {}; + void Event_FlyZip() {}; + void Event_EndZipBeams() {}; + void Event_HealerReset() {} +#else + hhDroid(); + ~hhDroid(); + void Spawn( void ); + virtual void Think( void ); + void FlyTurn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void AdjustFlySpeed( idVec3 &vel ); +protected: + idList< idEntityPtr > beamBurstList; + idEntityPtr beamZip; + idEntityPtr staticPoint; + idEntityPtr chargeShot; + void Event_StartBurst(void); + void Event_EndBurst(void); + void Event_StartStaticBeam(void); + void Event_EndStaticBeam(void); + void Event_StartChargeShot(void); + void Event_EndChargeShot(void); + void Event_StartPathing(void); + void Event_EndPathing(void); + void Event_FlyZip(); + void Event_EndZipBeams(); + void Event_HealerReset(); + idProjectile *LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ); + virtual bool Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void Show( void ); + void FlyMove( void ); + void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + void UpdateModelTransform(); + + idVec3 savedGravity; + float chargeShotSize; + float burstLength; + float burstSpread; + float burstDuration; + float staticDuration; //how long staticbeam lasts + float staticRange; //distance to check for staticbeam points + int numBurstBeams; + idVec3 beamOffset; + float old_fly_bob_strength; + float enemyRange; + float flyDampening; + int lives; + float spinAngle; + bool bHealer; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif \ No newline at end of file diff --git a/src/Prey/ai_gasbag_simple.cpp b/src/Prey/ai_gasbag_simple.cpp new file mode 100644 index 0000000..bb38090 --- /dev/null +++ b/src/Prey/ai_gasbag_simple.cpp @@ -0,0 +1,801 @@ + + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_AcidBlast("acidBlast"); +const idEventDef EV_AcidDrip(""); +const idEventDef EV_DeathCloud(""); +const idEventDef EV_LaunchPod("launchPod"); +const idEventDef EV_NewPod("", "e"); +const idEventDef EV_SpawnBlastDebris(""); +const idEventDef EV_ChargeEnemy("chargeEnemy"); +const idEventDef EV_GrabEnemy("grabEnemy", NULL, 'f'); +const idEventDef EV_BiteEnemy("biteEnemy"); +const idEventDef EV_DirectMoveToPosition("directMoveToPosition", "v"); +const idEventDef EV_BindUnfroze("", "e"); +const idEventDef EV_GrabCheck("grabCheck", "", 'd'); +const idEventDef EV_MoveToGrabPosition("moveToGrabPosition"); +const idEventDef EV_CheckRange("checkRange", "", 'd'); +const idEventDef EV_EnemyRangeZ("enemyRangeZ", "", 'f'); + +CLASS_DECLARATION(hhMonsterAI, hhGasbagSimple) + EVENT(EV_AcidBlast, hhGasbagSimple::Event_AcidBlast) + EVENT(EV_AcidDrip, hhGasbagSimple::Event_AcidDrip) + EVENT(EV_DeathCloud, hhGasbagSimple::Event_DeathCloud) + EVENT(EV_LaunchPod, hhGasbagSimple::Event_LaunchPod) + EVENT(EV_NewPod, hhGasbagSimple::Event_NewPod) + EVENT(EV_SpawnBlastDebris, hhGasbagSimple::Event_SpawnBlastDebris) + EVENT(EV_ChargeEnemy, hhGasbagSimple::Event_ChargeEnemy) + EVENT(EV_GrabEnemy, hhGasbagSimple::Event_GrabEnemy) + EVENT(EV_BiteEnemy, hhGasbagSimple::Event_BiteEnemy) + EVENT(EV_DirectMoveToPosition, hhGasbagSimple::Event_DirectMoveToPosition) + EVENT(EV_BindUnfroze, hhGasbagSimple::Event_BindUnfroze) + EVENT(EV_GrabCheck, hhGasbagSimple::Event_GrabCheck) + EVENT(EV_MoveToGrabPosition, hhGasbagSimple::Event_MoveToGrabPosition) + EVENT(EV_CheckRange, hhGasbagSimple::Event_CheckRange) + EVENT(EV_EnemyRangeZ, hhGasbagSimple::Event_EnemyRangeZ) +END_CLASS + +static const idEventDef EV_Unfreeze( "" ); + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +// +// ~hhGasbagSimple() +// +hhGasbagSimple::~hhGasbagSimple(void) { + // Make sure to unbind the player if we've grabbed him to prevent deleting him + if (enemy.IsValid() && enemy->IsBoundTo(this) && enemy->IsType(hhPlayer::Type)) { + enemy->Unbind(); + enemy->PostEventMS(&EV_Unfreeze, 0); + } +} + +// +// Spawn() +// +void hhGasbagSimple::Spawn(void) { + Event_SetMoveType(MOVETYPE_FLY); // So we ignore gravity zones from the beginning + podOffset = spawnArgs.GetVector("pod_offset", "0 0 64"); + podRange = spawnArgs.GetFloat("podRange", "250"); + dripCount = 0; + nextWoundTime = 0; + + bindController = static_cast( gameLocal.SpawnClientObject(spawnArgs.GetString("def_bindController"), NULL) ); + HH_ASSERT( bindController.IsValid() ); + bindController->fl.networkSync = false; + + float yawLimit = spawnArgs.GetFloat("yawlimit", "180"); + const char *handName = spawnArgs.GetString("def_tractorhand"); + const char *animName = spawnArgs.GetString("boundanim"); + + bindController->SetRiderParameters(animName, handName, yawLimit, 0.0f); + bindController->Bind(this, true); + bindController->SetOrigin(spawnArgs.GetVector("grab_offset")); + bindController->fl.neverDormant = true; +} + +// +// Ticker() +// +void hhGasbagSimple::Ticker(void) { + for (int i = 0; i < podList.Num(); i++) { + if (!podList[i].IsValid()) { + podList.RemoveIndex(i); + i--; // This index is gone, so don't increment + continue; + } + } + + // Update the podcount + AI_PODCOUNT = podList.Num(); + + if (podList.Num() == 0) { + BecomeInactive(TH_TICKER); + } +} + +// +// Save() +// +void hhGasbagSimple::Save(idSaveGame *savefile) const { + savefile->WriteInt(dripCount); + + savefile->WriteInt(podList.Num()); + for (int i = 0; i < podList.Num(); i++) { + podList[i].Save(savefile); + } + bindController.Save(savefile); +} + +// +// Restore() +// +void hhGasbagSimple::Restore(idRestoreGame *savefile) { + Spawn(); + savefile->ReadInt(dripCount); + + int num; + savefile->ReadInt(num); + podList.SetNum(num); + for (int i = 0; i < num; i++) { + podList[i].Restore(savefile); + } + bindController.Restore(savefile); + + nextWoundTime = 0; +} + +// +// Event_AcidBlast() +// +void hhGasbagSimple::Event_AcidBlast(void) { + if (health <= 0) { // Don't launch a pod if we're dead + return; + } + + if (!enemy.IsValid()) { + return; + } + + idEntity *target = enemy.GetEntity(); + idVec3 targetOrigin = target->GetOrigin(); + float len, nearest = podRange; + + // See if we have a pod near the enemy. If we do, target it instead. + idEntity *entList[100]; + int num = gameLocal.EntitiesWithinRadius(targetOrigin, podRange, entList, 100); + for (int i = 0; i < num; i++) { + if (!entList[i]->IsType(hhPod::Type)) { + continue; + } + + len = (entList[i]->GetOrigin() - targetOrigin).Length(); + + // Always target the pod nearest to the enemy + if (len < nearest) { + target = entList[i]; + nearest = len; + } + } + + // Spawn a "muzzle flash"-like fx + hhFxInfo fx; + fx.SetEntity(this); + fx.RemoveWhenDone(true); + SpawnFxLocal(spawnArgs.GetString("fx_fire"), GetOrigin() + idVec3(0, 0, -400), enemy->GetPhysics()->GetGravityNormal().ToMat3(), &fx); + + // Spawn an acid blob in the player's general direction + const idDict *projDef = gameLocal.FindEntityDefDict(spawnArgs.GetString("def_projectile"), false); + LaunchProjectile(spawnArgs.GetString("acidbone", "LmbRt"), target, false, projDef); + + PostEventMS(&EV_SpawnBlastDebris, 100 + gameLocal.random.RandomInt(100)); + + dripCount = gameLocal.random.RandomInt(3) + 1; + PostEventMS(&EV_AcidDrip, 200 + gameLocal.random.RandomInt(100)); // Drip acid after firing +} + +// +// Event_SpawnBlastDebris() +// +void hhGasbagSimple::Event_SpawnBlastDebris(void) { + idDict args; + args.Clear(); + args.Set("origin", (GetPhysics()->GetOrigin() + spawnArgs.GetVector("blast_debris_offset", "0 0 128")).ToString()); + gameLocal.SpawnObject("debrisSpawner_gasbag_fire", &args); +} + +// +// Event_AcidDrip() +// +void hhGasbagSimple::Event_AcidDrip(void) { + assert(dripCount > 0); + dripCount--; + + idVec3 target = GetPhysics()->GetOrigin(); + target.z -= 128; + + idVec3 rndVec = hhUtils::RandomVector() * gameLocal.random.RandomInt(20); + const idDict *projDef = gameLocal.FindEntityDefDict(spawnArgs.GetString("def_drip_projectile", "projectile_acidspray_gasbag"), false); + LaunchProjectileAtVec(spawnArgs.GetString("acidbone", "LmbRt"), target + rndVec, false, projDef); + + if (dripCount > 0) { + PostEventMS(&EV_AcidDrip, 200 + gameLocal.random.RandomInt(100)); // Fire again in 200-300ms + } +} + +// +// Event_DeathCloud() +// +void hhGasbagSimple::Event_DeathCloud(void) { + const char *fx = spawnArgs.GetString("fx_death"); + if (!fx || !fx[0]) { + return; + } + hhFxInfo fxInfo; + + fxInfo.RemoveWhenDone(true); + BroadcastFxInfo(fx, GetOrigin(), GetAxis(), &fxInfo); +} + +// +// Event_LaunchPod() +// +void hhGasbagSimple::Event_LaunchPod() { + if (health <= 0) { // Don't launch a pod if we're dying + return; + } + + idVec3 target, origin, offset = vec3_origin; + if (enemy.IsValid()) { + // Target a random area around the enemy + origin = enemy->GetEyePosition(); + if (gameLocal.random.RandomInt(100) > 30) { // Possibly target the enemy exactly + offset.x = (gameLocal.random.RandomFloat() * 100.0f) - 50.0f; + offset.y = (gameLocal.random.RandomFloat() * 100.0f) - 50.0f; + } + } else { + // Target a random area beneath us + origin = GetOrigin(); + offset.x = gameLocal.random.RandomFloat() * 10.0f; + offset.y = gameLocal.random.RandomFloat() * 10.0f; + offset.z = -10.0f; + } + target = origin + offset; + + const idDict *projDef = gameLocal.FindEntityDefDict(spawnArgs.GetString("def_pod_projectile", "projectile_gasbag_pod"), false); + LaunchProjectileAtVec(spawnArgs.GetString("acidbone", "LmbRt"), target, false, projDef); +} + +// +// Event_NewPod +// +void hhGasbagSimple::Event_NewPod(hhPod *pod) { + HH_ASSERT(pod && pod->IsType(hhPod::Type)); + + podList.Append(idEntityPtr(pod)); + + // Update the podcount + AI_PODCOUNT = podList.Num(); + + BecomeActive(TH_TICKER); +} + +// +// Killed +// +void hhGasbagSimple::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) { + bool wasDead = (AI_DEAD != 0); + + hhMonsterAI::Killed(inflictor, attacker, damage, dir, location); + + if (wasDead) { + return; + } + + CancelEvents(&EV_AcidDrip); + CancelEvents(&EV_AcidBlast); + CancelEvents(&EV_LaunchPod); + dripCount = 0; + + // Release any grabbed enemy + idEntity *rider; + if ((rider = bindController->GetRider()) != NULL) { + bindController->Detach(); + if (rider->IsType(hhPlayer::Type)) { + rider->ProcessEvent(&EV_Unfreeze); + } + } + + int deathAnim = GetAnimator()->GetAnim("death"); + GetAnimator()->PlayAnim(ANIMCHANNEL_TORSO, deathAnim, gameLocal.time, 300.0f); + int ttl = GetAnimator()->AnimLength(deathAnim); + // Freeze + physicsObj.EnableGravity(false); + physicsObj.PutToRest(); + + SetSkinByName( spawnArgs.GetString( "skin_death" ) ); + SetDeformation(DEFORMTYPE_DEATHEFFECT, gameLocal.time + 2500, 8000); // starttime, duration + PostEventSec( &EV_StartSound, 1.5f, "snd_acid", SND_CHANNEL_ANY, 1 ); + + // Spawn gas cloud + PostEventMS(&EV_DeathCloud, ttl); + PostEventMS(&EV_Dispose, 0); +} + +// +// LaunchProjectile +// +idProjectile *hhGasbagSimple::LaunchProjectileAtVec( const char *jointname, const idVec3 &target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { //HUMANHEAD mdc - added desiredProjectileDef for supporting multiple projs. + idVec3 muzzle; + idVec3 dir; + idVec3 start; + trace_t tr; + idBounds projBounds; + float distance; + const idClipModel *projClip; + float attack_accuracy; + float attack_cone; + float projectile_spread; + float diff; + float angle; + float spin; + idAngles ang; + int num_projectiles; + int i; + idMat3 axis; + idVec3 tmp; + idProjectile *lastProjectile; + + //HUMANHEAD mdc - added to support multiple projectiles + if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile + int projIndex = FindProjectileInfo( desiredProjectileDef ); + if( projIndex >= 0 ) { + SetCurrentProjectile( projIndex ); + } + } + //HUMANHEAD END + + + if ( !projectileDef ) { + gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); + return NULL; + } + + attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); + attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); + projectile_spread = spawnArgs.GetFloat( "projectile_spread", "0" ); + num_projectiles = spawnArgs.GetInt( "num_projectiles", "1" ); + + GetMuzzle( jointname, muzzle, axis ); + + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, axis[ 0 ] ); + } + + lastProjectile = projectile.GetEntity(); + + tmp = target - muzzle; + tmp.Normalize(); + axis = tmp.ToMat3(); + + // rotate it because the cone points up by default + tmp = axis[2]; + axis[2] = axis[0]; + axis[0] = -tmp; + + // make sure the projectile starts inside the monster bounding box + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + projClip = lastProjectile->GetPhysics()->GetClipModel(); + projBounds = projClip->GetBounds().Rotate( axis ); + + // check if the owner bounds is bigger than the projectile bounds + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { + start = muzzle + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); + muzzle = tr.endpos; + + // set aiming direction + //GetAimDir( muzzle, target, this, dir ); + dir = target - muzzle; + ang = dir.ToAngles(); + + // adjust his aim so it's not perfect. uses sine based movement so the tracers appear less random in their spread. + float t = MS2SEC( gameLocal.time + entityNumber * 497 ); + ang.pitch += idMath::Sin16( t * 5.1 ) * attack_accuracy; + ang.yaw += idMath::Sin16( t * 6.7 ) * attack_accuracy; + + if ( clampToAttackCone ) { + // clamp the attack direction to be within monster's attack cone so he doesn't do + // things like throw the missile backwards if you're behind him + diff = idMath::AngleDelta( ang.yaw, current_yaw ); + if ( diff > attack_cone ) { + ang.yaw = current_yaw + attack_cone; + } else if ( diff < -attack_cone ) { + ang.yaw = current_yaw - attack_cone; + } + } + + axis = ang.ToMat3(); + + float spreadRad = DEG2RAD( projectile_spread ); + for( i = 0; i < num_projectiles; i++ ) { + // spread the projectiles out + angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); + spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); + dir = axis[ 0 ] + axis[ 2 ] * ( angle * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir.Normalize(); + + // launch the projectile + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, dir ); + } + lastProjectile = projectile.GetEntity(); + lastProjectile->Launch( muzzle, dir, vec3_origin ); + projectile = NULL; + } + + TriggerWeaponEffects( muzzle, axis ); + + lastAttackTime = gameLocal.time; + +//HUMANHEAD mdc - added to support multiple projectiles + projectile = NULL; + SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side +//HUMANHEAD END + + return lastProjectile; +} + +// +// LaunchProjectile +// +idProjectile *hhGasbagSimple::LaunchProjectileAtVec( const idVec3 &startOrigin, const idMat3 &startAxis, const idVec3 &target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { //HUMANHEAD mdc - added desiredProjectileDef for supporting multiple projs. + idVec3 muzzle; + idVec3 dir; + idVec3 start; + trace_t tr; + idBounds projBounds; + float distance; + const idClipModel *projClip; + //float attack_accuracy; + //float attack_cone; + float projectile_spread; + float angle; + float spin; + idAngles ang; + int num_projectiles; + int i; + idMat3 axis; + idVec3 tmp; + idProjectile *lastProjectile; + + //HUMANHEAD mdc - added to support multiple projectiles + if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile + int projIndex = FindProjectileInfo( desiredProjectileDef ); + if( projIndex >= 0 ) { + SetCurrentProjectile( projIndex ); + } + } + //HUMANHEAD END + + + if ( !projectileDef ) { + gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); + return NULL; + } + + //attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); + //attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); + projectile_spread = spawnArgs.GetFloat( "projectile_spread", "0" ); + num_projectiles = spawnArgs.GetInt( "num_projectiles", "1" ); + + muzzle = startOrigin; + axis = startAxis; + + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, axis[ 0 ] ); + } + + lastProjectile = projectile.GetEntity(); + + // make sure the projectile starts inside the monster bounding box + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + projClip = lastProjectile->GetPhysics()->GetClipModel(); + projBounds = projClip->GetBounds().Rotate( axis ); + + // check if the owner bounds is bigger than the projectile bounds + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { + start = muzzle + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); + muzzle = tr.endpos; + + float spreadRad = DEG2RAD( projectile_spread ); + for( i = 0; i < num_projectiles; i++ ) { + // spread the projectiles out + angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); + spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); + dir = axis[ 0 ] + axis[ 2 ] * ( angle * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir.Normalize(); + + // launch the projectile + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, dir ); + } + lastProjectile = projectile.GetEntity(); + lastProjectile->Launch( muzzle, dir, vec3_origin ); + projectile = NULL; + } + + TriggerWeaponEffects( muzzle, axis ); + + lastAttackTime = gameLocal.time; + +//HUMANHEAD mdc - added to support multiple projectiles + projectile = NULL; + SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side +//HUMANHEAD END + + return lastProjectile; +} + +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhGasbagSimple::LinkScriptVariables(void) { + hhMonsterAI::LinkScriptVariables(); + + LinkScriptVariable(AI_PODCOUNT); + LinkScriptVariable(AI_CHARGEDONE); + LinkScriptVariable(AI_SWOOP); + LinkScriptVariable(AI_DODGEDAMAGE); +} + +// +// Event_GrabEnemy() +// +void hhGasbagSimple::Event_GrabEnemy(void) { + idThread::ReturnFloat( GrabEnemy() ); +} + +// +// GrabEnemy() +// +bool hhGasbagSimple::GrabEnemy(void) { + if (!enemy.IsValid() || enemy->IsBound() || bindController->GetRider()) { + return false; + } + + // Don't grab Talon + if (enemy->IsType(hhTalon::Type)) { + return false; + } + + if (enemy->IsType(hhPlayer::Type)) { + hhPlayer *player = reinterpret_cast (enemy.GetEntity()); + if (player->IsSpiritOrDeathwalking() || player->InGravityZone()) { // Refuse to grab spirit players, or players affected by a gravity zone + return false; + } + player->Freeze(); + } + + bindController->Attach(enemy.GetEntity()); + enemy->SetOrigin(vec3_origin); + return true; +} + +// +// Event_BiteEnemy() +// +void hhGasbagSimple::Event_BiteEnemy(void) { + if (!enemy.IsValid() || bindController->GetRider() != enemy.GetEntity()) { + return; + } + + if (enemy->IsType(hhSpiritProxy::Type)) { + float diff = gameLocal.GetTime() - reinterpret_cast (enemy.GetEntity())->GetActivationTime(); + if (diff < 1150.0f) { + // Don't allow the spirit proxy to ignore damage + scriptThread->WaitMS(diff + 250.0f); + CancelEvents(&EV_BiteEnemy); + PostEventMS(&EV_BiteEnemy, diff + 100.0f); + return; + } + } + + // Direct damage + bindController->Detach(); + enemy->Damage(this, this, vec3_origin, spawnArgs.GetString("def_damage_bite"), 1.0f, INVALID_JOINT); +} + +// +// Damage() +// +void hhGasbagSimple::Damage(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location) { + int curHealth = health; + idEntity *enemyEnt = enemy.GetEntity(); + hhMonsterAI::Damage(inflictor, attacker, dir, damageDefName, damageScale, location); + + if (health < curHealth) { + if (gameLocal.time > nextWoundTime && location != INVALID_JOINT) { + hhFxInfo fx; + fx.SetEntity(this); + fx.RemoveWhenDone(true); + SpawnFxLocal(spawnArgs.GetString("fx_damage"), inflictor->GetOrigin(), dir.ToMat3(), &fx); + + nextWoundTime = gameLocal.time + 250; + } + + // Only release enemy if we actually took damage + idEntity *rider; + if ((rider = bindController->GetRider()) != NULL) { + bindController->Detach(); + if (rider->IsType(hhPlayer::Type)) { + rider->PostEventMS(&EV_Unfreeze, 0); + } + } + + AI_DODGEDAMAGE = AI_DODGEDAMAGE + (curHealth - health); + } +} + +// +// Event_DirectMoveToPosition() +// +void hhGasbagSimple::Event_DirectMoveToPosition(const idVec3 &pos) { + StopMove(MOVE_STATUS_DONE); + DirectMoveToPosition(pos); +} + +// +// Event_ChargeEnemy +// +void hhGasbagSimple::Event_ChargeEnemy(void) { + AI_CHARGEDONE = true; + StopMove(MOVE_STATUS_DEST_NOT_FOUND); + if (!enemy.IsValid() || enemy->IsBound()) { + return; + } + + idVec3 enemyOrg; + + fly_offset = 0.0f; + + // position destination so that we're in the enemy's view + enemyOrg = enemy->GetEyePosition(); + enemyOrg -= enemy->GetPhysics()->GetGravityNormal() * fly_offset; + + DirectMoveToPosition(enemyOrg); + HH_ASSERT(move.bEnemyBlocks == false); +} + +void hhGasbagSimple::Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ) { + hhMonsterAI::Event_EnemyIsSpirit(player, proxy); + if (bindController->GetRider() == player) { + bindController->Detach(); + player->Unfreeze(); + GrabEnemy(); + } +} + +void hhGasbagSimple::Event_EnemyIsPhysical( hhPlayer *player, hhSpiritProxy *proxy ) { + hhMonsterAI::Event_EnemyIsPhysical(player, proxy); + if (bindController->GetRider() == player) { + player->Freeze(); + } +} + +// +// Event_BindUnfroze +// +void hhGasbagSimple::Event_BindUnfroze(idEntity *unfrozenBind) { + if (!enemy.IsValid() || unfrozenBind != enemy.GetEntity() || bindController->GetRider() != enemy.GetEntity()) { + gameLocal.Warning("Gasbag received BindUnfroze event but enemy is not bound to it.\n"); + return; + } + + bindController->Detach(); +} + +// +// FlyTurn +// +void hhGasbagSimple::FlyTurn(void) { + if (!AI_SWOOP && (AI_ENEMY_VISIBLE || move.moveCommand == MOVE_FACE_ENEMY)) { + TurnToward( lastVisibleEnemyPos ); + } else if ((move.moveCommand == MOVE_FACE_ENTITY) && move.goalEntity.GetEntity()) { + TurnToward(move.goalEntity.GetEntity()->GetPhysics()->GetOrigin()); + } else if (move.speed > 0.1f) { + const idVec3 &vel = physicsObj.GetLinearVelocity(); + if (vel.ToVec2().LengthSqr() > 0.1f) { + TurnToward(vel.ToYaw()); + } + } + Turn(); +} + +// +// Event_GrabCheck() +// +void hhGasbagSimple::Event_GrabCheck(void) { + idThread::ReturnInt(enemy.IsValid() && enemy->IsBoundTo(this)); +} + + +void hhGasbagSimple::Event_MoveToGrabPosition(void) { + StopMove(MOVE_STATUS_DEST_NOT_FOUND); + if (!enemy.IsValid() || enemy->IsBound()) { + return; + } + + fly_offset = 0.0f; + + // position destination so that we're right on the enemy when we grab him + DirectMoveToPosition(enemy->GetOrigin() - spawnArgs.GetVector( "grab_offset" )); + HH_ASSERT(move.bEnemyBlocks == false); +} + +void hhGasbagSimple::SetEnemy(idActor *newEnemy) { + idEntity *rider; + if ((rider = bindController->GetRider())) { // This is usually caused by Talon + if (rider->IsType(hhPlayer::Type)) { + rider->PostEventMS(&EV_Unfreeze, 0); + } + bindController->Detach(); + } + + hhMonsterAI::SetEnemy(newEnemy); +} + +void hhGasbagSimple::Event_CheckRange(void) { + if (!enemy.IsValid()) { + idThread::ReturnInt(0); + return; + } + + int retval; + float dist = ( enemy->GetPhysics()->GetOrigin() - GetPhysics()->GetOrigin() ).Length(); + + if (enemy->IsType(hhPlayer::Type)) { + float min = spawnArgs.GetFloat( "dda_range_min" ); + float max = spawnArgs.GetFloat( "dda_range_max" ); + float player_dist = min + (max - min) * gameLocal.GetDDAValue(); + + retval = (int) (dist <= player_dist); + } else { + retval = (int) (dist <= 100.0f); + } + + idThread::ReturnInt(retval); +} + +void hhGasbagSimple::Gib( const idVec3 &dir, const char *damageDefName ) { + // Bypass idActor::Gib() + idAFEntity_Gibbable::Gib( dir, damageDefName ); +} + +int hhGasbagSimple::ReactionTo( const idEntity *ent ) { + //gasbags skip the spirit stuff in hhMonsterAI::ReactionTo() + if ( bNoCombat ) { + return ATTACK_IGNORE; + } + const idActor *actor = static_cast( ent ); + if( actor && actor->IsType(hhDeathProxy::Type) ) { + return ATTACK_IGNORE; + } + if ( ent->IsType( hhMonsterAI::Type ) ) { + const hhMonsterAI *entAI = static_cast( ent ); + if ( entAI && entAI->bNeverTarget ) { + return ATTACK_IGNORE; + } + } + + return idAI::ReactionTo( ent ); +} + +void hhGasbagSimple::Event_EnemyRangeZ( void ) { + idThread::ReturnFloat( lastVisibleEnemyPos.z - GetOrigin().z ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_gasbag_simple.h b/src/Prey/ai_gasbag_simple.h new file mode 100644 index 0000000..6c5ffeb --- /dev/null +++ b/src/Prey/ai_gasbag_simple.h @@ -0,0 +1,90 @@ +#ifndef __PREY_AI_GASBAG_SIMPLE_H__ +#define __PREY_AI_GASBAG_SIMPLE_H__ + +extern const idEventDef EV_NewPod; + +class hhPod; + +class hhGasbagSimple : public hhMonsterAI { +public: + CLASS_PROTOTYPE(hhGasbagSimple); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_AcidBlast(void) {}; + void Event_AcidDrip(void) {}; + void Event_DeathCloud(void) {}; + void Event_NewPod(hhPod *pod) {}; + void Event_LaunchPod(void) {}; + void Event_SpawnBlastDebris(void) {}; + void Event_ChargeEnemy(void) {}; + void Event_GrabEnemy(void) {}; + void Event_BiteEnemy(void) {}; + void Event_DirectMoveToPosition(const idVec3 &pos) {}; + void Event_BindUnfroze(idEntity *unfrozenBind) {}; + virtual void Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ) {}; + virtual void Event_EnemyIsPhysical( hhPlayer *player, hhSpiritProxy *proxy ) {}; + void Event_GrabCheck(void) {}; + void Event_MoveToGrabPosition(void) {}; + void Event_CheckRange(void) {}; + void Event_EnemyRangeZ(void) {}; +#else + virtual ~hhGasbagSimple(void); + + void Spawn(void); + void Save(idSaveGame *savefile) const; + void Restore(idRestoreGame *savefile); + + virtual void FlyTurn(void); + + virtual void Gib(const idVec3 &dir, const char *damageDefName); + int ReactionTo( const idEntity *ent ); + +protected: + void Event_AcidBlast(void); + void Event_AcidDrip(void); + void Event_DeathCloud(void); + void Event_NewPod(hhPod *pod); + void Event_LaunchPod(void); + void Event_SpawnBlastDebris(void); + void Event_ChargeEnemy(void); + void Event_GrabEnemy(void); + void Event_BiteEnemy(void); + void Event_DirectMoveToPosition(const idVec3 &pos); + void Event_BindUnfroze(idEntity *unfrozenBind); + virtual void Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ); + virtual void Event_EnemyIsPhysical( hhPlayer *player, hhSpiritProxy *proxy ); + void Event_GrabCheck(void); + void Event_MoveToGrabPosition(void); + void Event_CheckRange(void); + void Event_EnemyRangeZ(void); + + bool GrabEnemy(void); + + virtual void LinkScriptVariables(void); + + virtual void Ticker(void); + virtual idProjectile *LaunchProjectileAtVec(const char *jointname, const idVec3 &target, bool clampToAttackCone, const idDict* desiredProjectileDef); + virtual idProjectile *LaunchProjectileAtVec(const idVec3 &startOrigin, const idMat3 &startAxis, const idVec3 &target, bool clampToAttackCone, const idDict* desiredProjectileDef); + virtual void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location); + virtual void Damage(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location); + virtual void SetEnemy(idActor *newEnemy); + + idVec3 podOffset; + float podRange; + int dripCount; + + idList< idEntityPtr > podList; + + idScriptInt AI_PODCOUNT; + idScriptBool AI_CHARGEDONE; + idScriptBool AI_SWOOP; + idScriptInt AI_DODGEDAMAGE; + + int nextWoundTime; + + idEntityPtr bindController; // Bind controller for tractor beam +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif + diff --git a/src/Prey/ai_harvester_simple.cpp b/src/Prey/ai_harvester_simple.cpp new file mode 100644 index 0000000..941d4de --- /dev/null +++ b/src/Prey/ai_harvester_simple.cpp @@ -0,0 +1,838 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef MA_AntiProjectileAttack("", "e"); +const idEventDef MA_StartPreDeath("" ); +const idEventDef MA_HandlePassageway("", NULL); +const idEventDef MA_EnterPassageway("", "e",NULL); +const idEventDef MA_ExitPassageway("", "e",NULL); +const idEventDef MA_UseThisPassageway("useThisPassageway", "ed",NULL); +const idEventDef MA_GibOnDeath("gibOnDeath", "d"); + +CLASS_DECLARATION( hhMonsterAI, hhHarvesterSimple ) + EVENT( MA_OnProjectileLaunch, hhHarvesterSimple::Event_OnProjectileLaunch ) + EVENT( MA_AntiProjectileAttack, hhHarvesterSimple::Event_AntiProjectileAttack ) + EVENT( MA_StartPreDeath, hhHarvesterSimple::Event_StartPreDeath ) + EVENT( MA_HandlePassageway, hhHarvesterSimple::Event_HandlePassageway ) + EVENT( MA_EnterPassageway, hhHarvesterSimple::Event_EnterPassageway ) + EVENT( MA_ExitPassageway, hhHarvesterSimple::Event_ExitPassageway ) + EVENT( MA_UseThisPassageway, hhHarvesterSimple::Event_UseThisPassageway ) + EVENT( EV_Broadcast_AppendFxToList, hhHarvesterSimple::Event_AppendFxToList ) + EVENT( MA_GibOnDeath, hhHarvesterSimple::Event_GibOnDeath ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +// +// Spawn() +// +void hhHarvesterSimple::Spawn(void) { + allowPreDeath = true; + lastAntiProjectileAttack = 0; + lastPassagewayTime = 0; + passageCount = spawnArgs.GetInt("passage_count", "0"); // Used for passing to and from torso + bSmokes = spawnArgs.GetBool("smokes", "0"); + bGibOnDeath = false; +} + +// +// Event_OnProjectileLaunch() +// +void hhHarvesterSimple::Event_OnProjectileLaunch(hhProjectile *proj) { + + // Can't launch again yet + float min = spawnArgs.GetFloat( "dda_delay_min" ); + float max = spawnArgs.GetFloat( "dda_delay_max" ); + float delay = min + (max - min) * (1.0f - gameLocal.GetDDAValue()); + + if(gameLocal.GetTime() - lastAntiProjectileAttack < delay) + return; + + // The person who launched this projectile wasn't someone to worry about + if(proj->GetOwner() && !(ReactionTo(proj->GetOwner()) & (ATTACK_ON_SIGHT | ATTACK_ON_DAMAGE))) + return; + + // TODO: more intelligent checks for if we should launch the anti-projectile chaff + idVec3 fw = viewAxis[0]; + idVec3 projFw = proj->GetAxis()[0]; + if(proj->GetOwner()) + projFw = proj->GetOwner()->GetAxis()[0]; + float dot = fw * projFw; + if(dot > -.7f) + return; + + ProcessEvent(&MA_AntiProjectileAttack, proj); +} + + +// +// Event_AntiProjectileAttack() +// +void hhHarvesterSimple::Event_AntiProjectileAttack(hhProjectile *proj) { + + if(!spawnArgs.GetBool("block_projectiles", "1")) { + return; + } + + const idDict *projectileDef = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_chaff") ); + if ( !projectileDef ) { + gameLocal.Error( "Unknown def_chaff: %s\n", spawnArgs.GetString("def_chaff") ); + } + hhProjectile* projectile = NULL; + + //bjk: new parms + int numProj = spawnArgs.GetInt("shieldNumProj", "10"); + float spread = DEG2RAD( spawnArgs.GetFloat("shieldSpread", "10") ); + float yaw = DEG2RAD( spawnArgs.GetFloat("shieldYawSpread", "10") ); + + //StartSound("snd_fire_chaff", SND_CHANNEL_ANY, 0, true, NULL); + torsoAnim.PlayAnim( GetAnimator()->GetAnim( "antiprojectile_attack" ) ); + + idVec3 dir = proj->GetOrigin() - GetOrigin(); + dir.Normalize(); + dir = dir+idVec3(0.f, 0.f, -.3f); // bjk: bias towards ground + dir.Normalize(); + idMat3 muzzleAxis = dir.ToMat3(); + // todo: read this in from a bone + idVec3 launchPos = GetOrigin() + idVec3(0.0f, 0.0f, 64.0f); + + for(int i=0; iCreate(this, launchPos, dir); + projectile->Launch(launchPos, dir, idVec3(0.0f, 0.0f, 0.0f)); + } + + lastAntiProjectileAttack = gameLocal.GetTime(); +} + +bool hhHarvesterSimple::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + int preDeathThresh = spawnArgs.GetInt("predeath_thresh", "-1"); + if(allowPreDeath && !bGibOnDeath && preDeathThresh > 0 && health < preDeathThresh) { + allowPreDeath = false; // We've tried, cannot try anymore + float preDeathFreq = spawnArgs.GetFloat("predeath_freq", "1"); + if(gameLocal.random.RandomFloat() < preDeathFreq) { + ProcessEvent(&MA_StartPreDeath); + } + } + + if ( bSmokes ) { + if ( !fxSmoke[0].IsValid() && health <= AI_PASSAGEWAY_HEALTH ) { + SpawnSmoke(); + } else if (fxSmoke[0].IsValid() && health > AI_PASSAGEWAY_HEALTH) { + ClearSmoke(); + } + } + + return( idAI::Pain(inflictor, attacker, damage, dir, location) ); +} + +void hhHarvesterSimple::Event_StartPreDeath(void) { + int i; + + // No torso? Just kill myself ..... + if(!spawnArgs.GetString("def_torso")) { + ProcessEvent(&AI_Kill); + return; + } + + // Spawn death fx, if any + const char *fx = spawnArgs.GetString("fx_death"); + if (fx && fx[0]) { + hhFxInfo fxInfo; + + fxInfo.RemoveWhenDone(true); + BroadcastFxInfo(fx, GetOrigin(), GetAxis(), &fxInfo); + } + + // do blood splats + float size = spawnArgs.GetFloat("decal_torso_pop_size","96"); + + // copy my target keys to the keys that will spawn the torso + idDict dict; + const idDict *torsoDict = gameLocal.FindEntityDefDict(spawnArgs.GetString("def_torso")); + if ( !torsoDict ) { + gameLocal.Error("Unknown def_torso: %s\n", spawnArgs.GetString("def_torso")); + } + dict.Copy(*torsoDict); + for(i=0;i 0) { + sprintf(key, "target%i", i); + } + + dict.Set((const char*)key, (const char*)targets[i].GetEntity()->name); + } + + // Passageway variables + dict.SetInt("passage_health", AI_PASSAGEWAY_HEALTH); + dict.SetInt("passage_count", passageCount); + dict.SetVector("origin", GetOrigin()); + dict.SetMatrix("rotation", GetAxis()); + + // remove my target keys (so they don't get fired) + targets.Clear(); + + // Spawn the torso + idEntity *e; + hhMonsterAI *aiTorso; + if(!gameLocal.SpawnEntityDef(dict, &e)) + gameLocal.Error("Failed to spawn ai torso."); + HH_ASSERT(e && e->IsType(hhMonsterAI::Type)); + aiTorso = static_cast(e); + HH_ASSERT(aiTorso != NULL); + + // Throw gibs + idStr debrisDef = spawnArgs.GetString("def_gibDebrisSpawnerPreDeath"); + if(debrisDef.Length() > 0) { + hhUtils::SpawnDebrisMass(debrisDef.c_str(), this, 1.0f); + } + + health = 0; + + // remove myself + Hide(); + PostEventMS(&EV_Remove, 3000); +} + +void hhHarvesterSimple::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if ( attacker == this ) { + return; + } + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( !AI_DEAD ) { + if ( damageDef && damageDef->GetBool( "ice" ) && spawnArgs.GetBool( "can_freeze", "0" ) ) { + spawnArgs.Set( "bAlwaysGib", "0" ); + allowPreDeath = false; + } else { + allowPreDeath = true; + } + if ( damageDef && damageDef->GetBool( "no_special_death" ) ) { + allowPreDeath = false; + } + } + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} + +int hhHarvesterSimple::EvaluateReaction( const hhReaction *react ) { + // Let the script limit the distance of Climb reactions we consider + if ( react->desc->effect == hhReactionDesc::Effect_Climb && AI_CLIMB_RANGE >= 0.0f && ( react->causeEntity->GetOrigin() - GetOrigin()).LengthSqr() > AI_CLIMB_RANGE ) { + return 0; + } + + // If we're supposed to use a specific passageway, only accept it and reject the others + if ( nextPassageway.IsValid() && react->desc->effect == hhReactionDesc::Effect_Passageway ) { + if (react->causeEntity.GetEntity() == nextPassageway.GetEntity()) { + return 100; + } else { + return 0; + } + } + + int rank = hhMonsterAI::EvaluateReaction( react ); + if ( react->desc->effect == hhReactionDesc::Effect_Passageway && + react->causeEntity.GetEntity() == lastPassageway.GetEntity() ) { + if ( lastPassagewayTime + 10000 > gameLocal.time ) { + if (rank > 10) { + // Discourage entering the same passageway we just left + return 10; + } + } else { + // Ignore previous passageway for 10 seconds + return 0; + } + } + return rank; +} + +#define CheckPassageFreqInitial 6000 +void hhHarvesterSimple::Event_EnterPassageway(hhAIPassageway *pn) { + CancelEvents( &MA_OnProjectileLaunch ); // Clear any anti-projectile attacks + CancelEvents( &MA_AntiProjectileAttack ); // Clear any anti-projectile attacks + if (nextPassageway.IsValid()) { + if (nextPassageway.GetEntity() != pn) { + gameLocal.Warning("hhAIPassageway entered was not the specified one!\n"); + } + nextPassageway.Clear(); + } + + if(currPassageway != NULL) { + gameLocal.Error("%s tried to ENTER a passageway but already had a passageway!", (const char*)name); + } + + HH_ASSERT(currPassageway == NULL); + Hide(); // Should already be hidden but just to make sure + + // Only consider passageCount if damaged (ignores scripted passage sequences) or if it's the torso + if (health < spawnHealth || !idStr::Icmp(spawnArgs.GetString("classname"), "monster_harvester_torso") ) { + passageCount++; + if (passageCount > 1) { + //TODO make this affected by the DDA system + AI_PASSAGEWAY_HEALTH = AI_PASSAGEWAY_HEALTH * 0.75f; // Reduce passageway seeking health level to 3/4 of it's original value + } + } + + GetAnimator()->ClearAllAnims(gameLocal.GetTime(), 0); + Event_AnimState(ANIMCHANNEL_LEGS, "Legs_Idle", (int)0); + Event_AnimState(ANIMCHANNEL_TORSO, "Torso_Idle", (int)0); + torsoAnim.UpdateState(); + legsAnim.UpdateState(); + + StartSound("snd_inside_passage", SND_CHANNEL_VOICE, SSF_LOOPING, true, NULL); + + currPassageway = pn; + if(GetPhysics()) + GetPhysics()->SetContents(0); + + // Does this passage way give us health? + int h = 0; + pn->spawnArgs.GetInt("give_health", "0", h); + health += h; + health = idMath::ClampInt(0, spawnArgs.GetInt("health"), health); + + + // Should we transform to another monster when entering? + idStr transTo; + if(spawnArgs.GetString("passage_transform_to", "", transTo)) { + const idDict *def = gameLocal.FindEntityDefDict( transTo ); + if(!def) { + gameLocal.Error("Unknown def : %s", (const char*)transTo); + } + + // copy my target keys to the keys of the transformed-to entity + idDict dict; + dict.Copy(*def); + for(int i=0;i 0) + sprintf(key, "target%i", i); + + dict.Set((const char*)key, (const char*)targets[i].GetEntity()->name); + } + + // Passageway variables + dict.SetInt("passage_health", AI_PASSAGEWAY_HEALTH); + dict.SetInt("passage_count", passageCount); + + idEntity *e = NULL; + gameLocal.SpawnEntityDef(dict, &e); + HH_ASSERT( e->IsType( hhHarvesterSimple::Type ) ); + hhHarvesterSimple *ai = static_cast(e); + ai->SetOrigin(GetOrigin()); + ai->SetAxis(GetAxis()); + ai->Event_EnterPassageway(pn); + targets.Clear(); + PostEventSec(&EV_Remove, 0.1f); + return; + } + + + PostEventMS(&MA_HandlePassageway, CheckPassageFreqInitial); +} + +void hhHarvesterSimple::Event_ExitPassageway(hhAIPassageway *pn) { + if(currPassageway == NULL) { + gameLocal.Error("%s tried to EXIT a passageway but did NOT have a curr passageway!", (const char*)name); + } + + HH_ASSERT(currPassageway != NULL); + + // Store this passageway so we don't jump right back into it. + lastPassageway = idEntityPtr (pn); + lastPassagewayTime = gameLocal.time; + + if(GetPhysics()) + GetPhysics()->SetContents(CONTENTS_BODY); + currPassageway = NULL; + AI_ACTIVATED = true; // Sometimes this is NOT true?!?! + StopSound(SND_CHANNEL_VOICE, true); + + // Teleport to new pos + idAngles angs = pn->GetAxis().ToAngles(); + idVec3 pos = pn->GetExitPos(); + Teleport( pos, angs, pn ); + Show(); + + current_yaw = angs.yaw; + ideal_yaw = angs.yaw; + turnVel = 0.0f; + HH_ASSERT(FacingIdeal()); + + // Exit anim to play? + idStr exitAnim = pn->spawnArgs.GetString("exit_anim"); + if(exitAnim.Length() && GetAnimator()->HasAnim(exitAnim)) { + GetAnimator()->ClearAllAnims(gameLocal.GetTime(), 0); + torsoAnim.UpdateState(); + legsAnim.UpdateState(); + torsoAnim.PlayAnim( GetAnimator()->GetAnim( exitAnim ) ); + legsAnim.PlayAnim( GetAnimator()->GetAnim( exitAnim ) ); + } +} + +// +// Event_HandlePassageway() +// +#define MaxExits 32 +#define CheckPassageFreq 3000 +void hhHarvesterSimple::Event_HandlePassageway(void) { + + if(currPassageway == NULL) { + gameLocal.Error("%s tried to handle a passageway but did NOT have a passageway!", (const char*)name); + } + + HH_ASSERT(currPassageway != NULL); + hhAIPassageway *exitNode = NULL; + + // CATCH 'went inside hole - never came out' bug + if(currPassageway.GetEntity()->lastEntered == this && gameLocal.GetTime() - currPassageway.GetEntity()->timeLastEntered > 30000) { + gameLocal.Warning("%s has been stuck in a passageway for >30 secs, killing it off.", (const char*)name); + fl.takedamage = true; + // Use hhMonsterAI::Damage because our Damage returns if we're the attacker + hhMonsterAI::Damage(this, this, vec3_origin, "damage_gib", 1.0f, INVALID_JOINT); + return; + } + + // No other way out - gotta go out the way we came in + if(currPassageway->targets.Num() <= 0) { + exitNode = currPassageway.GetEntity(); + } + // We can go out another way + else { + hhAIPassageway *possibleExits[MaxExits]; + hhAIPassageway *tmp; + int count = 0; + + bool isRandom; + if (gameLocal.GetTime() - currPassageway.GetEntity()->timeLastEntered > SEC2MS(10)) { + // Try to prevent us from being stuck after 10 seconds + isRandom = true; + } else { + // Choose possibleExits[] randomly if we have no enemy or based on chance. Otherwise choose based on line-of-sight to the player + isRandom = !enemy.IsValid() || gameLocal.random.RandomInt(100) > 50; + } + trace_t tr; + idVec3 toPos; + if (!isRandom) { + toPos = enemy->GetOrigin(); + } + + // We've tried once, now we can use our entrance as an exit + if(gameLocal.GetTime() - currPassageway.GetEntity()->timeLastEntered > CheckPassageFreqInitial + 1000) { + count = 1; + possibleExits[0] = currPassageway.GetEntity(); // We can always go out the same way we came in + } + for(int i=0;itargets.Num();i++) { + if(currPassageway->targets[i] != NULL && currPassageway->targets[i].GetEntity()->IsType(hhAIPassageway::Type)) { + if (isRandom) { + possibleExits[count++] = static_cast(currPassageway->targets[i].GetEntity()); + } else { + tmp = static_cast(currPassageway->targets[i].GetEntity()); + // Choose based on visibility to our enemy + gameLocal.clip.TracePoint( tr, tmp->GetOrigin(), toPos, MASK_SHOT_BOUNDINGBOX, tmp ); + if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == enemy.GetEntity() ) ) { + lastVisibleEnemyPos = toPos; // Can now see enemy + possibleExits[count++] = tmp; + } + } + } + + // We've got enough + if(count >= MaxExits) + break; + } + + if(count == 0) { + exitNode = currPassageway.GetEntity(); + } else if ( enemy.IsValid() && gameLocal.random.RandomInt(100) > 70 ) { + // Find the one closest to the last known enemy pos + if (!exitNode) { + float bestDistance = -1.0f, distSq; + for (int i = 0; i < count; i++) { + distSq = ( possibleExits[i]->GetOrigin() - lastVisibleEnemyPos ).LengthSqr(); + if ( bestDistance == -1.0f || distSq < bestDistance ) { + exitNode = possibleExits[i]; + bestDistance = distSq; + } + } + } + } else { + // Pick a random one + int randExit = gameLocal.random.RandomInt(count); // TODO: Randomness isn't very random! Fix! + HH_ASSERT(randExit >= 0 && randExit < count); + exitNode = possibleExits[randExit]; + } + } + + if(!exitNode) + exitNode = currPassageway.GetEntity(); + + // The one we picked is disabled, so go out where we came in + // TODO: make this time based, try a few more times before going back out + if(!exitNode->IsPassagewayEnabled()) { + exitNode = currPassageway.GetEntity(); + } + + // Special case: The passageway is never meant to be exited from, use it's specified exit point instead + const char *altPassage = exitNode->spawnArgs.GetString("force_passageway", ""); + if (altPassage && altPassage[0]) { + idEntity *node = gameLocal.FindEntity(altPassage); + if (node && node->IsType(hhAIPassageway::Type)) { + exitNode = reinterpret_cast (node); + } + } + + // Make sure no one is blocking where we want to go out + idBounds myBnds = GetPhysics()->GetBounds(); + myBnds.Expand(12.0f); + myBnds.TranslateSelf(exitNode->GetExitPos()); + idEntity *ents[MAX_GENTITIES]; + int count = gameLocal.clip.EntitiesTouchingBounds(myBnds, -1, ents, MAX_GENTITIES); + for(int i=0;iIsHidden() && (ents[i]->IsType(idActor::Type) || ents[i]->IsType(hhMoveable::Type))) { + // Chunk moveables + if ( ents[i]->IsType(idMoveable::Type) ) { + ents[i]->Damage( this, this, vec3_origin, "damage_gib", 1.0f, INVALID_JOINT ); + ents[i]->SquishedByDoor(this); + continue; + } + gameLocal.Warning("Passageway exit blocked.\n"); + PostEventMS(&MA_HandlePassageway, CheckPassageFreq); + if(ai_debugBrain.GetInteger() != 0) { + gameRenderWorld->DebugBounds(colorYellow, myBnds, vec3_origin, 2000); + } + return; + } + } + + // Else we can just exit now - its safe + exitNode->ProcessEvent(&EV_Activate, this); +} + +#define LinkScriptVariable( name ) name.LinkTo(scriptObject, #name) +void hhHarvesterSimple::LinkScriptVariables() { + hhMonsterAI::LinkScriptVariables(); + LinkScriptVariable(AI_CLIMB_RANGE); + LinkScriptVariable(AI_PASSAGEWAY_HEALTH); +} + +void hhHarvesterSimple::Event_UseThisPassageway(hhAIPassageway *pn, bool force) { + nextPassageway.Assign(pn); + + if (force) { // Snap to position + Event_FindReaction("passageway"); + hhReaction *reaction = targetReaction.GetReaction(); + if (!reaction) { + gameLocal.Warning("useThisPassageway could not find a valid reaction!\n"); + } else { + idVec3 tp = GetTouchPos( reaction->causeEntity.GetEntity(), reaction->desc ); + SetOrigin(tp); + } + } +} + +/* +===================== +hhHarvesterSimple::Save +===================== +*/ +void hhHarvesterSimple::Save( idSaveGame *savefile ) const { + savefile->WriteBool( allowPreDeath ); + savefile->WriteInt( lastAntiProjectileAttack ); + savefile->WriteInt( passageCount ); + savefile->WriteBool( bGibOnDeath ); + savefile->WriteInt( lastPassagewayTime ); + + lastPassageway.Save( savefile ); + nextPassageway.Save( savefile ); +} + +/* +===================== +hhHarvesterSimple::Restore +===================== +*/ +void hhHarvesterSimple::Restore( idRestoreGame *savefile ) { + savefile->ReadBool( allowPreDeath ); + savefile->ReadInt( lastAntiProjectileAttack ); + savefile->ReadInt( passageCount ); + savefile->ReadBool( bGibOnDeath ); + savefile->ReadInt( lastPassagewayTime ); + + lastPassageway.Restore( savefile ); + nextPassageway.Restore( savefile ); + + bSmokes = spawnArgs.GetBool("smokes", "0"); +} + +void hhHarvesterSimple::SpawnSmoke(void) { + const char *bones[MAX_HARVESTER_LEGS]; + bones[0] = spawnArgs.GetString( "bone_smoke_front_left" ); + bones[1] = spawnArgs.GetString( "bone_smoke_front_right" ); + bones[2] = spawnArgs.GetString( "bone_smoke_rear_left" ); + bones[3] = spawnArgs.GetString( "bone_smoke_rear_right" ); + + const char *psystem = spawnArgs.GetString( "fx_smoke" ); + if ( psystem && *psystem ) { + idVec3 bonePos; + idMat3 boneAxis; + + for(int i = 0; i < MAX_HARVESTER_LEGS; i++) { + if( fxSmoke[i] != NULL ) { + gameLocal.Warning("Harvester already had smoke particles for leg %i.\n", i); + continue; + } + + hhFxInfo fxInfo; + + this->GetJointWorldTransform( bones[i], bonePos, boneAxis ); + fxInfo.SetEntity( this ); + fxInfo.SetBindBone( bones[i] ); + fxInfo.Toggle(); + fxInfo.RemoveWhenDone( false ); + + BroadcastFxInfo( psystem, bonePos, boneAxis, &fxInfo, &EV_Broadcast_AppendFxToList ); + } + } +} + +void hhHarvesterSimple::ClearSmoke(void) { + for(int i=0; i < MAX_HARVESTER_LEGS; i++) { + if(fxSmoke[i].IsValid()) { + fxSmoke[i]->Nozzle(false); + } + } + + for(int i = 0; i < MAX_HARVESTER_LEGS; i++) { + SAFE_REMOVE(fxSmoke[MAX_HARVESTER_LEGS]); + } +} + +void hhHarvesterSimple::Event_AppendFxToList(hhEntityFx *fx) { + for(int i = 0; i < MAX_HARVESTER_LEGS; ++i) { + if(fxSmoke[i] != NULL) { + continue; + } + + fxSmoke[i] = fx; + return; + } +} + +void hhHarvesterSimple::Event_GibOnDeath(const idList* parmList) { + bGibOnDeath = (atoi((*parmList)[0].c_str()) != 0); +} + +void hhHarvesterSimple::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) { + ClearSmoke(); + hhMonsterAI::Killed(inflictor, attacker, damage, dir, location); + + if (bGibOnDeath) { + Gib(vec3_origin, "damage_gib"); + } +} + +void hhHarvesterSimple::Event_Activate(idEntity *activator) { + if (fl.hidden) { + // If we're hidden, we're probably birthing out of passageway. Figure out which one. + hhAIPassageway *nearest = NULL; + float dist, nearDist = idMath::INFINITY; + hhAIPassageway *passage = reinterpret_cast (gameLocal.FindEntityOfType( hhAIPassageway::Type, NULL )); + while (passage) { + dist = (passage->GetOrigin() - GetOrigin()).Length(); + if (dist < nearDist) { + nearDist = dist; + nearest = passage; + } + passage = reinterpret_cast (gameLocal.FindEntityOfType( hhAIPassageway::Type, passage )); + } + + // If we found one near us and it's near enough, mark it as our last passageway + if (nearest && nearDist < 250.0f) { + lastPassageway = nearest; + lastPassagewayTime = gameLocal.time; + } + } + hhMonsterAI::Event_Activate(activator); +} + +void hhHarvesterSimple::AnimMove( void ) { + idVec3 goalPos; + idVec3 delta; + idVec3 goalDelta; + float goalDist; + monsterMoveResult_t moveResult; + idVec3 newDest; + + // Turn on physics to prevent flying harvesters + BecomeActive(TH_PHYSICS); + + idVec3 oldorigin = physicsObj.GetOrigin(); +#ifdef HUMANHEAD //jsh wallwalk + idMat3 oldaxis = GetGravViewAxis(); +#else + idMat3 oldaxis = viewAxis; +#endif + + AI_BLOCKED = false; + + if ( move.moveCommand < NUM_NONMOVING_COMMANDS ){ + move.lastMoveOrigin.Zero(); + move.lastMoveTime = gameLocal.time; + } + + move.obstacle = NULL; + if ( ( move.moveCommand == MOVE_FACE_ENEMY ) && enemy.GetEntity() ) { + TurnToward( lastVisibleEnemyPos ); + goalPos = oldorigin; + } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + goalPos = oldorigin; + } else if ( GetMovePos( goalPos ) ) { + if ( move.moveCommand != MOVE_WANDER ) { + CheckObstacleAvoidance( goalPos, newDest ); + TurnToward( newDest ); + } else { + TurnToward( goalPos ); + } + } + + Turn(); + + if ( move.moveCommand == MOVE_SLIDE_TO_POSITION ) { + if ( gameLocal.time < move.startTime + move.duration ) { + goalPos = move.moveDest - move.moveDir * MS2SEC( move.startTime + move.duration - gameLocal.time ); + delta = goalPos - oldorigin; + delta.z = 0.0f; + } else { + delta = move.moveDest - oldorigin; + delta.z = 0.0f; + StopMove( MOVE_STATUS_DONE ); + } + } else if ( allowMove ) { +#ifdef HUMANHEAD //jsh wallwalk + GetMoveDelta( oldaxis, GetGravViewAxis(), delta ); +#else + GetMoveDelta( oldaxis, viewAxis, delta ); +#endif + } else { + delta.Zero(); + } + + if ( move.moveCommand == MOVE_TO_POSITION ) { + goalDelta = move.moveDest - oldorigin; + goalDist = goalDelta.LengthFast(); + if ( goalDist < delta.LengthFast() ) { + delta = goalDelta; + } + } + +#ifdef HUMANHEAD //shrink functionality + float scale = renderEntity.shaderParms[SHADERPARM_ANY_DEFORM_PARM1]; + if ( scale > 0.0f && scale < 2.0f ) { + delta *= scale; + } +#endif + physicsObj.SetDelta( delta ); + physicsObj.ForceDeltaMove( disableGravity ); + + RunPhysics(); + + if ( ai_debugMove.GetBool() ) { + // HUMANHEAD JRM - so we can see if grav is on or off + if(disableGravity) { + gameRenderWorld->DebugLine( colorRed, oldorigin, physicsObj.GetOrigin(), 5000 ); + } else { + gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 5000 ); + } + } + + moveResult = physicsObj.GetMoveResult(); + if ( !af_push_moveables && attack.Length() && TestMelee() ) { + DirectDamage( attack, enemy.GetEntity() ); + } else { + idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); + if ( blockEnt && blockEnt->IsType( hhHarvesterSimple::Type ) ) { + StopMove( MOVE_STATUS_BLOCKED_BY_MONSTER ); + return; + } + if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + } + } + + BlockedFailSafe(); + + AI_ONGROUND = physicsObj.OnGround(); + + idVec3 org = physicsObj.GetOrigin(); + if ( oldorigin != org ) { + TouchTriggers(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); + gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); + DrawRoute(); + } +} + +void hhHarvesterSimple::Event_CanBecomeSolid( void ) { + int i; + int num; + idEntity * hit; + idClipModel *cm; + idClipModel *clipModels[ MAX_GENTITIES ]; + + num = gameLocal.clip.ClipModelsTouchingBounds( physicsObj.GetAbsBounds(), MASK_MONSTERSOLID, clipModels, MAX_GENTITIES ); + for ( i = 0; i < num; i++ ) { + cm = clipModels[ i ]; + + // don't check render entities + if ( cm->IsRenderModel() ) { + continue; + } + + hit = cm->GetEntity(); + if ( hit == this ) { + continue; + } + + // Special case, crush any moveables blocking our start point + if ( hit->IsType( idMoveable::Type ) ) { + hit->Damage( this, this, vec3_origin, "damage_gib", 1.0f, INVALID_JOINT ); + hit->SquishedByDoor(this); + } + + if ( !hit->fl.takedamage ) { + continue; + } + + if ( physicsObj.ClipContents( cm ) ) { + idThread::ReturnFloat( false ); + return; + } + } + + idThread::ReturnFloat( true ); +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_harvester_simple.h b/src/Prey/ai_harvester_simple.h new file mode 100644 index 0000000..2b06d57 --- /dev/null +++ b/src/Prey/ai_harvester_simple.h @@ -0,0 +1,79 @@ + +#ifndef __PREY_AI_HARVESTERSIMPLE_H__ +#define __PREY_AI_HARVESTERSIMPLE_H__ + +extern const idEventDef AI_OnProjectileLaunch; +extern const idEventDef MA_EnterPassageway; +extern const idEventDef MA_ExitPassageway; + +#define MAX_HARVESTER_LEGS 4 + +class hhHarvesterSimple : public hhMonsterAI { + +public: + CLASS_PROTOTYPE(hhHarvesterSimple); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + virtual void Event_StartPreDeath(void) {}; + void Event_OnProjectileLaunch(hhProjectile *proj) {}; + void Event_AntiProjectileAttack(hhProjectile *proj) {}; + void Event_EnterPassageway(hhAIPassageway *pn) {}; + void Event_ExitPassageway(hhAIPassageway *pn) {}; + void Event_HandlePassageway(void) {}; + void Event_UseThisPassageway(hhAIPassageway *pn, bool force) {}; + void Event_AppendFxToList(hhEntityFx *fx) {}; + void Event_GibOnDeath(const idList* parmList) {}; + virtual void Event_Activate(idEntity *activator) {}; + virtual void Event_CanBecomeSolid(void) {}; +#else + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void LinkScriptVariables(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location); + +protected: + + void Spawn(void); + + virtual int EvaluateReaction( const hhReaction *react ); + + void SpawnSmoke(void); + void ClearSmoke(void); + + //predeath torso code + virtual void Event_StartPreDeath(void); + virtual bool Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + bool allowPreDeath; // TRUE if we will allow predeath + + virtual void AnimMove(void); + + //anti-projectile chaff + void Event_OnProjectileLaunch(hhProjectile *proj); + void Event_AntiProjectileAttack(hhProjectile *proj); + void Event_EnterPassageway(hhAIPassageway *pn); + void Event_ExitPassageway(hhAIPassageway *pn); + void Event_HandlePassageway(void); + void Event_UseThisPassageway(hhAIPassageway *pn, bool force); + void Event_AppendFxToList(hhEntityFx *fx); + void Event_GibOnDeath(const idList* parmList); + virtual void Event_Activate(idEntity *activator); + virtual void Event_CanBecomeSolid(void); + + int lastAntiProjectileAttack; + int lastPassagewayTime; + idEntityPtr lastPassageway; + idEntityPtr nextPassageway; + idScriptFloat AI_CLIMB_RANGE; + idScriptFloat AI_PASSAGEWAY_HEALTH; + + idEntityPtr fxSmoke[MAX_HARVESTER_LEGS]; + + int passageCount; + bool bSmokes; + bool bGibOnDeath; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif \ No newline at end of file diff --git a/src/Prey/ai_hunter_simple.cpp b/src/Prey/ai_hunter_simple.cpp new file mode 100644 index 0000000..b40e8f1 --- /dev/null +++ b/src/Prey/ai_hunter_simple.cpp @@ -0,0 +1,2139 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef MA_LaserOn("laserOn", NULL); +const idEventDef MA_LaserOff("laserOff", NULL); +const idEventDef MA_EscapePortal( "escapePortal" ); +const idEventDef MA_AssignSniperFx( "", "e" ); +const idEventDef MA_KickAngle( "kickAngle", "v" ); +const idEventDef MA_AssignFlashLightFx( "", "e" ); +const idEventDef MA_FlashLightOn( "flashLightOn" ); +const idEventDef MA_FlashLightOff( "flashLightOff" ); +const idEventDef MA_EnemyCanSee( "enemyCanSee", NULL, 'd' ); +const idEventDef MA_PrintAction( "printAction", "s" ); +const idEventDef MA_GetAlly( "getAlly", NULL, 'e' ); +const idEventDef MA_TriggerDelay( "triggerDelay", "Ef" ); +const idEventDef MA_CallBackup( "callBackup", "f" ); +const idEventDef MA_SaySound( "saySound", "s", 'f' ); +const idEventDef MA_CheckRush( "" ); +const idEventDef MA_CheckRetreat( "" ); +const idEventDef MA_SayEnemyInfo( "sayEnemyInfo", NULL, 'd' ); +const idEventDef MA_SetNextVoiceTime( "setNextVoiceTime", "d" ); +const idEventDef MA_GetCoverNode( "getCoverNode", NULL, 'v' ); +const idEventDef MA_GetCoverPoint( "getCoverPoint", NULL, 'v' ); +const idEventDef MA_OnProjectileHit( "", "e" ); +const idEventDef MA_GetSightNode( "getSightNode", NULL, 'v' ); +const idEventDef MA_GetNearSightPoint( "getNearSightPoint", NULL, 'v' ); +const idEventDef MA_Blocked( "" ); +const idEventDef MA_EnemyPortal( "", "e" ); +const idEventDef MA_GetEnemyPortal( "getEnemyPortal", NULL, 'e' ); +const idEventDef MA_EnemyVehicleDocked( "enemyVehicleDocked", NULL, 'd' ); + +CLASS_DECLARATION(hhMonsterAI, hhHunterSimple) + EVENT( MA_OnProjectileLaunch, hhHunterSimple::Event_OnProjectileLaunch ) + EVENT( MA_OnProjectileHit, hhHunterSimple::Event_OnProjectileHit ) + EVENT( MA_LaserOn, hhHunterSimple::Event_LaserOn) + EVENT( MA_LaserOff, hhHunterSimple::Event_LaserOff) + EVENT( MA_EscapePortal, hhHunterSimple::Event_EscapePortal ) + EVENT( MA_AssignSniperFx, hhHunterSimple::Event_AssignSniperFx ) + EVENT( MA_KickAngle, hhHunterSimple::Event_KickAngle ) + EVENT( MA_AssignFlashLightFx, hhHunterSimple::Event_AssignFlashLightFx ) + EVENT( MA_FlashLightOn, hhHunterSimple::Event_FlashLightOn ) + EVENT( MA_FlashLightOff, hhHunterSimple::Event_FlashLightOff ) + EVENT( MA_EnemyCanSee, hhHunterSimple::Event_EnemyCanSee ) + EVENT( MA_PrintAction, hhHunterSimple::Event_PrintAction ) + EVENT( MA_GetAlly, hhHunterSimple::Event_GetAlly ) + EVENT( MA_TriggerDelay, hhHunterSimple::Event_TriggerDelay ) + EVENT( MA_CallBackup, hhHunterSimple::Event_CallBackup ) + EVENT( MA_SaySound, hhHunterSimple::Event_SaySound ) + EVENT( MA_CheckRush, hhHunterSimple::Event_CheckRush ) + EVENT( MA_CheckRetreat, hhHunterSimple::Event_CheckRetreat ) + EVENT( MA_SayEnemyInfo, hhHunterSimple::Event_SayEnemyInfo ) + EVENT( MA_SetNextVoiceTime, hhHunterSimple::Event_SetNextVoiceTime ) + EVENT( MA_GetCoverNode, hhHunterSimple::Event_GetCoverNode ) + EVENT( MA_GetCoverPoint, hhHunterSimple::Event_GetCoverPoint ) + EVENT( MA_GetSightNode, hhHunterSimple::Event_GetSightNode ) + EVENT( MA_GetNearSightPoint, hhHunterSimple::Event_GetNearSightPoint ) + EVENT( MA_Blocked, hhHunterSimple::Event_Blocked ) + EVENT( MA_EnemyPortal, hhHunterSimple::Event_EnemyPortal ) + EVENT( MA_GetEnemyPortal, hhHunterSimple::Event_GetEnemyPortal ) + EVENT( MA_EnemyVehicleDocked, hhHunterSimple::Event_EnemyVehicleDocked ) +END_CLASS + +hhHunterSimple::hhHunterSimple() { + beamLaser = 0; + kickSpeed = ang_zero; + kickAngles = ang_zero; + alternateAccuracy = false; + bFlashLight = false; + nodeList.Clear(); + ally.Clear(); + nextEnemyCheck = 0; + enemyPortal = 0; + nextBlockCheckTime = 0; + lastMoveOrigin = vec3_zero; + nextSpiritCheck = 0; //HUMANHEAD jsh PCF 5/2/06 hunter combat fixes +} + +void hhHunterSimple::Spawn() { + bFlashLight = false; + lastChargeTime = 0; + endSpeechTime = 0; + enemyRushCount = 0; + enemyRetreatCount = 0; + nextVoiceTime = 0; + currentAction.Clear(); + currentSpeech.Clear(); + flashlightLength = spawnArgs.GetFloat( "flashlightLength", "90" ); + flashlightTime = gameLocal.time + SEC2MS( spawnArgs.GetInt( "flashlight_delay" ) ); + beamLaser = hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "def_beamLaser" ) ); + initialOrigin = GetOrigin(); + if( beamLaser.IsValid() ) { + beamLaser->Activate( false ); + idMat3 boneAxis; + idVec3 boneOrigin; + GetJointWorldTransform( spawnArgs.GetString("laser_bone"), boneOrigin, boneAxis ); + beamLaser->SetOrigin( boneOrigin ); + beamLaser->BindToJoint( this, spawnArgs.GetString("laser_bone"), false ); + } + PostEventSec( &MA_CheckRush, 0 ); + PostEventSec( &MA_CheckRetreat, 0 ); +} + +void hhHunterSimple::Event_PostSpawn() { + hhMonsterAI::Event_PostSpawn(); + + //look for ainodes in targets list + for ( int i=0;iIsType( hhAINode::Type ) ) { + nodeList.Append( targets[i] ); + } + } + if ( nodeList.Num() <= 0 ) { + //if targets list had none, find dynamically + idEntity *ent; + float distSq; + aasPath_t path; + int toAreaNum, areaNum; + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if ( !ent || !ent->spawnArgs.GetBool( "ainode", "0" ) ) { + continue; + } + distSq = (GetOrigin() - ent->GetOrigin()).LengthSqr(); + if ( distSq > 2250000 ) { //less than 1500 + continue; + } + //check reachability + toAreaNum = PointReachableAreaNum( ent->GetOrigin() ); + areaNum = PointReachableAreaNum( physicsObj.GetOrigin() ); + if ( !toAreaNum || !PathToGoal( path, areaNum, physicsObj.GetOrigin(), toAreaNum, ent->GetOrigin() ) ) { + continue; + } + nodeList.Append( ent ); + } + } +} + +void hhHunterSimple::Event_OnProjectileLaunch(hhProjectile *proj) { + if ( !enemy.IsValid() || !proj || AI_VEHICLE ) { + return; + } + if ( GetPhysics() && GetPhysics()->GetGravityNormal() != idVec3( 0,0,-1 ) ) { + return; + } + + float min = spawnArgs.GetFloat( "dda_dodge_min", "0.3" ); + float max = spawnArgs.GetFloat( "dda_dodge_max", "0.8" ); + float dodgeChance = 0.6f; + + dodgeChance = (min + (max-min)*gameLocal.GetDDAValue() ); + + if ( ai_debugBrain.GetBool() ) { + gameLocal.Printf( "%s dodge chance %f\n", GetName(), dodgeChance ); + } + if ( proj->GetOwner() && proj->GetOwner() == enemy.GetEntity() ) { + float reactChance = proj->spawnArgs.GetFloat( "react_chance" ); + if ( gameLocal.random.RandomFloat() < reactChance ) { + if ( ai_debugBrain.GetBool() ) { + gameLocal.Printf( "say %s\n", proj->spawnArgs.GetString( "react_sound" ) ); + } + AI_ENEMY_RETREAT = true; + PostEventSec( &MA_SaySound, gameLocal.random.RandomFloat(), proj->spawnArgs.GetString( "react_sound" ) ); + } + } + if ( gameLocal.random.RandomFloat() > dodgeChance ) { + return; + } + if ( proj->GetOwner() && proj->GetOwner()->IsType( hhHunterSimple::Type ) ) { + return; + } + + //determine which side to dodge to + const function_t *newstate = NULL; + idVec3 povPos, targetPos; + povPos = enemy->GetPhysics()->GetOrigin(); + targetPos = GetPhysics()->GetOrigin(); + idVec3 povToTarget = targetPos - povPos; + povToTarget.z = 0.f; + povToTarget.Normalize(); + idVec3 povLeft, povUp; + povToTarget.OrthogonalBasis(povLeft, povUp); + povLeft.Normalize(); + + idVec3 projVel = proj->GetPhysics()->GetLinearVelocity(); + projVel.Normalize(); + float dot = povLeft * projVel; + + if ( dot < 0 ) { + newstate = GetScriptFunction( "state_DodgeRight" ); + } else { + newstate = GetScriptFunction( "state_DodgeLeft" ); + } + + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +} + +void hhHunterSimple::Event_LaserOn() { + if ( beamLaser.IsValid() ) { + beamLaser->Activate( true ); + } +} + +void hhHunterSimple::Event_LaserOff() { + if ( beamLaser.IsValid() ) { + beamLaser->Activate( false ); + } +} + +void hhHunterSimple::Event_KickAngle( const idAngles &ang ) { + kickSpeed = ang; +} + +void hhHunterSimple::Think() { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { + return; + } + + hhMonsterAI::Think(); + + trace_t trace; + if ( flashlightTime <= 0 || gameLocal.time > flashlightTime ) { + flashlightTime = 0; + if ( !AI_DEAD && !IsHidden() && !InVehicle() ) { + idMat3 boneAxis; + idVec3 boneOrigin; + GetJointWorldTransform( "fx_barrel", boneOrigin, boneAxis ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, boneOrigin, boneOrigin + boneAxis[0] * flashlightLength, 10, 1 ); + } + gameLocal.clip.TracePoint( trace, boneOrigin, boneOrigin + boneAxis[0] * flashlightLength, MASK_OPAQUE, this ); + if ( trace.fraction < 1.0f ) { + Event_FlashLightOff(); + } else { + Event_FlashLightOn(); + } + } else if ( bFlashLight ) { + Event_FlashLightOff(); + } + } + if ( enemy.IsValid() && beamLaser.IsValid() && beamLaser->IsActivated() ) { + memset(&trace, 0, sizeof(trace)); + + float frametime = (gameLocal.time - gameLocal.previousTime); + kickSpeed-=4*kickSpeed*frametime/1000; + kickSpeed-=30*kickAngles*frametime/1000; + kickAngles+=kickSpeed*frametime/1000; + + idAngles ang = (lastVisibleEnemyPos + enemy->EyeOffset() - idVec3( 0,0,10 ) - beamLaser->GetOrigin()).ToAngles(); + ang += kickAngles; + idVec3 forward, up, right; + ang.ToVectors( &forward, &right, &up ); + gameLocal.clip.TracePoint( trace, beamLaser->GetOrigin(), beamLaser->GetOrigin() + forward * 4000, MASK_SHOT_BOUNDINGBOX, this ); + if ( trace.fraction < 1.0f ) { + idEntity *hitEnt = gameLocal.GetTraceEntity( trace ); + if ( hitEnt && hitEnt->IsType( idPlayer::Type ) || + ( hitEnt && hitEnt->IsType( hhVehicle::Type ) ) ) { + //if we hit the player, set the endpoint a little farther back + //to prevent the laser from stopping at our camera viewpoint + idVec3 offset = (trace.endpos - beamLaser->GetOrigin()).ToNormal() * 100; + beamLaser->SetTargetLocation( trace.endpos + offset ); + } else { + beamLaser->SetTargetLocation( trace.endpos ); + } + } else { + beamLaser->SetTargetLocation( beamLaser->GetOrigin() + forward * 5000 ); + } + } + + if ( ai_debugBrain.GetBool() && ally.IsValid() && ally->GetHealth() > 0 ) { + gameRenderWorld->DebugArrow( colorGreen, GetOrigin(), ally->GetOrigin(), 10, 1 ); + } + + if ( health > 0 && ai_debugActions.GetBool() && currentAction.Length() ) { + if ( AI_ENEMY_VISIBLE ) { + gameRenderWorld->DrawText("visible", GetEyePosition() + idVec3(0.0f, 0.0f, 30.0f), 0.4f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + if ( AI_ENEMY_SHOOTABLE ) { + gameRenderWorld->DrawText("shootable", GetEyePosition() + idVec3(0.0f, 0.0f, 20.0f), 0.4f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + gameRenderWorld->DrawText(va( "%s", currentAction.c_str() ), GetEyePosition() + idVec3(0.0f, 0.0f, 0.0f), 0.4f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + if ( health > 0 && ai_printSpeech.GetBool() && currentSpeech.Length() ) { + gameRenderWorld->DrawText(va( "%s", currentSpeech.c_str() ), GetEyePosition() + idVec3(0.0f, 0.0f, 10.0f), 0.4f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + if ( endSpeechTime > 0 && gameLocal.time > endSpeechTime ) { + currentSpeech.Clear(); + endSpeechTime = 0; + } + } + + if ( enemy.IsValid() ) { + //HUMANHEAD jsh PCF 5/2/06 hunter combat fixes + if ( nextSpiritCheck > 0 && gameLocal.time > nextSpiritCheck && enemy->IsType( hhSpiritProxy::Type ) ) { + nextSpiritCheck = 0; + SetEnemy( gameLocal.GetLocalPlayer() ); + } + if ( AI_ENEMY_VISIBLE ) { + AI_ENEMY_LAST_SEEN = MS2SEC( gameLocal.realClientTime ); + } + if ( enemy->GetHealth() < spawnArgs.GetInt( "health_warning", "25" ) ) { + AI_ENEMY_HEALTH_LOW = true; + } + } +} + +idProjectile *hhHunterSimple::LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { //HUMANHEAD mdc - added desiredProjectileDef for supporting multiple projs. + //jsh overridden to allow per-projectile accuracy + idVec3 muzzle; + idVec3 dir; + idVec3 start; + trace_t tr; + idBounds projBounds; + float distance; + const idClipModel *projClip; + float attack_accuracy; + float attack_cone; + float projectile_spread; + float diff; + float angle; + float spin; + idAngles ang; + int num_projectiles; + int i; + idMat3 axis; + idVec3 tmp; + idProjectile *lastProjectile; + + //HUMANHEAD mdc - added to support multiple projectiles + if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile + int projIndex = FindProjectileInfo( desiredProjectileDef ); + if( projIndex >= 0 ) { + SetCurrentProjectile( projIndex ); + } + } + //HUMANHEAD END + + + if ( !projectileDef || AI_DEAD ) { + gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); + return NULL; + } + + if ( projectileDef->GetBool( "snipe", "0" ) ) { + attack_accuracy = 0; + } else { + if ( projectileDef->GetFloat( "attack_accuracy" ) ) { + attack_accuracy = projectileDef->GetFloat( "attack_accuracy", "7" ); + } else { + attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); + } + } + + attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); + if ( projectileDef->GetFloat( "projectile_spread" ) ) { + projectile_spread = projectileDef->GetFloat( "projectile_spread", "0" ); + } else { + projectile_spread = spawnArgs.GetFloat( "projectile_spread", "0" ); + } + if ( projectileDef->GetFloat( "num_projectiles" ) ) { + num_projectiles = projectileDef->GetFloat( "num_projectiles", "1" ); + } else { + num_projectiles = spawnArgs.GetFloat( "num_projectiles", "1" ); + } + + + GetMuzzle( jointname, muzzle, axis ); + + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, axis[ 0 ] ); + } + + lastProjectile = projectile.GetEntity(); + + if ( target != NULL ) { + tmp = target->GetPhysics()->GetAbsBounds().GetCenter() - muzzle; + tmp.Normalize(); + axis = tmp.ToMat3(); + } else { + axis = viewAxis; + } + + // rotate it because the cone points up by default + tmp = axis[2]; + axis[2] = axis[0]; + axis[0] = -tmp; + + // make sure the projectile starts inside the monster bounding box + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + projClip = lastProjectile->GetPhysics()->GetClipModel(); + projBounds = projClip->GetBounds().Rotate( axis ); + + // check if the owner bounds is bigger than the projectile bounds + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { + start = muzzle + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); + muzzle = tr.endpos; + + // set aiming direction + GetAimDir( muzzle, target, this, dir ); + ang = dir.ToAngles(); + + // hunter inaccuracy is calculated instead of random spread based + float hitchance; + if ( !projectileDef->GetFloat( "hit_chance", "0.2", hitchance ) ) { + hitchance = spawnArgs.GetFloat( "hit_chance" ); + } + float hitAccuracy; + if ( !projectileDef->GetFloat( "hit_accuracy", "0", hitAccuracy ) ) { + hitAccuracy = spawnArgs.GetFloat( "hit_accuracy" ); + } + float missAccuracy; + if ( !projectileDef->GetFloat( "miss_accuracy", "0", missAccuracy ) ) { + missAccuracy = spawnArgs.GetFloat( "miss_accuracy" ); + } + float t = MS2SEC( gameLocal.time + entityNumber * 497 ); + alternateAccuracy = !alternateAccuracy; + if ( gameLocal.random.RandomFloat() < hitchance ) { + //rolled a hit so add very little inaccuracy + ang.pitch += idMath::Fabs( idMath::Sin16( t * 5.1 ) * hitAccuracy ); + ang.yaw += idMath::Sin16( t * 6.7 ) * hitAccuracy; + } else { + float rand = gameLocal.random.RandomFloat(); + if ( alternateAccuracy ) { + rand *= -1; + } + ang.pitch += rand * missAccuracy; + if ( !alternateAccuracy ) { + rand *= -1; + } + ang.yaw += rand * missAccuracy; + } + + //quick hack that should be fine for now + bool oriented = GetPhysics()->GetGravityNormal() != idVec3( 0,0,-1 ); + if ( !oriented && clampToAttackCone ) { + // clamp the attack direction to be within monster's attack cone so he doesn't do + // things like throw the missile backwards if you're behind him + diff = idMath::AngleDelta( ang.yaw, current_yaw ); + if ( diff > attack_cone ) { + ang.yaw = current_yaw + attack_cone; + } else if ( diff < -attack_cone ) { + ang.yaw = current_yaw - attack_cone; + } + } + + axis = ang.ToMat3(); + + float spreadRad = DEG2RAD( projectile_spread ); + for( i = 0; i < num_projectiles; i++ ) { + // spread the projectiles out + angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); + spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); + dir = axis[ 0 ] + axis[ 2 ] * ( angle * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir.Normalize(); + + // launch the projectile + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, dir ); + } + lastProjectile = projectile.GetEntity(); + lastProjectile->Launch( muzzle, dir, vec3_origin ); + projectile = NULL; + } + + TriggerWeaponEffects( muzzle, axis ); + + lastAttackTime = gameLocal.time; + +//HUMANHEAD mdc - added to support multiple projectiles + projectile = NULL; + SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side +//HUMANHEAD END + + return lastProjectile; +} + +void hhHunterSimple::Event_EscapePortal() { + static const char * passPrefix = "portal_"; + const char * portalDef; + idEntity * portal; + idDict portalArgs; + idList xferKeys; + idList xferValues; + const idKeyValue * buddyKV; + + + // Find out which portal def to spawn - If none specified, then exit; + buddyKV = spawnArgs.FindKey( "portal_buddy" ); + if ( buddyKV && buddyKV->GetValue().Length() && gameLocal.FindEntity(buddyKV->GetValue().c_str()) ) { + // Case of a valid portal_buddy key, make a real portal + portalDef = spawnArgs.GetString( "def_portal" ); + } else { + portalDef = spawnArgs.GetString( "def_fakeportal" ); + } + + if ( !portalDef || !portalDef[0] ) { + return; + } + + // Set the origin of the portal to us. + //portalArgs.SetVector( "origin", GetOrigin() ); + idVec3 escapePortalOffset = spawnArgs.GetVector( "escapePortalOffset", "40 0 0" ); + portalArgs.SetVector( "origin", GetOrigin() + ( escapePortalOffset * GetAxis() ) ); + portalArgs.SetFloat( "angle", GetAxis()[0].ToYaw() - 180 ); + + // Set the portal draw distance to zero (so the player cannot see through the creature portal) + portalArgs.Set( "shaderParm5", "0" ); + + // Pass along all 'portal_' keys to the portal's spawnArgs; + hhUtils::GetKeysAndValues( spawnArgs, passPrefix, xferKeys, xferValues ); + for ( int i = 0; i < xferValues.Num(); ++i ) { + xferKeys[ i ].StripLeadingOnce( passPrefix ); + portalArgs.Set( xferKeys[ i ].c_str(), xferValues[ i ].c_str() ); + } + + // Set the name of the associated game portal so it can be turned on and off + portalArgs.Set( "gamePortalName", GetName() ); + + // Spawn the portal + portal = gameLocal.SpawnObject( portalDef, &portalArgs ); + if ( !portal ) { + return; + } + + // Move the portal up some pre determinted amt, since its origin is in the middle of it + float offset = spawnArgs.GetFloat( "offset_portal", 0 ); + portal->GetPhysics()->SetOrigin( portal->GetPhysics()->GetOrigin() + (portal->GetAxis()[2] * offset) ); + + // Update the camera stuff + portal->ProcessEvent( &EV_UpdateCameraTarget ); + + // Open the portal - Need to delay this, so that PostSpawn gets called/sets up the partner portal + //? Should we always pass in the player? + portal->PostEventSec( &EV_Activate, 0, gameLocal.GetLocalPlayer() ); + + // Maybe wait for it to finish? + + +} + +void hhHunterSimple::Event_FindReaction( const char* effect ) { + idEntity* ent; + hhReaction* react; + hhReactionDesc::Effect react_effect; + idEntity* bestEnt = NULL; + int bestReactIndex = -1; + float bestDistance = -1; + float meToReact, enemyToReact; + int bestRank = -1; + + react_effect = hhReactionDesc::StrToEffect( effect ); + + if( react_effect == hhReactionDesc::Effect_Invalid ) { + gameLocal.Warning( "unknown effect '%s' requested from FindReaction", effect ); + idThread::ReturnEntity( NULL ); + return; + } + + for ( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if( !ent || ent->fl.isDormant || !ent->fl.refreshReactions ) { + continue; + } + + for( int j = 0; j < ent->GetNumReactions(); j++ ) { + react = ent->GetReaction( j ); + if( !react || !react->IsActive() ) { + continue; + } + if( react_effect != react->desc->effect ) { + continue; + } + if( react->desc->flags & hhReactionDesc::flag_Exclusive ) { //check exclusiveness + if ( react->exclusiveOwner.IsValid() && react->exclusiveOwner->health <= 0 ) { + react->exclusiveOwner.Clear(); + } + if( react->exclusiveOwner.GetEntity() && react->exclusiveOwner != this ) { + continue; + } + } + //Skip based on flag requirements + if( (react->desc->flags & hhReactionDesc::flagReq_RangeAttack) && !AI_HAS_RANGE_ATTACK ) { + continue; + } + // Skip monsters without melee attack + if( (react->desc->flags & hhReactionDesc::flagReq_MeleeAttack) && !AI_HAS_MELEE_ATTACK ) { + continue; + } + if( react->desc->flags & hhReactionDesc::flagReq_KeyValue ) { + idStr val; + if( spawnArgs.GetString(react->desc->key, "", val) ) { + if( val != react->desc->keyVal ) { + continue; + } + } + else { + continue; + } + } + // Skip monsters without specific animation? + if( react->desc->flags & hhReactionDesc::flagReq_Anim ) { + if( !GetAnimator()->HasAnim(react->desc->anim) ) { + continue; + } + } + + // Skip monsters in vehicles? + if( (react->desc->flags & hhReactionDesc::flagReq_NoVehicle) && InVehicle() ) { + continue; + } + + float distToEnt, distToEnemy, distEnemyToEnt; + // Check actual specifics for reaction type + switch( react->desc->effect ) { + case hhReactionDesc::Effect_HaveFun: + case hhReactionDesc::Effect_Vehicle: + case hhReactionDesc::Effect_VehicleDock: + case hhReactionDesc::Effect_ProvideCover: + //only use reasonably reachable objects + if ( enemy.IsValid() ) { + distToEnt = TravelDistance( GetOrigin(), ent->GetOrigin() ); + distToEnemy = (GetOrigin() - enemy->GetOrigin()).Length(); + distEnemyToEnt = (enemy->GetOrigin() - ent->GetOrigin()).Length(); + if ( distToEnt > 1.0 ) { + float ratio = distToEnt / distEnemyToEnt; + if ( ratio > 2.0f ) { + //skip if object is farther than enemy or if enemy is closer to object than i am + continue; + } + } + } + case hhReactionDesc::Effect_Heal: + if ( enemy.IsValid() ) { + distToEnt = TravelDistance( GetOrigin(), ent->GetOrigin() ); + distToEnemy = (GetOrigin() - enemy->GetOrigin()).Length(); + distEnemyToEnt = (enemy->GetOrigin() - ent->GetOrigin()).Length(); + if ( distToEnt > 1.0 ) { + float ratio = distToEnt / distEnemyToEnt; + if ( ratio > 2.0f ) { + //skip if object is farther than enemy or if enemy is closer to object than i am + continue; + } + } + } + case hhReactionDesc::Effect_Climb: + case hhReactionDesc::Effect_Passageway: + case hhReactionDesc::Effect_CallBackup: + break; + case hhReactionDesc::Effect_DamageEnemy: + //check if enemy is close enough to matter + if ( enemy.IsValid() && react->desc->effectRadius > 0 ) { + enemyToReact = (enemy->GetOrigin() - react->causeEntity->GetOrigin()).LengthSqr(); + if ( enemyToReact > react->desc->effectRadius * react->desc->effectRadius ) { + continue; + } + } + //check if i'm too close + meToReact = (GetOrigin() - react->causeEntity->GetOrigin()).LengthSqr(); + if ( meToReact < react->desc->effectRadius * react->desc->effectRadius ) { + continue; + } + break; + case hhReactionDesc::Effect_Damage: + default: + continue; + } + // if we have actually gotten this far, do our intense calculations last.. + int rank = EvaluateReaction( react ); + if( rank == 0 ) { + continue; + } + +// We have a valid reaction... + float distSq = ( react->causeEntity->GetOrigin() - GetOrigin() ).LengthSqr(); + if ( bestRank == -1 || bestRank <= rank ) { // Check the reaction rank against our best rank + if (rank == bestRank && distSq > bestDistance) { // If they are the same rank, but the current one is farther then ignore it. + continue; + } + bestEnt = ent; + bestReactIndex = j; + bestDistance = distSq; + bestRank = rank; + } + } + } + + if ( bestEnt ) { + targetReaction.entity = bestEnt; + targetReaction.reactionIndex = bestReactIndex; + hhReaction *reaction = targetReaction.GetReaction(); + if( reaction && reaction->desc->flags & hhReactionDesc::flagReq_RangeAttack ) { + shootTarget = bestEnt; + } else { + shootTarget = NULL; + } + idThread::ReturnEntity( targetReaction.entity.GetEntity() ); + } else { + idThread::ReturnEntity( NULL ); + } +} + +void hhHunterSimple::AnimMove( void ) { + //overridden to set AI_BLOCKED when blockEnt is found + idVec3 goalPos; + idVec3 delta; + idVec3 goalDelta; + float goalDist; + monsterMoveResult_t moveResult; + idVec3 newDest; + + idVec3 oldorigin = physicsObj.GetOrigin(); +#ifdef HUMANHEAD //jsh wallwalk + idMat3 oldaxis = GetGravViewAxis(); +#else + idMat3 oldaxis = viewAxis; +#endif + + AI_BLOCKED = false; + + if ( move.moveCommand < NUM_NONMOVING_COMMANDS ){ + move.lastMoveOrigin.Zero(); + move.lastMoveTime = gameLocal.time; + } + + move.obstacle = NULL; + if ( ( move.moveCommand == MOVE_FACE_ENEMY ) && enemy.GetEntity() ) { + TurnToward( lastVisibleEnemyPos ); + goalPos = oldorigin; + } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + goalPos = oldorigin; + } else if ( GetMovePos( goalPos ) ) { + if ( move.moveCommand != MOVE_WANDER ) { + CheckObstacleAvoidance( goalPos, newDest ); + nextMovePos = newDest; + TurnToward( newDest ); + } else { + TurnToward( goalPos ); + } + } + + Turn(); + + if ( move.moveCommand == MOVE_SLIDE_TO_POSITION ) { + if ( gameLocal.time < move.startTime + move.duration ) { + goalPos = move.moveDest - move.moveDir * MS2SEC( move.startTime + move.duration - gameLocal.time ); + delta = goalPos - oldorigin; + delta.z = 0.0f; + } else { + delta = move.moveDest - oldorigin; + delta.z = 0.0f; + StopMove( MOVE_STATUS_DONE ); + } + } else if ( allowMove ) { +#ifdef HUMANHEAD //jsh wallwalk + GetMoveDelta( oldaxis, GetGravViewAxis(), delta ); +#else + GetMoveDelta( oldaxis, viewAxis, delta ); +#endif + } else { + delta.Zero(); + } + + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, physicsObj.GetOrigin(), physicsObj.GetOrigin() + delta.ToNormal() * 100, 10 ); + gameRenderWorld->DebugArrow( colorGreen, physicsObj.GetOrigin(), physicsObj.GetOrigin() + renderEntity.axis[1] * 100, 10 ); + gameRenderWorld->DebugArrow( colorGreen, physicsObj.GetOrigin(), physicsObj.GetOrigin() + GetPhysics()->GetAxis()[2] * 100, 10 ); + } + + if ( move.moveCommand == MOVE_TO_POSITION ) { + goalDelta = move.moveDest - oldorigin; + goalDist = goalDelta.LengthFast(); + if ( goalDist < delta.LengthFast() ) { + delta = goalDelta; + } + } + physicsObj.SetDelta( delta ); + physicsObj.ForceDeltaMove( disableGravity ); + + RunPhysics(); + + moveResult = physicsObj.GetMoveResult(); + if ( !af_push_moveables && attack.Length() && TestMelee() ) { + DirectDamage( attack, enemy.GetEntity() ); + } else { + idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); + if ( blockEnt ) { + AI_BLOCKED = true; + AI_NEXT_DIR_TIME = 0; + + if ( blockEnt->IsType( idAI::Type ) ) { + StopMove( MOVE_STATUS_BLOCKED_BY_MONSTER ); + } + } + if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + } + } + + BlockedFailSafe(); + + AI_ONGROUND = physicsObj.OnGround(); + + idVec3 org = physicsObj.GetOrigin(); + if ( oldorigin != org ) { + TouchTriggers(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); + gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); + DrawRoute(); + } +} + +void hhHunterSimple::Event_AssignSniperFx( hhEntityFx* fx ) { + sniperFx = fx; +} + +void hhHunterSimple::Event_AssignFlashLightFx( hhEntityFx* fx ) { + flashLightFx = fx; +} + +bool hhHunterSimple::UpdateAnimationControllers( void ) { + idVec3 local; + idVec3 focusPos; + idVec3 left; + idVec3 dir; + idVec3 orientationJointPos; + idVec3 localDir; + idAngles newLookAng; + idAngles diff; + idMat3 mat; + idMat3 axis; + idMat3 orientationJointAxis; + idAFAttachment *headEnt = head.GetEntity(); + idVec3 eyepos; + idVec3 pos; + int i; + idAngles jointAng; + float orientationJointYaw; + + if ( AI_DEAD ) { + return idActor::UpdateAnimationControllers(); + } + + if ( orientationJoint == INVALID_JOINT ) { + orientationJointAxis = viewAxis; + orientationJointPos = physicsObj.GetOrigin(); + orientationJointYaw = current_yaw; + } else { + GetJointWorldTransform( orientationJoint, gameLocal.time, orientationJointPos, orientationJointAxis ); + orientationJointYaw = orientationJointAxis[ 2 ].ToYaw(); + orientationJointAxis = idAngles( 0.0f, orientationJointYaw, 0.0f ).ToMat3(); + } + + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorCyan, orientationJointPos, orientationJointPos + orientationJointAxis[0] * 64.0, 10, 1 ); + } + + if ( focusJoint != INVALID_JOINT ) { + if ( headEnt ) { + headEnt->GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis ); + } else { + // JRMMERGE_GRAVAXIS - What about GetGravAxis() are we still using/needing that? + GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis ); + } + eyeOffset.z = eyepos.z - physicsObj.GetOrigin().z; + } else { + eyepos = GetEyePosition(); + } + + if ( headEnt ) { + CopyJointsFromBodyToHead(); + } + + // Update the IK after we've gotten all the joint positions we need, but before we set any joint positions. + // Getting the joint positions causes the joints to be updated. The IK gets joint positions itself (which + // are already up to date because of getting the joints in this function) and then sets their positions, which + // forces the heirarchy to be updated again next time we get a joint or present the model. If IK is enabled, + // or if we have a seperate head, we end up transforming the joints twice per frame. Characters with no + // head entity and no ik will only transform their joints once. Set g_debuganim to the current entity number + // in order to see how many times an entity transforms the joints per frame. + idActor::UpdateAnimationControllers(); + + idEntity *focusEnt = focusEntity.GetEntity(); + //HUMANHEAD jsh allow eyefocus independent from allowJointMod + if ( ( !allowJointMod && !allowEyeFocus ) || ( gameLocal.time >= focusTime && focusTime != -1 ) ) { + focusPos = GetEyePosition() + orientationJointAxis[ 0 ] * 512.0f; + } else if ( focusEnt == NULL ) { + // keep looking at last position until focusTime is up + focusPos = currentFocusPos; + } else if ( focusEnt == enemy.GetEntity() ) { + if ( beamLaser.IsValid() && beamLaser->IsActivated() ) { + focusPos = beamLaser->GetTargetLocation(); + } else { + focusPos = lastVisibleEnemyPos + lastVisibleEnemyEyeOffset - eyeVerticalOffset * enemy.GetEntity()->GetPhysics()->GetGravityNormal(); + } + } else if ( focusEnt->IsType( idActor::Type ) ) { + focusPos = static_cast( focusEnt )->GetEyePosition() - eyeVerticalOffset * focusEnt->GetPhysics()->GetGravityNormal(); + } else { + focusPos = focusEnt->GetPhysics()->GetOrigin(); + } + + currentFocusPos = currentFocusPos + ( focusPos - currentFocusPos ) * eyeFocusRate; + + // determine yaw from origin instead of from focus joint since joint may be offset, which can cause us to bounce between two angles + dir = focusPos - orientationJointPos; + newLookAng.yaw = idMath::AngleNormalize180( dir.ToYaw() - orientationJointYaw ); + newLookAng.roll = 0.0f; + newLookAng.pitch = 0.0f; + newLookAng += lookOffset; + + // determine pitch from joint position + dir = focusPos - eyepos; + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorYellow, eyepos, eyepos + dir, 10, 1 ); + } + dir.NormalizeFast(); + physicsObj.GetAxis().ProjectVector( dir, localDir ); + newLookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ) + lookOffset.pitch; + newLookAng.roll = 0.0f; + diff = newLookAng - lookAng; + + if ( eyeAng != diff ) { + eyeAng = diff; + eyeAng.Clamp( eyeMin, eyeMax ); + idAngles angDelta = diff - eyeAng; + if ( !angDelta.Compare( ang_zero, 0.1f ) ) { + alignHeadTime = gameLocal.time; + } else { + alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime; + } + } + + if ( idMath::Fabs( newLookAng.yaw ) < 0.1f ) { + alignHeadTime = gameLocal.time; + } + + if ( ( gameLocal.time >= alignHeadTime ) || ( gameLocal.time < forceAlignHeadTime ) ) { + alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime; + destLookAng = newLookAng; + destLookAng.Clamp( lookMin, lookMax ); + } + + diff = destLookAng - lookAng; + if ( ( lookMin.pitch == -180.0f ) && ( lookMax.pitch == 180.0f ) ) { + if ( ( diff.pitch > 180.0f ) || ( diff.pitch <= -180.0f ) ) { + diff.pitch = 360.0f - diff.pitch; + } + } + if ( ( lookMin.yaw == -180.0f ) && ( lookMax.yaw == 180.0f ) ) { + if ( diff.yaw > 180.0f ) { + diff.yaw -= 360.0f; + } else if ( diff.yaw <= -180.0f ) { + diff.yaw += 360.0f; + } + } + lookAng = lookAng + diff * headFocusRate; + lookAng.Normalize180(); + + jointAng.roll = 0.0f; + if ( allowJointMod ) { + //quick hack. dont change yaw if in different gravity + bool oriented = GetPhysics()->GetGravityNormal() != idVec3( 0,0,-1 ); + for( i = 0; i < lookJoints.Num(); i++ ) { + if ( oriented ) { + jointAng.yaw = 0; + } else { + jointAng.yaw = lookAng.yaw * lookJointAngles[ i ].yaw; + } + jointAng.pitch = lookAng.pitch * lookJointAngles[ i ].pitch; + animator.SetJointAxis( lookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() ); + } + } + + if ( move.moveType == MOVETYPE_FLY ) { + // lean into turns + AdjustFlyingAngles(); + } + + if ( headEnt ) { + idAnimator *headAnimator = headEnt->GetAnimator(); + + // HUMANHEAD pdm: Added support for look joints in head entities + if ( allowJointMod ) { + for( i = 0; i < headLookJoints.Num(); i++ ) { + jointAng.pitch = lookAng.pitch * headLookJointAngles[ i ].pitch; + jointAng.yaw = lookAng.yaw * headLookJointAngles[ i ].yaw; + headAnimator->SetJointAxis( headLookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() ); + } + } + // HUMANHEAD END + + if ( allowEyeFocus ) { + idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3(); idMat3 headTranspose = headEnt->GetPhysics()->GetAxis().Transpose(); + axis = eyeAxis * orientationJointAxis; + left = axis[ 1 ] * eyeHorizontalOffset; + eyepos -= headEnt->GetPhysics()->GetOrigin(); + headAnimator->SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose ); + headAnimator->SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f - left ) * headTranspose ); + + //if ( ai_debugMove.GetBool() ) { + // gameRenderWorld->DebugLine( colorRed, orientationJointPos, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose, gameLocal.msec ); + //} + } else { + headAnimator->ClearJoint( leftEyeJoint ); + headAnimator->ClearJoint( rightEyeJoint ); + } + } else { + if ( allowEyeFocus ) { + idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3(); + axis = eyeAxis * orientationJointAxis; + left = axis[ 1 ] * eyeHorizontalOffset; + eyepos += axis[ 0 ] * 64.0f - physicsObj.GetOrigin(); + animator.SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + left ); + animator.SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos - left ); + } else { + animator.ClearJoint( leftEyeJoint ); + animator.ClearJoint( rightEyeJoint ); + } + } + + //HUMANHEAD pdm jawflap + hhAnimator *theAnimator; + if (head.IsValid()) { + theAnimator = head->GetAnimator(); + } + else { + theAnimator = GetAnimator(); + } + JawFlap(theAnimator); + //END HUMANHEAD + + return true; +} + +void hhHunterSimple::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + //overridden to remove muzzle fx on death + if ( AI_DEAD ) { + AI_DAMAGE = true; + return; + } + if ( enemy.IsValid() ) { + gameLocal.AlertAI( enemy.GetEntity(), spawnArgs.GetInt( "death_alert_radius", "800" ) ); + } + Event_FlashLightOff(); + if ( beamLaser.IsValid() ) { + beamLaser->Unbind(); + SAFE_REMOVE( beamLaser ); + } + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + + //taken from idEntity::RemoveBinds() + //remove any lingering fx on death + idEntity *ent; + idEntity *next; + for( ent = GetTeamChain(); ent != NULL; ent = next ) { + next = ent->GetTeamChain(); + if ( ent->GetBindMaster() == this && ent->IsType( idEntityFx::Type ) ) { + ent->Unbind(); + if( !ent->fl.noRemoveWhenUnbound ) { + ent->PostEventMS( &EV_Remove, 0 ); + if (gameLocal.isClient) { + ent->Hide(); + } + } + next = GetTeamChain(); + } + } + +} + +void hhHunterSimple::Save( idSaveGame *savefile ) const { + beamLaser.Save( savefile ); + savefile->WriteVec3( nextMovePos ); + savefile->WriteAngles( kickAngles ); + savefile->WriteAngles( kickSpeed ); + savefile->WriteFloat( flashlightLength ); + sniperFx.Save( savefile ); + + ally.Save( savefile ); + muzzleFx.Save( savefile ); + flashLightFx.Save( savefile ); + savefile->WriteBool( bFlashLight ); + savefile->WriteInt( endSpeechTime ); + savefile->WriteInt( nextEnemyCheck ); + savefile->WriteFloat( lastEnemyDistance ); + savefile->WriteInt( lastChargeTime ); + savefile->WriteInt( nextVoiceTime ); + savefile->WriteVec3( initialOrigin ); + savefile->WriteInt( flashlightTime ); + enemyPortal.Save( savefile ); + savefile->WriteVec3( lastMoveOrigin ); + savefile->WriteInt( nextBlockCheckTime ); + savefile->WriteInt( nextSpiritCheck ); //HUMANHEAD jsh PCF 5/2/06 hunter combat fixes + + //HUMANHEAD jsh PCF 4/28/06 save nodelist + savefile->WriteInt(nodeList.Num()); + for (int i = 0; i < nodeList.Num(); i++) { + nodeList[i].Save(savefile); + } + //END HUMANHEAD +}; + +void hhHunterSimple::Restore( idRestoreGame *savefile ) { + beamLaser.Restore( savefile ); + savefile->ReadVec3( nextMovePos ); + savefile->ReadAngles( kickAngles ); + savefile->ReadAngles( kickSpeed ); + savefile->ReadFloat( flashlightLength ); + sniperFx.Restore( savefile ); + + ally.Restore( savefile ); + muzzleFx.Restore( savefile ); + flashLightFx.Restore( savefile ); + savefile->ReadBool( bFlashLight ); + savefile->ReadInt( endSpeechTime ); + savefile->ReadInt( nextEnemyCheck ); + savefile->ReadFloat( lastEnemyDistance ); + savefile->ReadInt( lastChargeTime ); + savefile->ReadInt( nextVoiceTime ); + savefile->ReadVec3( initialOrigin ); + savefile->ReadInt( flashlightTime ); + enemyPortal.Restore( savefile ); + savefile->ReadVec3( lastMoveOrigin ); + savefile->ReadInt( nextBlockCheckTime ); + savefile->ReadInt( nextSpiritCheck ); //HUMANHEAD jsh PCF 5/2/06 hunter combat fixes + + //HUMANHEAD jsh PCF 4/28/06 save nodelist + int num; + savefile->ReadInt(num); + nodeList.SetNum(num); + for (int i = 0; i < num; i++) { + nodeList[i].Restore(savefile); + } + //END HUMANHEAD + + alternateAccuracy = false; + enemyRushCount = 0; + enemyRetreatCount = 0; + enemyHiddenCount = 0; + currentAction.Clear(); + currentSpeech.Clear(); +}; + +void hhHunterSimple::Event_FlashLightOn() { + if ( !bFlashLight && gameLocal.time > flashlightTime && spawnArgs.GetBool( "can_flashlight" ) ) { + bFlashLight = true; + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_flashLight"), "fx_barrel", NULL, &MA_AssignFlashLightFx, false ); + } +} + +void hhHunterSimple::Event_FlashLightOff() { + bFlashLight = false; + SAFE_REMOVE( flashLightFx ); +} + +void hhHunterSimple::PrintDebug() { + hhMonsterAI::PrintDebug(); + gameLocal.Printf( " Ally: %s\n", ally.IsValid() ? ally->GetName() : "none" ); + gameLocal.Printf( " Allow Movement: %s\n", allowMove ? "yes" : "no" ); + gameLocal.Printf( " Turn rate : %f\n", turnRate ); + if ( nodeList.Num() ) { + gameLocal.Printf( " Cover Points:\n" ); + aasPath_t path; + int toAreaNum, areaNum; + for( int i=0;iGetOrigin() ); + areaNum = PointReachableAreaNum( physicsObj.GetOrigin() ); + if ( !toAreaNum || !PathToGoal( path, areaNum, physicsObj.GetOrigin(), toAreaNum, nodeList[i]->GetOrigin() ) ) { + common->Printf( " %s: " S_COLOR_RED "NOT REACHABLE\n", nodeList[i]->GetName() ); + } else { + common->Printf( " %s: " S_COLOR_GREEN " REACHABLE\n", nodeList[i]->GetName() ); + } + } + } + } +} + +bool hhHunterSimple::TurnToward( const idVec3 &pos ) { + idVec3 dir; + idVec3 local_dir; + float lengthSqr; + + if (AI_VEHICLE && InVehicle()) { + GetVehicleInterfaceLocal()->OrientTowards( pos, 0.5 ); + return true; + } + + dir = pos - physicsObj.GetOrigin(); +#ifdef HUMANHEAD //jsh wallwalk + physicsObj.GetAxis().ProjectVector( dir, local_dir ); +#else + physicsObj.GetGravityAxis().ProjectVector( dir, local_dir ); +#endif + local_dir.z = 0.0f; + lengthSqr = local_dir.LengthSqr(); + if ( lengthSqr > Square( 2.0f ) || ( lengthSqr > Square( 0.1f ) && enemy.GetEntity() == NULL ) ) { + //HUMANHEAD jsh PCF 4/29/06 changed to use enemy rather than player for directional movement + if ( !AI_DIR_MOVEMENT || AI_PATHING || !enemy.IsValid() ) { + AI_DIR_MOVEMENT = false; + ideal_yaw = idMath::AngleNormalize180( local_dir.ToYaw() ); + AI_DIR = HUNTER_N; + } else { + float toEnemy = (enemy->GetOrigin() - GetOrigin()).ToAngles().yaw; + float toPos = (pos - GetOrigin()).ToAngles().yaw; + if ( gameLocal.time > AI_NEXT_DIR_TIME && lengthSqr > Square( 2.0f ) ) { + float ang = toEnemy - toPos; + ang = idMath::AngleNormalize360( ang ); + AI_NEXT_DIR_TIME = gameLocal.time + SEC2MS(1); + if ( ang >= 135 && ang < 225 ) { + AI_DIR = HUNTER_S; + ideal_yaw = idMath::AngleNormalize180( local_dir.ToYaw() + 180 ); + } else if ( ang >= 45 && ang < 135 ) { + AI_DIR = HUNTER_W; + ideal_yaw = idMath::AngleNormalize180( local_dir.ToYaw() + 90 ); + } else if ( ang >= 225 && ang < 315 ) { + AI_DIR = HUNTER_E; + ideal_yaw = idMath::AngleNormalize180( local_dir.ToYaw() + 270 ); + } else { + AI_DIR = HUNTER_N; + ideal_yaw = idMath::AngleNormalize180( local_dir.ToYaw() ); + } + } + } + } + + bool result = FacingIdeal(); + return result; +} + +void hhHunterSimple::LinkScriptVariables( void ) { + hhMonsterAI::LinkScriptVariables(); + AI_DIR.LinkTo( scriptObject, "AI_DIR" ); + AI_DIR_MOVEMENT.LinkTo( scriptObject, "AI_DIR_MOVEMENT" ); + AI_LAST_DAMAGE_TIME.LinkTo( scriptObject, "AI_LAST_DAMAGE_TIME" ); + AI_ENEMY_RUSH.LinkTo( scriptObject, "AI_ENEMY_RUSH" ); + AI_ENEMY_RETREAT.LinkTo( scriptObject, "AI_ENEMY_RETREAT" ); + AI_ENEMY_HEALTH_LOW.LinkTo( scriptObject, "AI_ENEMY_HEALTH_LOW" ); + AI_ENEMY_RESURRECTED.LinkTo( scriptObject, "AI_ENEMY_RESURRECTED" ); + AI_ALLOW_ORDERS.LinkTo( scriptObject, "AI_ALLOW_ORDERS" ); + AI_ONGROUND.LinkTo( scriptObject, "AI_ONGROUND" ); + AI_ENEMY_SHOOTABLE.LinkTo( scriptObject, "AI_ENEMY_SHOOTABLE" ); + AI_ENEMY_LAST_SEEN.LinkTo( scriptObject, "AI_ENEMY_LAST_SEEN" ); + AI_SHOTBLOCK.LinkTo( scriptObject, "AI_SHOTBLOCK" ); + AI_NEXT_DIR_TIME.LinkTo( scriptObject, "AI_NEXT_DIR_TIME" ); + AI_BLOCKED_FAILSAFE.LinkTo( scriptObject, "AI_BLOCKED_FAILSAFE" ); + AI_KNOCKBACK.LinkTo( scriptObject, "AI_KNOCKBACK" ); +} + +void hhHunterSimple::Event_GetAlly() { + if ( ally.IsValid() ) { + idEntity *foo = ally.GetEntity(); + if ( ally->GetHealth() > 0 ) { + idThread::ReturnEntity( ally.GetEntity() ); + return; + } else { + ally.Clear(); + } + } + + idBounds bo( GetOrigin() ); + bo.ExpandSelf( 2000 ); + hhHunterSimple *entAI = NULL; + idLocationEntity *myLoc = NULL; + idLocationEntity *entLoc = NULL; + float bestDistSq = 9999999; + float distSq = 0; + entAI = static_cast(gameLocal.FindEntity( spawnArgs.GetString( "ally" ) )); + if ( entAI && entAI->ally.IsValid() ) { + entAI = NULL; + } + if ( !entAI ) { + hhHunterSimple *hunter = NULL; + for ( int i=0;i(hhMonsterAI::allSimpleMonsters[i]); + if ( !hunter || hunter == this || hunter->health <= 0 ) { + continue; + } + if ( !hunter->IsType( hhHunterSimple::Type ) || hunter->IsHidden() || hunter->ally.IsValid() ) { + continue; + } + if ( !hunter->GetEnemy() || hunter->GetEnemy() != GetEnemy() ) { + continue; + } + + //if both locations are null, it likely means this locations haven't been set + //i.e. dev test map so just let the hunters to ally + myLoc = gameLocal.LocationForPoint( GetOrigin() ); + entLoc = gameLocal.LocationForPoint( hunter->GetOrigin() ); + if ( myLoc != entLoc ) { + continue; + } + + distSq = (GetOrigin() - hunter->GetOrigin()).LengthSqr(); + if ( distSq > 2250000 ) { //less than 1500 + continue; + } + if ( distSq < bestDistSq ) { + entAI = hunter; + bestDistSq = distSq; + } + } + } + if ( entAI ) { + ally.Assign( entAI ); + entAI->ally.Assign( this ); + idThread::ReturnEntity( ally.GetEntity() ); + return; + } else { + ally.Clear(); + idThread::ReturnEntity( NULL ); + return; + } +} + +void hhHunterSimple::Event_PrintAction( const char *action ) { + currentAction = action; +} + +void hhHunterSimple::Event_EnemyCanSee() { + trace_t tr; + if ( !enemy.IsValid() ) { + idThread::ReturnInt( false ); + return; + } + + // Check if a trace succeeds from the player to the entity + gameLocal.clip.TracePoint( tr, enemy->GetEyePosition(), GetEyePosition(), MASK_MONSTERSOLID, enemy.GetEntity() ); + if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == this ) ) { + idThread::ReturnInt( true ); + return; + } + idThread::ReturnInt( false ); +} + +void hhHunterSimple::Event_TriggerDelay( idEntity *ent, float delay ) { + if ( !ent ) { + return; + } + if ( delay < 0.0f ) { + delay = 0.0f; + } + ent->PostEventSec( &EV_Activate, delay, this ); +} + +void hhHunterSimple::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef && damageDef->GetBool( "pain_knockback", "0" ) ) { + AI_KNOCKBACK = true; + } + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + AI_LAST_DAMAGE_TIME = MS2SEC(gameLocal.time); +} + +void hhHunterSimple::Event_CallBackup( float delay ) { + idEntity *entAI = gameLocal.FindEntity( spawnArgs.GetString( "backup" ) ); + if ( entAI && entAI->IsType( idAI::Type ) && entAI->spawnArgs.GetBool( "portal", "0" ) ) { + if ( delay < 0.0f ) { + delay = 0.0f; + } + entAI->PostEventSec( &EV_Activate, delay, this ); + } +} + +bool hhHunterSimple::CanSee( idEntity *ent, bool useFov ) { + trace_t tr; + idVec3 eye; + idVec3 toPos; + + if ( ent->IsHidden() ) { + return false; + } + + if ( ent->IsType( idActor::Type ) ) { + idActor *act = static_cast(ent); + + // If this actor is in a vehicle, look at the vehicle, not the actor + if(act->InVehicle()) { + ent = act->GetVehicleInterface()->GetVehicle(); + } + } + + if ( ent->IsType( idActor::Type ) ) { + toPos = ( ( idActor * )ent )->GetEyePosition(); + } else { + toPos = ent->GetPhysics()->GetOrigin(); + } + + if ( useFov && !CheckFOV( toPos ) ) { + return false; + } + + eye = GetEyePosition(); + + if ( InVehicle() ) { + gameLocal.clip.TracePoint( tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, GetVehicleInterface()->GetVehicle() ); // HUMANHEAD JRM + if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == ent ) ) { + return true; + } + } else { + gameLocal.clip.TracePoint( tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, this ); // HUMANHEAD JRM + + //if enemy is close to where the trace hit, he is shootable + if ( tr.fraction < 1.0f && enemy.IsValid() ) { + float hitDist = (GetEyePosition() - tr.endpos).LengthFast(); + float fullDist = (enemy->GetEyePosition() - GetEyePosition()).LengthFast(); + if ( fullDist > 0.0 ) { + if ( hitDist / fullDist > 0.75f ) { + AI_ENEMY_SHOOTABLE = true; + } else { + AI_ENEMY_SHOOTABLE = false; + } + } + } + + if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == ent ) ) { + return true; + } else if ( bSeeThroughPortals && aas ) { + shootTarget = NULL; + int myArea = gameRenderWorld->PointInArea( GetOrigin() ); + int numPortals = gameRenderWorld->NumGamePortalsInArea( myArea ); + if ( numPortals > 0 ) { + int enemyArea = gameRenderWorld->PointInArea( ent->GetOrigin() ); + for ( int i=0;iGetSoundPortal( myArea, i ).areas[0] == enemyArea ) { + //find the portal and set it as this monster's shoottarget + idEntity *spawnedEnt = NULL; + for( spawnedEnt = gameLocal.spawnedEntities.Next(); spawnedEnt != NULL; spawnedEnt = spawnedEnt->spawnNode.Next() ) { + if ( !spawnedEnt->IsType( hhPortal::Type ) ) { + continue; + } + if ( gameRenderWorld->PointInArea( spawnedEnt->GetOrigin() ) == myArea) { + shootTarget = spawnedEnt; + return true; + } + } + } + } + } + } + } + + return false; +} + +void hhHunterSimple::Event_SaySound( const char *soundName ) { + if ( AI_DEAD || ai_skipSpeech.GetBool() || gameLocal.time < nextVoiceTime ) { + return; + } + //if we've got an ally, clear out this sound so that he won't say it + if ( ally.IsValid() ) { + ally->spawnArgs.Set( soundName, "" ); + } + int time; + StartSound( soundName, ( s_channelType )SND_CHANNEL_VOICE2, 0, 0, &time ); + idThread::ReturnFloat( MS2SEC( time ) ); +} + +void hhHunterSimple::Event_OnProjectileLand(hhProjectile *proj) { + const function_t *newstate = NULL; + newstate = GetScriptFunction( "state_AvoidGrenade" ); + shootTarget = proj; + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +} + +void hhHunterSimple::Event_CheckRush() { + if ( enemy.IsValid() ) { + float currentDist = (enemy->GetOrigin() - GetOrigin()).Length(); + if ( currentDist < lastEnemyDistance ) { + //enemy is advancing + enemyRushCount++; + if ( enemyRushCount > spawnArgs.GetFloat( "charge_threshold", "3" ) ) { + if ( currentDist < spawnArgs.GetFloat( "rush_distance", "400" ) ) { + AI_ENEMY_RUSH = true; + } + } + } else { + enemyRushCount = 0; + AI_ENEMY_RUSH = false; + } + lastEnemyDistance = currentDist; + } + PostEventSec( &MA_CheckRush, spawnArgs.GetFloat( "rush_freq", "0.5" ) ); +} + +void hhHunterSimple::Event_SetNextVoiceTime( int nextTime ) { + nextVoiceTime = nextTime; +} + +void hhHunterSimple::Event_CheckRetreat() { + if ( enemy.IsValid() ) { + float currentDist = (enemy->GetOrigin() - GetOrigin()).Length(); + if ( currentDist > lastEnemyDistance ) { + //enemy is retreating + enemyRetreatCount++; + if ( enemyRetreatCount > spawnArgs.GetFloat( "retreat_threshold", "3" ) ) { + AI_ENEMY_RETREAT = true; + } + } else { + enemyRetreatCount = 0; + AI_ENEMY_RETREAT = false; + } + } + PostEventSec( &MA_CheckRetreat, spawnArgs.GetFloat( "retreat_freq", "0.5" ) ); +} + +void hhHunterSimple::Event_SayEnemyInfo() { + if ( AI_DEAD || !enemy.IsValid() || !ally.IsValid() || gameLocal.time < nextVoiceTime ) { + idThread::ReturnInt( false ); + return; + } + hhHunterSimple *hunterAlly = static_cast(ally.GetEntity()); + if ( !hunterAlly ) { + idThread::ReturnInt( false ); + return; + } + bool closerToEnemy = false; + idVec3 meToEnemy = enemy->GetOrigin() - GetOrigin(); + idVec3 allyToEnemy = enemy->GetOrigin() - ally->GetOrigin(); + float myDistToEnemy = meToEnemy.LengthFast(); + float allyDistToEnemy = allyToEnemy.LengthFast(); + float distToAlly = (ally->GetOrigin() - GetOrigin()).LengthFast(); + if ( myDistToEnemy < allyDistToEnemy ) { + closerToEnemy = true; + } + if ( !CanSee( enemy.GetEntity(), false ) ) { + hunterAlly->SetNextVoiceTime( gameLocal.time + SEC2MS(1) ); + Event_SaySound( "snd_behind_cover" ); + idThread::ReturnInt( true ); + return; + } + if ( closerToEnemy ) { + //check if enemy is underneath + if ( allyToEnemy.z < -spawnArgs.GetFloat( "z_down_dist", "75" ) ) { + if ( idMath::Fabs(meToEnemy.z) < spawnArgs.GetFloat( "z_equal_dist", "40" ) ) { + Event_SaySound( "snd_down_here" ); + hunterAlly->SetNextVoiceTime( gameLocal.time + SEC2MS(1) ); + idThread::ReturnInt( true ); + return; + } + } + if ( myDistToEnemy < spawnArgs.GetFloat( "over_there_dist", "400" ) ) { + Event_SaySound( "snd_over_here" ); + hunterAlly->SetNextVoiceTime( gameLocal.time + SEC2MS(1) ); + idThread::ReturnInt( true ); + return; + } + } else { + if ( myDistToEnemy > spawnArgs.GetFloat( "over_there_dist", "400" ) ) { + Event_SaySound( "snd_over_there" ); + hunterAlly->SetNextVoiceTime( gameLocal.time + SEC2MS(1) ); + idThread::ReturnInt( true ); + return; + } + } + idThread::ReturnInt( false ); +} + +bool hhHunterSimple::StartSound( const char *soundName, const s_channelType channel, int soundShaderFlags, bool broadcast, int *length ) { + const idSoundShader *shader; + const char *sound; + + if ( length ) { + *length = 0; + } + + // we should ALWAYS be playing sounds from the def. + // hardcoded sounds MUST be avoided at all times because they won't get precached. + assert( idStr::Icmpn( soundName, "snd_", 4 ) == 0 ); + + if ( !spawnArgs.GetString( soundName, "", &sound ) ) { + return false; + } + + if ( sound[0] == '\0' ) { + return false; + } + + if ( !gameLocal.isNewFrame ) { + // don't play the sound, but don't report an error + return true; + } + + // HUMANHEAD nla - Check if this sound should be played every X seconds + float minInterval; + int lastPlayed; + if ( spawnArgs.GetFloat( va( "min_%s", soundName ), "0", minInterval ) && minInterval > 0 ) { + if ( GetLastTimeSoundPlayed()->GetInt( soundName, "-1", lastPlayed ) ) { + if ( gameLocal.time - minInterval * 1000 < lastPlayed ) { + return( false ); + } + } + GetLastTimeSoundPlayed()->SetInt( soundName, gameLocal.time ); + } else if ( channel == SND_CHANNEL_VOICE2 ) { + //only play voice over sounds once for the hunter + if ( GetLastTimeSoundPlayed()->GetInt( soundName, "-1", lastPlayed ) ) { + if ( lastPlayed > 0 ) { + return( false ); + } + } + GetLastTimeSoundPlayed()->SetInt( soundName, gameLocal.time ); + } + // HUMANHEAD END + + shader = declManager->FindSound( sound ); + bool result; + result = StartSoundShader( shader, channel, soundShaderFlags, broadcast, length ); + if ( result && shader && channel == SND_CHANNEL_VOICE2 ) { + //print debug info for voice overs + currentSpeech = shader->GetName(); + endSpeechTime = gameLocal.time + SEC2MS(3); + } + return result; +} + +void hhHunterSimple::Event_GetCoverNode() { + float dist, bestDist; + idEntity *bestNode = NULL; + idEntity *node = NULL; + if ( !enemy.IsValid() || !EnemyPositionValid() ) { + idThread::ReturnEntity( NULL ); + return; + } + + //find the closest attack node that can see our enemy + bestNode = NULL; + bestDist = 9999999.0f; + idLocationEntity *myLocation = gameLocal.LocationForPoint( physicsObj.GetOrigin() ); + idLocationEntity *entLocation = NULL; + for( int i = 0; i < nodeList.Num(); i++ ) { + if ( !nodeList[i].IsValid() ) { + continue; + } + if ( !nodeList[i]->IsType( hhAINode::Type ) ) { + continue; + } + node = nodeList[ i ].GetEntity(); + if ( !node ) { + continue; + } + + //check if node is used + if ( node->IsType( hhAINode::Type ) ) { + hhAINode *aiNode = static_cast(node); + if ( aiNode && aiNode->user.IsValid() && !aiNode->user.IsEqualTo( this ) ) { + if ( !aiNode->user->IsHidden() && aiNode->user->GetHealth() > 0 ) { + continue; + } else { + aiNode->user.Clear(); + } + } + } + + //skip if node is farther than enemy or enemy is closer to object than i am + float distToNode = TravelDistance( GetOrigin(), node->GetOrigin() ); + float distEnemyToNode = (enemy->GetOrigin() - node->GetOrigin()).Length(); + if ( distEnemyToNode < distToNode || distEnemyToNode < 200 ) { + continue; + } + //skip if object is farther than enemy or if enemy is closer to object than i am + if ( distToNode > 1.0 && distToNode / distEnemyToNode > 2.0f ) { + continue; + } + + //skip if enemy can see the node + trace_t tr; + gameLocal.clip.TracePoint( tr, enemy->GetEyePosition(), node->GetOrigin(), MASK_SHOT_BOUNDINGBOX, this ); + if ( tr.fraction >= 1.0f ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorMagenta, enemy->GetEyePosition(), node->GetOrigin(), 30, 5000 ); + } + continue; + } + + //find closest node + idVec3 org = node->GetPhysics()->GetOrigin(); + dist = ( physicsObj.GetOrigin() - org ).LengthSqr(); + if ( dist > bestDist ) { + continue; + } + + //found a node that passed all tests + bestNode = node; + bestDist = dist; + } + if ( bestNode && bestNode->IsType( hhAINode::Type ) ) { + hhAINode *bestAINode = static_cast(bestNode); + if ( bestAINode ) { + bestAINode->user.Assign( this ); + } + } + if ( bestNode ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorGreen, enemy->GetEyePosition(), bestNode->GetOrigin(), 30, 5000 ); + } + idThread::ReturnVector( bestNode->GetOrigin() ); + return; + } + idThread::ReturnVector( vec3_zero ); +} + +void hhHunterSimple::Event_GetCoverPoint() { + if ( !enemy.IsValid() ) { + idThread::ReturnVector( vec3_zero ); + return; + } + //couldn't find a node. try sampling some nearby points + idVec3 testPoint; + float bestDist = 9999999.0f; + idVec3 finalPoint = vec3_zero; + int finalArea = -1; + bool clipped = false; + float yaw = (GetOrigin() - enemy->GetOrigin()).ToYaw(); + int num; + idClipModel *cm; + idClipModel *clipModels[ MAX_GENTITIES ]; + idBounds bounds; + trace_t tr; + float distance = spawnArgs.GetFloat( "cover_range", "200" ); + float initialYaw = yaw; + for ( int i=0;i<16;i++ ) { + testPoint = GetOrigin() + distance * idAngles( 0, yaw, 0 ).ToForward(); + testPoint.z += 35; + yaw += 22.5; + + if( gameLocal.clip.Contents( testPoint, GetPhysics()->GetClipModel(), viewAxis, CONTENTS_SOLID, this ) != 0 ) { + continue; + } + + //skip if node is farther than enemy or enemy is closer to object than i am + float distToNode = TravelDistance( GetOrigin(), testPoint ); + float distEnemyToNode = (enemy->GetOrigin() - testPoint).Length(); + if ( distEnemyToNode < distToNode || distEnemyToNode < 200 ) { + continue; + } + //skip if object is farther than enemy or if enemy is closer to object than i am + if ( distToNode > 1.0 && distToNode / distEnemyToNode > 2.0f ) { + continue; + } + + //make sure it wont clip into anything at testPoint + clipped = false; + bounds.FromTransformedBounds( GetPhysics()->GetBounds(), testPoint, GetPhysics()->GetAxis() ); + num = gameLocal.clip.ClipModelsTouchingBounds( bounds, MASK_MONSTERSOLID, clipModels, MAX_GENTITIES ); + for ( int j = 0; j < num; j++ ) { + cm = clipModels[ j ]; + // don't check render entities + if ( cm->IsRenderModel() ) { + continue; + } + idEntity *hit = cm->GetEntity(); + if ( ( hit == this ) || !hit->fl.takedamage ) { + continue; + } + if ( physicsObj.ClipContents( cm ) ) { + clipped = true; + break; + } + } + if ( clipped ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugBounds( colorRed, bounds, vec3_origin, 10000 ); + } + continue; + } + + //make sure we can path there + aasPath_t path; + int toAreaNum = PointReachableAreaNum( testPoint ); + int areaNum = PointReachableAreaNum( physicsObj.GetOrigin() ); + if ( !toAreaNum || !PathToGoal( path, areaNum, physicsObj.GetOrigin(), toAreaNum, testPoint ) ) { + continue; + } + + //make sure enemy cant see the point + gameLocal.clip.TracePoint( tr, enemy->GetEyePosition(), testPoint, MASK_SHOT_BOUNDINGBOX, this ); + if ( tr.fraction >= 1.0f ) { + continue; + } + + float distToPoint = (GetOrigin() - testPoint).Length(); + if ( distToPoint < bestDist ) { + finalPoint = testPoint; + bestDist = distToPoint; + finalArea = toAreaNum; + } + } + + if ( finalPoint != vec3_zero ) { + if ( aas ) { + aas->PushPointIntoAreaNum( finalArea, finalPoint ); + } + idThread::ReturnVector( finalPoint ); + return; + } + + idThread::ReturnVector( vec3_zero ); +} + +void hhHunterSimple::Event_GetSightNode() { + if ( !enemy.IsValid() ) { + idThread::ReturnVector( vec3_zero ); + return; + } + + //find a good spot to attack from + trace_t tr; + float distance = spawnArgs.GetFloat( "attack_range", "500" ); + idVec3 testPoint; + idVec3 finalPoint = vec3_zero; + bool clipped = false; + float yaw = (GetOrigin() - enemy->GetOrigin()).ToYaw(); + float initialYaw = yaw; + idEntity *bestNode = NULL; + idEntity *node = NULL; + + //find an attack node that can see our enemy + for( int i = 0; i < nodeList.Num(); i++ ) { + if ( !nodeList[i].IsValid() ) { + continue; + } + if ( !nodeList[i]->IsType( hhAINode::Type ) ) { + continue; + } + node = nodeList[ i ].GetEntity(); + if ( !node ) { + continue; + } + + //check if node is used + if ( node->IsType( hhAINode::Type ) ) { + hhAINode *aiNode = static_cast(node); + if ( aiNode && aiNode->user.IsValid() && !aiNode->user.IsEqualTo( this ) ) { + if ( !aiNode->user->IsHidden() && aiNode->user->GetHealth() > 0 ) { + continue; + } else { + aiNode->user.Clear(); + } + } + } + + //check if we can see enemy from this node + idVec3 eye = testPoint + ( GetPhysics()->GetGravityNormal() * -eyeOffset.z ); + //HUMANHEAD jsh PCF 4/27/06 changed trace to end at enemy origin instead of enemy eye position to fix slowdown + gameLocal.clip.TracePoint( tr, eye, enemy->GetOrigin(), MASK_SHOT_BOUNDINGBOX, enemy.GetEntity() ); + if ( tr.fraction < 1.0f || ( gameLocal.GetTraceEntity( tr ) != enemy.GetEntity() ) ) { + continue; + } + + //found a node that passed all tests + bestNode = node; + } + if ( bestNode && bestNode->IsType( hhAINode::Type ) ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorGreen, GetOrigin(), bestNode->GetOrigin(), 10, 5000 ); + } + idThread::ReturnVector( bestNode->GetOrigin() ); + return; + } + idThread::ReturnVector( vec3_zero ); +} + +void hhHunterSimple::Event_GetNearSightPoint() { + trace_t tr; + float distance = spawnArgs.GetFloat( "attack_range", "500" ); + idVec3 testPoint; + idVec3 finalPoint = vec3_zero; + bool clipped = false; + float yaw = 0; + float initialYaw = 0; + int num, i, j; + idClipModel *cm; + idClipModel *clipModels[ MAX_GENTITIES ]; + idBounds bounds; + int finalArea = -1; + idList listo; + + if ( !enemy.IsValid() ) { + idThread::ReturnVector( vec3_zero ); + return; + } + + yaw = (GetOrigin() - enemy->GetOrigin()).ToYaw(); + initialYaw = yaw; + //test 8 points around the monster, starting with one directly in front of it + for ( i=0;i<32;i++ ) { + testPoint = GetOrigin() + distance * idAngles( 0, yaw, 0 ).ToForward(); + testPoint.z += spawnArgs.GetFloat( "attack_z", "300" ); + yaw += 11.25; + + //make sure it wont clip into anything at testPoint + clipped = false; + bounds.FromTransformedBounds( GetPhysics()->GetBounds(), testPoint, GetPhysics()->GetAxis() ); + num = gameLocal.clip.ClipModelsTouchingBounds( bounds, MASK_MONSTERSOLID, clipModels, MAX_GENTITIES ); + for ( j = 0; j < num; j++ ) { + cm = clipModels[ j ]; + if ( cm->IsRenderModel() ) { + continue; + } + idEntity *hit = cm->GetEntity(); + if ( ( hit == this ) || !hit->fl.takedamage ) { + continue; + } + if ( physicsObj.ClipContents( cm ) ) { + clipped = true; + break; + } + } + if ( clipped ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugBounds( colorRed, bounds, vec3_origin, 10000 ); + } + continue; + } + + //make sure we can path there + int toAreaNum = PointReachableAreaNum( testPoint ); + int areaNum = PointReachableAreaNum( physicsObj.GetOrigin() ); + aasPath_t path; + if ( !toAreaNum || !PathToGoal( path, areaNum, physicsObj.GetOrigin(), toAreaNum, testPoint ) ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorYellow, GetOrigin(), testPoint, 10, 5000 ); + } + continue; + } + + //make sure we can see enemy there + idVec3 eye = testPoint + ( GetPhysics()->GetGravityNormal() * -eyeOffset.z ); + gameLocal.clip.TracePoint( tr, eye, enemy->GetEyePosition(), MASK_SHOT_BOUNDINGBOX, enemy.GetEntity() ); + if ( tr.fraction < 1.0f ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, eye, enemy->GetEyePosition(), 10, 5000 ); + } + continue; + } + + listo.Append( testPoint ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorGreen, GetOrigin(), testPoint, 10, 5000 ); + } + finalArea = toAreaNum; + } + if ( listo.Num() ) { + finalPoint = listo[gameLocal.random.RandomInt(listo.Num())]; + } + if ( finalPoint != vec3_zero ) { + if ( aas && finalArea != -1 ) { + aas->PushPointIntoAreaNum( finalArea, finalPoint ); + } + idThread::ReturnVector( finalPoint ); + return; + } + idThread::ReturnVector( vec3_zero ); +} + +void hhHunterSimple::Show() { + flashlightTime = gameLocal.time + SEC2MS( spawnArgs.GetInt( "flashlight_delay" ) ); + hhMonsterAI::Show(); +} + +void hhHunterSimple::Event_EnemyPortal( idEntity *ent ) { + //HUMANHEAD jsh PCF 4/27/06 Added check for enemy and hiddenness + if ( !enemy.IsValid() || AI_PATHING || fl.hidden ) { + return; + } + enemyPortal = ent; + SetState( GetScriptFunction( "state_PortalAttack" ) ); +} + +void hhHunterSimple::Event_GetEnemyPortal() { + if ( enemyPortal.IsValid() ) { + idThread::ReturnEntity( enemyPortal.GetEntity() ); + } else { + idThread::ReturnEntity( NULL ); + } +} + +void hhHunterSimple::Event_OnProjectileHit(hhProjectile *proj) { + if ( !enemy.IsValid() || !proj || AI_VEHICLE ) { + return; + } + if ( ( proj->GetOrigin().ToVec2() - GetOrigin().ToVec2() ).LengthFast() < 150 ) { + AI_SHOTBLOCK = true; + } +} + +void hhHunterSimple::BlockedFailSafe() { + if ( move.moveCommand < NUM_NONMOVING_COMMANDS || !ai_blockedFailSafe.GetBool() || blockedRadius < 0.0f || !enemy.IsValid() || AI_PATHING ) { + return; + } + if ( gameLocal.time > nextBlockCheckTime ) { + if ( ( lastMoveOrigin - physicsObj.GetOrigin() ).Length() < blockedRadius ) { + AI_BLOCKED_FAILSAFE = true; + } + lastMoveOrigin = physicsObj.GetOrigin(); + nextBlockCheckTime = gameLocal.time + blockedMoveTime; + } +} +void hhHunterSimple::UpdateEnemyPosition( void ) { + idActor *enemyEnt = enemy.GetEntity(); + int enemyAreaNum; + int areaNum; + aasPath_t path; + predictedPath_t predictedPath; + idVec3 enemyPos; + bool onGround; + + if ( !enemyEnt ) { + return; + } + + const idVec3 &org = physicsObj.GetOrigin(); + + if ( move.moveType == MOVETYPE_FLY ) { + enemyPos = enemyEnt->GetPhysics()->GetOrigin(); + onGround = true; + } else { + onGround = enemyEnt->GetFloorPos( 64.0f, enemyPos ); + if ( enemyEnt->OnLadder() ) { + onGround = false; + } + } + + if ( onGround ) { + // when we don't have an AAS, we can't tell if an enemy is reachable or not, + // so just assume that he is. + if ( !aas ) { + enemyAreaNum = 0; + lastReachableEnemyPos = enemyPos; + } else { + enemyAreaNum = PointReachableAreaNum( enemyPos, 1.0f ); + if ( enemyAreaNum ) { + areaNum = PointReachableAreaNum( org ); + if ( PathToGoal( path, areaNum, org, enemyAreaNum, enemyPos ) ) { + lastReachableEnemyPos = enemyPos; + } + } + } + } + + AI_ENEMY_IN_FOV = false; + AI_ENEMY_VISIBLE = false; + + if ( CanSee( enemyEnt, false ) ) { + AI_ENEMY_VISIBLE = true; + if ( CheckFOV( enemyEnt->GetPhysics()->GetOrigin() ) ) { + AI_ENEMY_IN_FOV = true; + } + + SetEnemyPosition(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorLtGrey, enemyEnt->GetPhysics()->GetBounds(), lastReachableEnemyPos, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorWhite, enemyEnt->GetPhysics()->GetBounds(), lastVisibleReachableEnemyPos, gameLocal.msec ); + } +} + +//HUMANHEAD jsh PCF 4/27/06 increased the upper z zalue of the bounds to 90.0f +bool hhHunterSimple::ReachedPos( const idVec3 &pos, const moveCommand_t moveCommand ) const { + if ( move.moveType == MOVETYPE_SLIDE ) { + idBounds bnds( idVec3( -4, -4.0f, -8.0f ), idVec3( 4.0f, 4.0f, 64.0f ) ); + bnds.TranslateSelf( physicsObj.GetOrigin() ); + if ( bnds.ContainsPoint( pos ) ) { + return true; + } + } else { + if ( ( moveCommand == MOVE_TO_ENEMY ) || ( moveCommand == MOVE_TO_ENTITY ) ) { + if ( physicsObj.GetAbsBounds().IntersectsBounds( idBounds( pos ).Expand( 8.0f ) ) ) { + return true; + } + } else { + idBounds bnds( idVec3( -16.0, -16.0f, -8.0f ), idVec3( 16.0, 16.0f, 90.0f ) ); + bnds.TranslateSelf( physicsObj.GetOrigin() ); + if ( bnds.ContainsPoint( pos ) ) { + return true; + } + } + } + return false; +} + +void hhHunterSimple::Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ) { + HH_ASSERT( player == enemy.GetEntity() ); + //HUMANHEAD jsh PCF 4/29/06 stop looking and make enemy not visible for a frame + Event_LookAtEntity( NULL, 0.0f ); + AI_ENEMY_VISIBLE = false; + enemy = proxy; + nextSpiritCheck = gameLocal.time + SEC2MS( spawnArgs.GetFloat( "spirit_check_delay", "5" ) ); +} + +void hhHunterSimple::Distracted( idActor *newEnemy ) { + if ( newEnemy && newEnemy->IsType( hhTalon::Type ) ) { + SetState( GetScriptFunction( "state_TalonCombat" ) ); + } else { + SetState( GetScriptFunction( "state_Combat" ) ); + } + + SetEnemy( newEnemy ); +} + +void hhHunterSimple::Event_EnemyVehicleDocked() { + if ( enemy.IsValid() && enemy->InVehicle() && enemy->GetVehicleInterface() ) { + hhVehicle *vehicle = enemy->GetVehicleInterface()->GetVehicle(); + if ( vehicle && vehicle->IsDocked() ) { + idThread::ReturnInt( true ); + return; + } + } + idThread::ReturnInt( false ); +} + +void hhHunterSimple::Event_Blocked() { + AI_NEXT_DIR_TIME = 0; + StopMove( MOVE_STATUS_DONE ); +} \ No newline at end of file diff --git a/src/Prey/ai_hunter_simple.h b/src/Prey/ai_hunter_simple.h new file mode 100644 index 0000000..3e1c199 --- /dev/null +++ b/src/Prey/ai_hunter_simple.h @@ -0,0 +1,128 @@ + +#ifndef __PREY_AI_HUNTER_SIMPLE_H__ +#define __PREY_AI_HUNTER_SIMPLE_H__ + +#define HUNTER_N 0 +#define HUNTER_NE 1 +#define HUNTER_E 2 +#define HUNTER_SE 3 +#define HUNTER_S 4 +#define HUNTER_SW 5 +#define HUNTER_W 6 +#define HUNTER_NW 7 + +class hhHunterSimple : public hhMonsterAI { + +public: + + CLASS_PROTOTYPE(hhHunterSimple); + + hhHunterSimple(); + void Event_OnProjectileLaunch(hhProjectile *proj); + void Event_OnProjectileHit(hhProjectile *proj); + void Event_LaserOn(); + void Event_LaserOff(); + void Event_FindReaction( const char* effect ); + + void Spawn(); + void Think(); + idProjectile *LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ); + void AnimMove(); + bool UpdateAnimationControllers(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void PrintDebug(); + virtual void LinkScriptVariables(); + bool TurnToward( const idVec3 &pos ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + bool StartSound( const char *soundName, const s_channelType channel, int soundShaderFlags, bool broadcast, int *length ); + void SetNextVoiceTime( int newTime ) { nextVoiceTime = newTime; } + void Show(); + void BlockedFailSafe(); + void UpdateEnemyPosition(); + void Distracted( idActor *newEnemy ); + bool ReachedPos( const idVec3 &pos, const moveCommand_t moveCommand ) const; + + void Event_EscapePortal(); + void Event_AssignSniperFx( hhEntityFx* fx ); + void Event_AssignMuzzleFx( hhEntityFx* fx ); + void Event_AssignFlashLightFx( hhEntityFx* fx ); + void Event_KickAngle( const idAngles &ang ); + void Event_FlashLightOn(); + void Event_FlashLightOff(); + void Event_PostSpawn(); + void Event_EnemyCanSee(); + void Event_PrintAction( const char *action ); + void Event_GetAlly(); + void Event_TriggerDelay( idEntity *ent, float delay ); + void Event_CallBackup( float delay ); + virtual bool CanSee( idEntity *ent, bool useFov ); + void Event_GetAdvanceNode(); + void Event_GetRetreatNode(); + void Event_OnProjectileLand(hhProjectile *proj); + void Event_SaySound( const char *soundName ); + void Event_CheckRush(); + void Event_CheckRetreat(); + void Event_SayEnemyInfo(); + void Event_SetNextVoiceTime( int nextTime ); + void Event_GiveOrders( const char *orders ); + void Event_AllowOrders( bool orders ); + void Event_GetCoverNode(); + void Event_GetCoverPoint(); + void Event_GetSightNode(); + void Event_GetNearSightPoint(); + void Event_Blocked(); + void Event_EnemyPortal( idEntity *portal ); + void Event_GetEnemyPortal(); + void Event_EnemyVehicleDocked(); + void Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ); //HUMANHEAD jsh PCF 5/2/06 hunter combat fixes + + idEntityPtr ally; + idScriptBool AI_ALLOW_ORDERS; + idScriptBool AI_SHOTBLOCK; +protected: + idVec3 nextMovePos; + idEntityPtr beamLaser; + idAngles kickAngles; + idAngles kickSpeed; + idEntityPtr sniperFx; + idEntityPtr muzzleFx; + idEntityPtr flashLightFx; + idList< idEntityPtr< idEntity > > nodeList; + bool alternateAccuracy; + bool bFlashLight; + idScriptInt AI_DIR; + idScriptFloat AI_LAST_DAMAGE_TIME; + idStr currentAction; + idStr currentSpeech; + int endSpeechTime; + idScriptBool AI_DIR_MOVEMENT; + idScriptBool AI_ENEMY_RUSH; + idScriptBool AI_ENEMY_RETREAT; + idScriptBool AI_ENEMY_HEALTH_LOW; + idScriptBool AI_ENEMY_RESURRECTED; + idScriptBool AI_ONGROUND; + idScriptBool AI_ENEMY_SHOOTABLE; + idScriptBool AI_BLOCKED_FAILSAFE; + idScriptInt AI_ENEMY_LAST_SEEN; + idScriptInt AI_NEXT_DIR_TIME; + idScriptBool AI_KNOCKBACK; + int nextEnemyCheck; + float lastEnemyDistance; + int enemyRushCount; + int enemyRetreatCount; + int enemyHiddenCount; + int lastChargeTime; + int nextVoiceTime; + idVec3 initialOrigin; + float flashlightLength; + int flashlightTime; + idEntityPtr enemyPortal; + idVec3 lastMoveOrigin; + int nextBlockCheckTime; + int nextSpiritCheck; //HUMANHEAD jsh PCF 5/2/06 hunter combat fixes +}; + +#endif /* __PREY_AI_HUNTER_SIMPLE_H__ */ + diff --git a/src/Prey/ai_inspector.cpp b/src/Prey/ai_inspector.cpp new file mode 100644 index 0000000..7e1a9dd --- /dev/null +++ b/src/Prey/ai_inspector.cpp @@ -0,0 +1,136 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef MA_CheckReactions( "", "e" ); + +CLASS_DECLARATION( hhMonsterAI, hhAIInspector ) + EVENT( EV_PostSpawn, hhAIInspector::Event_PostSpawn ) + EVENT( MA_CheckReactions, hhAIInspector::Event_CheckReactions ) +END_CLASS + +void hhAIInspector::Spawn() { +} + +void hhAIInspector::Event_PostSpawn() { + const function_t* func = GetScriptFunction( "state_InspectorIdle" ); + if( !func ) { + gameLocal.Warning( "unable to find state_InspectorIdle" ); + return; + } + SetState( func ); +} + +void hhAIInspector::CheckReactions( idEntity* entity ) { + PostEventMS( &MA_CheckReactions, 1000, entity ); +} + +void hhAIInspector::Event_CheckReactions( idEntity* entity ) { + checkReaction = entity; + targetReaction.entity = entity; + targetReaction.reactionIndex = 0; + if( !targetReaction.GetReaction() ) { + targetReaction.reactionIndex = -1; + gameLocal.Warning( "unable to properly use requested reaction... possibly no reactions on this entity" ); + return; + } + + const function_t* func = GetScriptFunction( "state_InspectorReactions" ); + if( !func ) { + gameLocal.Warning( "unable to find checkreaction state on inspector..." ); + return; + } + SetState( func ); +} + +void hhAIInspector::RestartInspector( const char* inspectClass, idEntity* newReaction, idPlayer* starter ) { + float yaw; + idVec3 org; + idDict spawnDict; + idEntity* react_scout; + idEntity* use_reaction = NULL; + +//Find our dictionary to use + const idDict* edict = gameLocal.FindEntityDefDict( inspectClass, false ); + if( !edict ) { + gameLocal.Printf( "Unable to find entitfydef '%s'", inspectClass ); + return; + } + spawnDict = *edict; //copy the values to an editable dict + +//Setup values for position/orientation + yaw = starter->viewAngles.yaw; + org = starter->GetPhysics()->GetOrigin() + idAngles( 0, yaw, 0 ).ToForward() * 180 + idVec3( 0, 0, 1 ); + +// Set dictionary values... + spawnDict.Set( "angle", va("%f", yaw + 180) ); + spawnDict.Set( "origin", org.ToString() ); + spawnDict.Set( "spawnClass", "hhAIInspector" ); + +// Perform actual spawn + react_scout = static_cast(gameLocal.SpawnEntityType(hhAIInspector::Type, &spawnDict)); + if( !react_scout ) { + gameLocal.Printf( "failed to spawn inspector monster..." ); + return; + } + + use_reaction = newReaction; + + if( gameLocal.inspector != NULL ) { +//save our old use_reaction if we haven't specified one + if( !use_reaction && gameLocal.inspector.IsValid() && gameLocal.inspector->checkReaction.IsValid() ) { + use_reaction = gameLocal.inspector->checkReaction.GetEntity(); + } +// Remove our old inspector + gameLocal.inspector->PostEventMS( &EV_Remove, 0 ); + } + gameLocal.inspector = react_scout; + if( use_reaction ) { + gameLocal.inspector->CheckReactions( use_reaction ); + } +} + +void hhAIInspector::RestartInspector( idEntity* newReaction, idPlayer* starter ) { + idEntity* react_scout; + float yaw; + idVec3 org; + + yaw = starter->viewAngles.yaw; + org = starter->GetPhysics()->GetOrigin() + idAngles( 0, yaw, 0 ).ToForward() * 180 + idVec3( 0, 0, 1 ); + + idDict dict = gameLocal.inspector.GetEntity()->spawnArgs; + + dict.Set( "angle", va("%f", yaw + 180) ); + dict.Set( "origin", org.ToString() ); + react_scout = static_cast(gameLocal.SpawnEntityType(hhAIInspector::Type, &dict)); + if( !react_scout ) { + gameLocal.Printf( "failed to spawn inspector monster..." ); + return; + } + + if( gameLocal.inspector != NULL ) { + gameLocal.inspector->PostEventMS( &EV_Remove, 0 ); + } + gameLocal.inspector = react_scout; + gameLocal.inspector->CheckReactions( newReaction ); +} + +/* +===================== +hhAIInspector::Save +===================== +*/ +void hhAIInspector::Save( idSaveGame *savefile ) const { + checkReaction.Save( savefile ); +} + +/* +===================== +hhAIInspector::Restore +===================== +*/ +void hhAIInspector::Restore( idRestoreGame *savefile ) { + checkReaction.Restore( savefile ); +} + diff --git a/src/Prey/ai_inspector.h b/src/Prey/ai_inspector.h new file mode 100644 index 0000000..1675bfe --- /dev/null +++ b/src/Prey/ai_inspector.h @@ -0,0 +1,28 @@ +#ifndef __PREY_AI_INSPECTOR_H__ +#define __PREY_AI_INSPECTOR_H__ + +class hhAIInspector : public hhMonsterAI { + +public: + CLASS_PROTOTYPE(hhAIInspector); + +public: + void Spawn(); + void CheckReactions( idEntity* entity ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + static void RestartInspector( const char* inspectClass, idEntity* newReaction, idPlayer* starter ); + static void RestartInspector( idEntity* newReaction, idPlayer* starter ); + +public: +// Events. + void Event_PostSpawn(); + void Event_CheckReactions( idEntity* entity ); + +protected: + idEntityPtr checkReaction; + +}; + +#endif diff --git a/src/Prey/ai_jetpack_harvester_simple.cpp b/src/Prey/ai_jetpack_harvester_simple.cpp new file mode 100644 index 0000000..9246289 --- /dev/null +++ b/src/Prey/ai_jetpack_harvester_simple.cpp @@ -0,0 +1,662 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef AI_RemoveJetFx("removeJetFx"); +const idEventDef AI_CreateJetFx("createJetFx"); +const idEventDef AI_LaunchMine("launchMine", "d"); +const idEventDef MA_IsEnemySniping("isEnemySniping", NULL, 'd'); +const idEventDef MA_NumMines("numMines", NULL, 'f'); +const idEventDef MA_DodgeProjectile("", "e"); +const idEventDef MA_AppendHoverFx( "", "e" ); + +CLASS_DECLARATION( hhMonsterAI, hhJetpackHarvesterSimple ) + EVENT( AI_RemoveJetFx, hhJetpackHarvesterSimple::Event_RemoveJetFx ) + EVENT( EV_Activate, hhJetpackHarvesterSimple::Event_Activate) + EVENT( AI_CreateJetFx, hhJetpackHarvesterSimple::Event_CreateJetFx ) + EVENT( AI_LaunchMine, hhJetpackHarvesterSimple::Event_LaunchMine ) + EVENT( EV_Broadcast_AppendFxToList, hhJetpackHarvesterSimple::Event_AppendToThrusterList ) + EVENT( MA_OnProjectileLaunch, hhJetpackHarvesterSimple::Event_OnProjectileLaunch ) + EVENT( MA_DodgeProjectile, hhJetpackHarvesterSimple::Event_DodgeProjectile ) + EVENT( MA_IsEnemySniping, hhJetpackHarvesterSimple::Event_IsEnemySniping ) + EVENT( MA_NumMines, hhJetpackHarvesterSimple::Event_NumMines ) + EVENT( MA_AppendHoverFx, hhJetpackHarvesterSimple::Event_AppendHoverFx ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +hhJetpackHarvesterSimple::~hhJetpackHarvesterSimple() { + for ( int i=0;iGetPhysics()->GetAxis(); + collision.endpos = mineList[i]->GetPhysics()->GetOrigin(); + collision.c.point = mineList[i]->GetPhysics()->GetOrigin(); + collision.c.normal.Set( 0, 0, 1 ); + mineList[i]->Explode( &collision, idVec3(0,0,0), 0 ); + } + } +} + +void hhJetpackHarvesterSimple::Spawn(void) { + lastAntiProjectileAttack = gameLocal.GetTime(); + for(int i=0;iGetJointWorldTransform( bones[i], bonePos, boneAxis ); + fxInfo.SetEntity( this ); + fxInfo.SetBindBone( bones[i] ); + fxInfo.Toggle(); + fxInfo.RemoveWhenDone( false ); + + if( fxThrusters[ThrustType_Idle][i] != NULL ) { + gameLocal.Warning("Jetpack harvester already had flamejet made."); + } + + BroadcastFxInfo( psystem, bonePos, boneAxis, &fxInfo, &EV_Broadcast_AppendFxToList ); + } + } + + psystem = spawnArgs.GetString( "fx_thruster_hover" ); + if ( psystem && *psystem ) { + for(int i=0;i* parmList ) { + if( !parmList || parmList->Num() != 2 ) { + return; + } + + idVec3 jointPos; + idMat3 jointAxis; + const char* projectileDefName = (*parmList)[0].c_str(); + const char* jointName = (*parmList)[1].c_str(); + + if( !projectileDefName || !projectileDefName[0] ) { + return; + } + + if( !GetJointWorldTransform(jointName, jointPos, jointAxis) ) { + return; + } + + hhHarvesterMine* mine = static_cast( gameLocal.SpawnObject(projectileDefName) ); + mine->Create( this, jointPos, jointAxis ); + mine->Launch( jointPos, jointAxis, physicsObj.GetPushedLinearVelocity() ); +} + +void hhJetpackHarvesterSimple::Hide( void ) { + hhMonsterAI::Hide(); + + for(int i=0;iNozzle(FALSE); + } + if(fxThrusters[ThrustType_Hover][i].GetEntity()) { + fxThrusters[ThrustType_Hover][i]->Nozzle(FALSE); + } + } +} + +void hhJetpackHarvesterSimple::Show( void ) { + hhMonsterAI::Show(); + + for(int i=0;iNozzle(TRUE); + } + if(fxThrusters[ThrustType_Hover][i].GetEntity()) { + fxThrusters[ThrustType_Hover][i]->Nozzle(FALSE); + } + } +} + +idProjectile *hhJetpackHarvesterSimple::LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { //HUMANHEAD mdc - added desiredProjectileDef for supporting multiple projs. + idVec3 muzzle; + idVec3 dir; + idVec3 start; + trace_t tr; + idBounds projBounds; + float distance; + const idClipModel *projClip; + float attack_accuracy; + float attack_cone; + float projectile_spread; + float diff; + float angle; + float spin; + idAngles ang; + int num_projectiles; + int i; + idMat3 axis; + idVec3 tmp; + idProjectile *lastProjectile; + + //HUMANHEAD mdc - added to support multiple projectiles + if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile + int projIndex = FindProjectileInfo( desiredProjectileDef ); + if( projIndex >= 0 ) { + SetCurrentProjectile( projIndex ); + } + } + //HUMANHEAD END + + + if ( !projectileDef ) { + gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); + return NULL; + } + + attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); + attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); + projectile_spread = desiredProjectileDef->GetFloat( "projectile_spread", "0" ); + num_projectiles = desiredProjectileDef->GetInt( "num_projectiles", "1" ); + idVec3 launch_velocity = vec3_zero; + if ( desiredProjectileDef->GetFloat( "launch_y", "0" ) > 0.0f ) { + if ( gameLocal.random.RandomFloat() < 0.5 ) { + launch_velocity.y = -desiredProjectileDef->GetFloat( "launch_y", "0" ); + } else { + launch_velocity.y = desiredProjectileDef->GetFloat( "launch_y", "0" ); + } + launch_velocity *= viewAxis; + } + + GetMuzzle( jointname, muzzle, axis ); + + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, axis[ 0 ] ); + } + + lastProjectile = projectile.GetEntity(); + + if ( target != NULL ) { + tmp = target->GetPhysics()->GetAbsBounds().GetCenter() - muzzle; + tmp.Normalize(); + axis = tmp.ToMat3(); + } else { + axis = viewAxis; + } + + // rotate it because the cone points up by default + tmp = axis[2]; + axis[2] = axis[0]; + axis[0] = -tmp; + + // make sure the projectile starts inside the monster bounding box + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + projClip = lastProjectile->GetPhysics()->GetClipModel(); + projBounds = projClip->GetBounds().Rotate( axis ); + + // check if the owner bounds is bigger than the projectile bounds + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { + start = muzzle + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); + muzzle = tr.endpos; + + // set aiming direction + GetAimDir( muzzle, target, this, dir ); + ang = dir.ToAngles(); + + // adjust his aim so it's not perfect. uses sine based movement so the tracers appear less random in their spread. + float t = MS2SEC( gameLocal.time + entityNumber * 497 ); + ang.yaw += idMath::Sin16( t * 6.7 ) * attack_accuracy; + + if ( clampToAttackCone ) { + // clamp the attack direction to be within monster's attack cone so he doesn't do + // things like throw the missile backwards if you're behind him + diff = idMath::AngleDelta( ang.yaw, current_yaw ); + if ( diff > attack_cone ) { + ang.yaw = current_yaw + attack_cone; + } else if ( diff < -attack_cone ) { + ang.yaw = current_yaw - attack_cone; + } + } + + axis = ang.ToMat3(); + + float spreadRad = DEG2RAD( projectile_spread ); + for( i = 0; i < num_projectiles; i++ ) { + // spread the projectiles out + angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); + spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); + //dir = axis[ 0 ] + axis[ 2 ] * ( 0 * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir = axis[ 0 ] - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir.Normalize(); + + // launch the projectile + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, dir ); + } + lastProjectile = projectile.GetEntity(); + lastProjectile->Launch( muzzle, dir, launch_velocity ); + projectile = NULL; + if ( projectileDef->GetBool( "mine", "0" ) ) { + idEntityPtr &entityPtr = mineList.Alloc(); + entityPtr = lastProjectile; + } + } + + TriggerWeaponEffects( muzzle, axis ); + + lastAttackTime = gameLocal.time; + +//HUMANHEAD mdc - added to support multiple projectiles + projectile = NULL; + SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side +//HUMANHEAD END + + return lastProjectile; +} + +void hhJetpackHarvesterSimple::AnimMove( void ) { + idVec3 goalPos; + idVec3 delta; + idVec3 goalDelta; + float goalDist; + monsterMoveResult_t moveResult; + idVec3 newDest; + + idVec3 oldorigin = physicsObj.GetOrigin(); +#ifdef HUMANHEAD //jsh wallwalk + idMat3 oldaxis = GetGravViewAxis(); +#else + idMat3 oldaxis = viewAxis; +#endif + + physicsObj.UseFlyMove( false ); + + AI_BLOCKED = false; + + if ( move.moveCommand < NUM_NONMOVING_COMMANDS ){ + move.lastMoveOrigin.Zero(); + move.lastMoveTime = gameLocal.time; + } + + move.obstacle = NULL; + if ( ( move.moveCommand == MOVE_FACE_ENEMY ) && enemy.GetEntity() ) { + TurnToward( lastVisibleEnemyPos ); + goalPos = oldorigin; + } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + goalPos = oldorigin; + } else if ( GetMovePos( goalPos ) ) { + if ( move.moveCommand != MOVE_WANDER ) { + CheckObstacleAvoidance( goalPos, newDest ); + TurnToward( newDest ); + } else { + TurnToward( goalPos ); + } + } + + Turn(); + + if ( move.moveCommand == MOVE_SLIDE_TO_POSITION ) { + if ( gameLocal.time < move.startTime + move.duration ) { + goalPos = move.moveDest - move.moveDir * MS2SEC( move.startTime + move.duration - gameLocal.time ); + delta = goalPos - oldorigin; + delta.z = 0.0f; + } else { + delta = move.moveDest - oldorigin; + delta.z = 0.0f; + StopMove( MOVE_STATUS_DONE ); + } + } else if ( allowMove ) { +#ifdef HUMANHEAD //jsh wallwalk + GetMoveDelta( oldaxis, GetGravViewAxis(), delta ); +#else + GetMoveDelta( oldaxis, viewAxis, delta ); +#endif + } else { + delta.Zero(); + } + + if ( move.moveCommand == MOVE_TO_POSITION ) { + goalDelta = move.moveDest - oldorigin; + goalDist = goalDelta.LengthFast(); + if ( goalDist < delta.LengthFast() ) { + delta = goalDelta; + } + } + + delta *= spawnArgs.GetFloat( "fly_scale", "1.0" ); + + physicsObj.SetDelta( delta ); + physicsObj.ForceDeltaMove( disableGravity ); + + RunPhysics(); + + if ( ai_debugMove.GetBool() ) { + // HUMANHEAD JRM - so we can see if grav is on or off + if(disableGravity) { + gameRenderWorld->DebugLine( colorRed, oldorigin, physicsObj.GetOrigin(), 5000 ); + } else { + gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 5000 ); + } + } + + moveResult = physicsObj.GetMoveResult(); + if ( !af_push_moveables && attack.Length() && TestMelee() ) { + DirectDamage( attack, enemy.GetEntity() ); + } else { + idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); + if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + } + } + + BlockedFailSafe(); + + AI_ONGROUND = physicsObj.OnGround(); + + idVec3 org = physicsObj.GetOrigin(); + if ( oldorigin != org ) { + TouchTriggers(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); + gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); + DrawRoute(); + } +} + +void hhJetpackHarvesterSimple::Event_NumMines() { + //we only want valid mines, so remove dead ones + for ( int i=0;ihealth <= 0 ) { + mineList.RemoveIndex(i); + } + } + idThread::ReturnFloat( float(mineList.Num()) ); +} + +void hhJetpackHarvesterSimple::Event_DodgeProjectile( hhProjectile *proj ) { + if ( !proj || !enemy.IsValid() ) { + return; + } + float min = spawnArgs.GetFloat( "dda_dodge_min", "0.3" ); + float max = spawnArgs.GetFloat( "dda_dodge_max", "0.8" ); + + float dodgeChance = (min + (max-min)*gameLocal.GetDDAValue() ); + + if ( ai_debugBrain.GetBool() ) { + gameLocal.Printf( "%s dodge chance %f\n", GetName(), dodgeChance ); + } + if ( gameLocal.random.RandomFloat() > dodgeChance ) { + return; + } + + idVec3 fw = viewAxis[0]; + idVec3 projFw = proj->GetAxis()[0]; + if(proj->GetOwner()) + projFw = proj->GetOwner()->GetAxis()[0]; + float dot = fw * projFw; + if(dot > -.7f) + return; + const function_t *newstate = NULL; + + //determine which side to dodge to + idVec3 povPos, targetPos; + povPos = enemy->GetPhysics()->GetOrigin(); + targetPos = GetPhysics()->GetOrigin(); + idVec3 povToTarget = targetPos - povPos; + povToTarget.z = 0.f; + povToTarget.Normalize(); + idVec3 povLeft, povUp; + povToTarget.OrthogonalBasis(povLeft, povUp); + povLeft.Normalize(); + idVec3 projVel = proj->GetPhysics()->GetLinearVelocity(); + projVel.Normalize(); + dot = povLeft * projVel; + if ( dot < 0 ) { + newstate = GetScriptFunction( "state_DodgeRight" ); + } else { + newstate = GetScriptFunction( "state_DodgeLeft" ); + } + if ( newstate ) { + lastAntiProjectileAttack = gameLocal.GetTime(); + SetState( newstate ); + UpdateScript(); + } +} + +void hhJetpackHarvesterSimple::Event_OnProjectileLaunch( hhProjectile *proj ) { + if ( !proj || !spawnArgs.GetBool( "can_dodge", "1" ) || health <= 0 ) { + return; + } + if ( gameLocal.GetTime() - lastAntiProjectileAttack < 1000 * int(spawnArgs.GetFloat( "dodge_freq", "2.0" )) ) { + return; + } + if( proj->GetOwner() && !(ReactionTo(proj->GetOwner()) & (ATTACK_ON_SIGHT | ATTACK_ON_DAMAGE)) ) { + return; // The person who launched this projectile wasn't someone to worry about + } + if ( !enemy.IsValid() ) { + return; + } + PostEventSec( &MA_DodgeProjectile, spawnArgs.GetFloat( "dodge_delay", "0.5" ), proj ); +} + +bool hhJetpackHarvesterSimple::Collide( const trace_t &collision, const idVec3 &velocity ) { + if ( af.IsActive() && !IsHidden() && !freezeDamage && !specialDamage ) { + const char *fx = spawnArgs.GetString("fx_death"); + if ( fx && *fx ) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone(true); + BroadcastFxInfo(fx, GetOrigin(), GetAxis(), &fxInfo); + Hide(); + PostEventMS(&EV_Remove, 1000); + } + } + + return false; +} + +void hhJetpackHarvesterSimple::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if (!frozen) { + // Being tractored can mess up predeath + fl.canBeTractored = false; + } + HandleNoGore(); + if ( AI_DEAD ) { + AI_PAIN = true; + AI_DAMAGE = true; + return; + } + + //stop rocket fx and sounds + StopSound( SND_CHANNEL_BODY3, false ); + for(int i=0;iGetGravityNormal() * 800, MASK_MONSTERSOLID, this); + if( TraceInfo.fraction < 1.0f ) { + if ( HasPathTo( TraceInfo.endpos ) ) { + Event_SetMoveType( MOVETYPE_ANIM ); + health += 30; + state = GetScriptFunction( "state_StartPreDeath" ); + SetState( state ); + SetWaitState( "" ); + return; + } + } + } + + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + + //explode creature's mines upon death + for ( int i=0;iGetPhysics()->GetAxis(); + collision.endpos = mineList[i]->GetPhysics()->GetOrigin(); + collision.c.point = mineList[i]->GetPhysics()->GetOrigin(); + collision.c.normal.Set( 0, 0, 1 ); + mineList[i]->Explode( &collision, idVec3(0,0,0), 0 ); + } + } +} + +void hhJetpackHarvesterSimple::Event_IsEnemySniping(void) { + idThread::ReturnInt( false ); +} + +/* +===================== +hhJetpackHarvesterSimple::Save +===================== +*/ +void hhJetpackHarvesterSimple::Save( idSaveGame *savefile ) const { + int i; + for ( i = 0; i < ThrustSide_Total; i++ ) { + for ( int j = 0; j < ThrustType_Total; j++ ) { + fxThrusters[i][j].Save( savefile ); + } + } + + savefile->WriteInt( lastAntiProjectileAttack ); + savefile->WriteBool( allowPreDeath ); + + int num = mineList.Num(); + savefile->WriteInt( num ); + savefile->WriteBool( specialDamage ); + for ( i = 0; i < num; i++ ) { + mineList[i].Save( savefile ); + } +} + +/* +===================== +hhJetpackHarvesterSimple::Restore +===================== +*/ +void hhJetpackHarvesterSimple::Restore( idRestoreGame *savefile ) { + int i; + for ( i = 0; i < ThrustSide_Total; i++ ) { + for ( int j = 0; j < ThrustType_Total; j++ ) { + fxThrusters[i][j].Restore( savefile ); + } + } + + savefile->ReadInt( lastAntiProjectileAttack ); + savefile->ReadBool( allowPreDeath ); + + int num; + savefile->ReadInt( num ); + savefile->ReadBool( specialDamage ); + mineList.SetNum( num ); + for ( i = 0; i < num; i++ ) { + mineList[i].Restore( savefile ); + } +} + +void hhJetpackHarvesterSimple::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( !AI_DEAD ) { + if ( damageDef && damageDef->GetBool( "ice" ) && spawnArgs.GetBool( "can_freeze", "0" ) ) { + freezeDamage = true; + allowPreDeath = false; + } else { + freezeDamage = false; + } + if ( damageDef && damageDef->GetBool( "no_special_death" ) ) { + specialDamage = true; + allowPreDeath = false; + } + } + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_jetpack_harvester_simple.h b/src/Prey/ai_jetpack_harvester_simple.h new file mode 100644 index 0000000..e8d0f7a --- /dev/null +++ b/src/Prey/ai_jetpack_harvester_simple.h @@ -0,0 +1,66 @@ + +#ifndef __PREY_AI_JETPACK_HARVESTER_SIMPLE_H__ +#define __PREY_AI_JETPACK_HARVESTER_SIMPLE_H__ + + +class hhJetpackHarvesterSimple : public hhMonsterAI { + +public: + CLASS_PROTOTYPE(hhJetpackHarvesterSimple); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_AppendHoverFx( hhEntityFx* fx ) {}; + void Event_AppendToThrusterList( hhEntityFx* fx ) {}; + void Event_RemoveJetFx(void) {}; + void Event_CreateJetFx(void) {}; + void Event_LaunchMine( const idList* parmList ) {}; + void Event_OnProjectileLaunch( hhProjectile *proj ) {}; + void Event_DodgeProjectile( hhProjectile *proj ) {}; + void Event_IsEnemySniping(void) {}; + void Event_NumMines(void) {}; +#else +protected: + enum ThrustSide + { + ThrustSide_Left, + ThrustSide_Right, + ThrustSide_Total + }; + + enum ThrustType + { + ThrustType_Idle = 0, + ThrustType_Hover, + ThrustType_Total + }; + + ~hhJetpackHarvesterSimple(); + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void AnimMove( void ); + void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + idProjectile* LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ); + void Event_AppendHoverFx( hhEntityFx* fx ); + void Event_AppendToThrusterList( hhEntityFx* fx ); + void Event_RemoveJetFx(void); + void Event_CreateJetFx(void); + void Event_LaunchMine( const idList* parmList ); + void Event_OnProjectileLaunch( hhProjectile *proj ); + void Event_DodgeProjectile( hhProjectile *proj ); + void Event_IsEnemySniping(void); + void Event_NumMines(void); + virtual void Hide( void ); + virtual void Show( void ); + virtual bool Collide( const trace_t &collision, const idVec3 &velocity ); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + idEntityPtr fxThrusters[ThrustType_Total][ThrustSide_Total]; + int lastAntiProjectileAttack; + bool allowPreDeath; + idList< idEntityPtr > mineList; + bool freezeDamage; + bool specialDamage; +#endif +}; + +#endif \ No newline at end of file diff --git a/src/Prey/ai_keeper_simple.cpp b/src/Prey/ai_keeper_simple.cpp new file mode 100644 index 0000000..3244fba --- /dev/null +++ b/src/Prey/ai_keeper_simple.cpp @@ -0,0 +1,694 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef MA_KeeperStartBlast("keeper_StartBlast", "e"); +const idEventDef MA_KeeperEndBlast("keeper_EndBlast", NULL); +const idEventDef MA_KeeperUpdateTelepathicThrow("keeper_UpdateTelepathicThrow", NULL, NULL); +const idEventDef MA_KeeperTelepathicThrow("keeper_TelepathicThrow", NULL, NULL); +const idEventDef MA_KeeperStartTeleport("keeper_StartTeleport", NULL); +const idEventDef MA_KeeperEndTeleport("keeper_EndTeleport", NULL); +const idEventDef MA_KeeperTeleportExit("keeper_TeleportExit", NULL); +const idEventDef MA_KeeperCreatePortal("keeper_createPortal", NULL); +const idEventDef MA_KeeperEnableShield("keeper_enableShield", NULL); +const idEventDef MA_KeeperDisableShield("keeper_disableShield", NULL); +const idEventDef MA_KeeperAssignShieldFx( "", "e" ); +const idEventDef MA_KeeperTrigger("keeper_Trigger", NULL, NULL); +const idEventDef MA_KeeperTeleportEnter("keeper_TeleportEnter", NULL); +const idEventDef MA_KeeperStartHeadFx("keeper_StartHeadFx"); +const idEventDef MA_KeeperEndHeadFx("keeper_EndHeadFx"); +const idEventDef MA_KeeperAssignHeadFx( "", "e" ); +const idEventDef MA_KeeperGetThrowEntity( "keeper_GetThrowEntity", NULL, 'e' ); +const idEventDef MA_KeeperGetTriggerEntity( "keeper_GetTriggerEntity", NULL, 'e' ); +const idEventDef MA_KeeperTriggerEntity( "keeper_TriggerEntity", "e" ); +const idEventDef MA_KeeperThrowEntity( "keeper_ThrowEntity", "e" ); +const idEventDef MA_KeeperForceDisableShield("keeper_forceDisableShield", NULL); + +CLASS_DECLARATION( hhMonsterAI, hhKeeperSimple ) + EVENT( MA_KeeperStartBlast, hhKeeperSimple::Event_StartBlast ) + EVENT( MA_KeeperEndBlast, hhKeeperSimple::Event_EndBlast ) + EVENT( MA_KeeperUpdateTelepathicThrow, hhKeeperSimple::Event_UpdateTelepathicThrow ) + EVENT( MA_KeeperTelepathicThrow, hhKeeperSimple::Event_TelepathicThrow ) + EVENT( MA_KeeperStartTeleport, hhKeeperSimple::Event_StartTeleport ) + EVENT( MA_KeeperEndTeleport, hhKeeperSimple::Event_EndTeleport ) + EVENT( MA_KeeperTeleportExit, hhKeeperSimple::Event_TeleportExit ) + EVENT( MA_KeeperCreatePortal, hhKeeperSimple::Event_CreatePortal ) + EVENT( MA_KeeperEnableShield, hhKeeperSimple::Event_EnableShield ) + EVENT( MA_KeeperDisableShield, hhKeeperSimple::Event_DisableShield ) + EVENT( MA_KeeperAssignShieldFx, hhKeeperSimple::Event_AssignShieldFx ) + EVENT( MA_KeeperTrigger, hhKeeperSimple::Event_KeeperTrigger ) + EVENT( MA_KeeperTeleportEnter, hhKeeperSimple::Event_TeleportEnter ) + EVENT( MA_KeeperStartHeadFx, hhKeeperSimple::Event_StartHeadFx ) + EVENT( MA_KeeperEndHeadFx, hhKeeperSimple::Event_EndHeadFx ) + EVENT( MA_KeeperAssignHeadFx, hhKeeperSimple::Event_AssignHeadFx ) + EVENT( MA_KeeperTriggerEntity, hhKeeperSimple::Event_TriggerEntity ) + EVENT( MA_KeeperThrowEntity, hhKeeperSimple::Event_ThrowEntity ) + EVENT( MA_KeeperGetTriggerEntity, hhKeeperSimple::Event_GetTriggerEntity ) + EVENT( MA_KeeperGetThrowEntity, hhKeeperSimple::Event_GetThrowEntity ) + EVENT( MA_KeeperForceDisableShield, hhKeeperSimple::Event_ForceDisableShield ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +hhKeeperSimple::hhKeeperSimple() { + shieldAlpha = 0.0f; +} + +hhKeeperSimple::~hhKeeperSimple() { + idEntity *ent = NULL; + if ( throwEntity.IsValid() ) { + ent = throwEntity.GetEntity(); + } else if ( targetReaction.entity.IsValid() ) { + ent = targetReaction.entity.GetEntity(); + } else { + return; + } + + if ( ent && ent->IsType(hhMoveable::Type)) { + hhMoveable *throwMoveable = static_cast(ent); + if ( throwMoveable ) { + throwMoveable->Event_Unhover(); + } + } +} + +void hhKeeperSimple::Spawn( void ) { + beamAttack = idEntityPtr ( hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "beamAttack" ) ) ); + if(beamAttack.IsValid()) { + beamAttack->MoveToJoint( this, spawnArgs.GetString("bone_beamAttack")); + beamAttack->BindToJoint( this, spawnArgs.GetString("bone_beamAttack"), false ); + beamAttack->Activate(FALSE); + } + + beamTelepathic = idEntityPtr (hhBeamSystem::SpawnBeam( vec3_origin, spawnArgs.GetString( "beamTelepathic" ) ) ); + if(beamTelepathic.IsValid()) { + beamTelepathic->MoveToJoint( this, spawnArgs.GetString("bone_beamTelepathic")); + beamTelepathic->BindToJoint( this, spawnArgs.GetString("bone_beamTelepathic"), false ); + beamTelepathic->Activate(FALSE); + beamTelepathic->Hide(); + } + if ( spawnArgs.GetInt( "always_shield", "0" ) && !spawnArgs.GetBool( "hide", "0" ) ) { + Event_EnableShield(); + } + nextShieldImpact = gameLocal.time; + bThrowing = false; +} + +void hhKeeperSimple::Think() { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { + return; + } + + idEntity *ent = NULL; + if ( throwEntity.IsValid() ) { + ent = throwEntity.GetEntity(); + } else if ( targetReaction.entity.IsValid() ) { + ent = targetReaction.entity.GetEntity(); + } + + // Draw beam from our brain to the object we are controlling + if ( beamTelepathic.IsValid() ) { + if( bThrowing && ent && !beamTelepathic->IsHidden() && beamTelepathic->IsActivated() ) { + beamTelepathic->SetTargetLocation(ent->GetOrigin()); + } else { + beamTelepathic->Activate( false ); + beamTelepathic->Hide(); + } + } + + if ( shieldFx.IsValid() ) { + if ( AI_SHIELD ) { + //fade on shield + shieldAlpha += spawnArgs.GetFloat( "shield_delta", "0.05" ); + if ( shieldAlpha > 1.0 ) { + shieldAlpha = 1.0f; + } + } else { + //fade off shield + shieldAlpha -= spawnArgs.GetFloat( "shield_delta", "0.05" ); + if ( shieldAlpha < 0.0 ) { + shieldAlpha = 0.0f; + } + } + shieldFx->SetParticleShaderParm( SHADERPARM_TIME_OF_DEATH, shieldAlpha ); + } + + hhMonsterAI::Think(); + + if ( physicsObj.HasGroundContacts() && !physicsObj.HadGroundContacts() ) { + AI_LANDED = true; + } +} + +void hhKeeperSimple::Event_StartBlast( idEntity *ent) { + if( beamAttack.IsValid() ) { + hhFxInfo fxInfo; + fxInfo.SetEntity( ent ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfo( spawnArgs.GetString("fx_portalstart"), ent->GetOrigin() + (spawnArgs.GetVector( "portal_start_offset" ) * ent->GetAxis()), ent->GetAxis(), &fxInfo ); + beamAttack->SetTargetLocation( ent->GetOrigin() + ent->GetAxis() * spawnArgs.GetVector( "telepathic_beam_offset" ) ); + beamAttack->Activate(TRUE); + } +} + +void hhKeeperSimple::Event_EndBlast() { + if( beamAttack.IsValid() ) { + beamAttack->Activate(FALSE); + } +} + +void hhKeeperSimple::Event_UpdateTelepathicThrow() { + idEntity *ent = NULL; + if ( throwEntity.IsValid() ) { + ent = throwEntity.GetEntity(); + } else if ( targetReaction.entity.IsValid() ) { + ent = targetReaction.entity.GetEntity(); + } else { + return; + } + if ( !ent || !ent->IsType(hhMoveable::Type) ) { + return; + } + bThrowing = true; + if ( beamTelepathic.IsValid() ) { + if ( beamTelepathic->IsHidden() ) { + beamTelepathic->Show(); + } + beamTelepathic->Activate( true ); + } + + hhMoveable *throwMoveable = static_cast(ent); + idVec3 hoverToPos; + + if(GetEnemy()) { + UpdateEnemyPosition(); + idVec3 toEnemy = GetLastEnemyPos() - GetOrigin(); + float dist = toEnemy.Normalize(); + hoverToPos = GetOrigin() + toEnemy * dist * 0.25f; + hoverToPos.z = GetOrigin().z + EyeHeight() + 200.0f; + } + else { + hoverToPos = throwMoveable->GetOrigin() + idVec3(0.0f, 0.0f, 128.0f); + } + throwMoveable->AllowImpact( true ); + throwMoveable->Event_HoverTo(hoverToPos); + throwMoveable->GetPhysics()->SetAngularVelocity(idVec3(10.0f, 10.0f, 10.0f)); + // HUMANHEAD mdl: Beef up the damage on movables thrown by the keeper + throwMoveable->SetDamageDef( spawnArgs.GetString( "moveableDamageDef", "damage_movable20" ) ); + PostEventMS(&MA_KeeperUpdateTelepathicThrow, 100); +} + +void hhKeeperSimple::Event_TelepathicThrow() { + idEntity *ent = NULL; + bThrowing = false; + if ( beamTelepathic.IsValid() ) { + beamTelepathic->Hide(); + } + if ( throwEntity.IsValid() ) { + ent = throwEntity.GetEntity(); + } else if ( targetReaction.entity.IsValid() ) { + ent = targetReaction.entity.GetEntity(); + } else { + return; + } + + // Start the object hovering + if ( !ent || !ent->IsType(hhMoveable::Type) ) { + return; + } + + targetReaction.entity.Clear(); + CancelEvents( &MA_KeeperUpdateTelepathicThrow ); + hhMoveable *throwMoveable = static_cast(ent); + if ( throwMoveable && throwMoveable->IsType( hhMoveable::Type ) ) { + throwMoveable->Event_Unhover(); + } + + // Throw it at an enemy if we have one + if(GetEnemy()) { + idVec3 toEnemy = GetEnemy()->GetAimPosition() - throwMoveable->GetOrigin(); + toEnemy.Normalize(); + throwMoveable->GetPhysics()->SetLinearVelocity(toEnemy * spawnArgs.GetFloat("telepathic_throw_velocity", "2000")); + } +} + +void hhKeeperSimple::Event_TeleportExit() { + const function_t *newstate = NULL; + newstate = GetScriptFunction( "state_TeleportExit" ); + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +} + +void hhKeeperSimple::Event_TeleportEnter() { + const function_t *newstate = NULL; + newstate = GetScriptFunction( "state_TeleportEnter" ); + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +} + +void hhKeeperSimple::Event_StartTeleport() { + Hide(); +} + +void hhKeeperSimple::Event_EndTeleport() { + idVec3 newOrig; + idEntity *ent; + idList nodeList; + float bestScore = -1; + float bestNodeIndex = -1; + + // Find the best position to teleport to + // Find where we want to teleport to + idStr randomNode = spawnArgs.RandomPrefix( "teleport_info", gameLocal.random ); + ent = gameLocal.FindEntity( randomNode ); + if ( ent ) { + newOrig = ent->GetOrigin(); + } else { + newOrig = GetOrigin(); + gameLocal.Warning( "%s doesn't have any teleport infos", name.c_str() ); + } + + //teleport and face enemy + SetOrigin( newOrig ); + if( GetEnemy() ) { + TurnToward(GetEnemy()->GetOrigin()); + current_yaw = ideal_yaw; + Turn(); + } +} + +float hhKeeperSimple::GetTeleportDestRating(const idVec3 &pos) { + // Make sure this spot doesn't contain something else + idEntity *entList[MAX_GENTITIES]; + idBounds myNewBnds = this->GetPhysics()->GetBounds(); + myNewBnds = myNewBnds.Translate(pos); + myNewBnds.ExpandSelf(24.0f); + int numHit = gameLocal.clip.EntitiesTouchingBounds(myNewBnds, -1, entList, MAX_GENTITIES); + if(numHit > 0) { + for(int i=0;iIsHidden() && (entList[i]->IsType(hhMoveable::Type) || entList[i]->IsType(idActor::Type))) { + return -1; + } + } + } + + float w = gameLocal.random.RandomFloat(); + + return idMath::ClampFloat(-1.0f, 1.0f, w); +} + +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhKeeperSimple::LinkScriptVariables() { + hhMonsterAI::LinkScriptVariables(); + LinkScriptVariable( AI_LANDED ); + LinkScriptVariable( AI_SHIELD ); +} + +void hhKeeperSimple::Event_CreatePortal (void) { + static const char * passPrefix = "portal_"; + const char * portalDef; + idEntity * portal; + idDict portalArgs; + idList xferKeys; + idList xferValues; + + const idKeyValue *buddyKV = spawnArgs.FindKey( "portal_buddy" ); + if ( buddyKV && buddyKV->GetValue().Length() && gameLocal.FindEntity(buddyKV->GetValue().c_str()) ) { + // Case of a valid portal_buddy key, make a real portal + portalDef = spawnArgs.GetString( "def_portal" ); + } else if ( spawnArgs.FindKey( "portal_cameraTarget" ) ) { // The monster portal has a camera target, so use a real portal instead of the fake portal + portalDef = spawnArgs.GetString( "def_portal" ); + } else { + portalDef = spawnArgs.GetString( "def_fakeportal" ); + } + + if ( !portalDef || !portalDef[0] ) { + return; + } + + // Set the origin of the portal to us. + portalArgs.SetVector( "origin", GetOrigin() ); + portalArgs.Set( "rotation", spawnArgs.GetString( "rotation" ) ); + portalArgs.Set( "remove_on_close", "1" ); + + // Pass along all 'portal_' keys to the portal's spawnArgs; + hhUtils::GetKeysAndValues( spawnArgs, passPrefix, xferKeys, xferValues ); + for ( int i = 0; i < xferValues.Num(); ++i ) { + xferKeys[ i ].StripLeadingOnce( passPrefix ); + portalArgs.Set( xferKeys[ i ].c_str(), xferValues[ i ].c_str() ); + } + + // Spawn the portal + portal = gameLocal.SpawnObject( portalDef, &portalArgs ); + if ( !portal ) { + return; + } + + // Move the portal up some pre determinted amt, since its origin is in the middle of it + trace_t tr; + gameLocal.clip.TracePoint(tr, GetOrigin(), GetOrigin() + idVec3(0,0,-200), MASK_MONSTERSOLID, this); + if ( tr.fraction < 1.0f ) { + portal->GetPhysics()->SetOrigin( tr.c.point + idVec3( 0,0,2 ) ); + } else { + float offset = spawnArgs.GetFloat( "offset_portal", 0 ); + portal->GetPhysics()->SetOrigin( portal->GetPhysics()->GetOrigin() + (portal->GetAxis()[2] * offset) ); + } + + // Update the camera stuff + portal->ProcessEvent( &EV_UpdateCameraTarget ); + + // Open the portal - Need to delay this, so that PostSpawn gets called/sets up the partner portal + //? Should we always pass in the player? + portal->PostEventSec( &EV_Activate, 0, gameLocal.GetLocalPlayer() ); +} + +void hhKeeperSimple::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + if ( beamAttack.IsValid() ) { + SAFE_REMOVE( beamAttack ); + } + if( beamTelepathic.IsValid() ) { + SAFE_REMOVE( beamTelepathic ); + } + hhMoveable *throwMoveable = static_cast(targetReaction.entity.GetEntity()); + if ( throwMoveable && throwMoveable->IsType( hhMoveable::Type ) ) { + targetReaction.entity.Clear(); + throwMoveable->Event_Unhover(); + } + if ( renderEntity.customSkin == NULL ) { + SetSkinByName( "skins/monsters/keeper_nolegs" ); + } +} + +void hhKeeperSimple::Event_EnableShield(void) { + if ( spawnArgs.GetInt( "never_shield", "0" ) ) { + return; + } + fl.applyDamageEffects = false; + AI_SHIELD = true; + if ( shieldFx.IsValid() ) { + SAFE_REMOVE( shieldFx ); + } + hhFxInfo fxInfo; + fxInfo.SetEntity( this ); + fxInfo.SetBindBone( "head" ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfo( spawnArgs.GetString("fx_shield"), GetOrigin(), mat3_identity, &fxInfo, &MA_KeeperAssignShieldFx ); +} + +void hhKeeperSimple::Event_ForceDisableShield(void) { + fl.applyDamageEffects = true; + AI_SHIELD = false; +} + +void hhKeeperSimple::Event_DisableShield(void) { + if ( spawnArgs.GetInt( "always_shield", "0" ) ) { + return; + } + fl.applyDamageEffects = true; + AI_SHIELD = false; +} + +void hhKeeperSimple::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int loc ) { + if ( AI_SHIELD ) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef && damageDef->GetBool( "spirit_damage" ) ) { + hhMonsterAI::Damage(inflictor, attacker, dir, damageDefName, damageScale, loc); + } + if ( inflictor ) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + if ( gameLocal.time >= nextShieldImpact ) { + nextShieldImpact = gameLocal.time + int(spawnArgs.GetFloat( "shield_impact_freq", "0.4" ) * 1000); + BroadcastFxInfoPrefixed( "fx_shield_impact", inflictor->GetOrigin() + GetAxis()*idVec3(50,0,0), mat3_identity, &fxInfo ); + } + StartSound( "snd_shield_impact", SND_CHANNEL_BODY ); + if ( inflictor->IsType( idProjectile::Type ) ) { + inflictor->PostEventMS( &EV_Remove, 0 ); + } + } + } else { + hhMonsterAI::Damage(inflictor, attacker, dir, damageDefName, damageScale, loc); + } +} + +void hhKeeperSimple::Event_AssignShieldFx( hhEntityFx* fx ) { + shieldFx = fx; +} + +void hhKeeperSimple::Event_StartHeadFx() { + hhFxInfo fxInfo; + fxInfo.SetEntity( this ); + fxInfo.SetBindBone( spawnArgs.GetString( "bone_telepathic_fx", "head" ) ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfo( spawnArgs.GetString("fx_telepathic"), GetOrigin(), mat3_identity, &fxInfo, &MA_KeeperAssignHeadFx ); +} + +void hhKeeperSimple::Event_EndHeadFx() { + SAFE_REMOVE( headFx ); +} + +void hhKeeperSimple::Event_AssignHeadFx( hhEntityFx* fx ) { + headFx = fx; +} + +void hhKeeperSimple::Event_KeeperTrigger() { + if ( targetReaction.entity.IsValid() ) { + targetReaction.entity->ProcessEvent( &EV_Activate, this ); + hhReaction *reaction = targetReaction.GetReaction(); + if (reaction) { + reaction->active = false; + } + } +} + +idProjectile *hhKeeperSimple::LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { //HUMANHEAD mdc - added desiredProjectileDef for supporting multiple projs. + idVec3 muzzle; + idVec3 dir; + idVec3 start; + trace_t tr; + idBounds projBounds; + float distance; + const idClipModel *projClip; + float attack_accuracy; + float attack_cone; + float projectile_spread; + float diff; + float angle; + float spin; + idAngles ang; + int num_projectiles; + int i; + idMat3 axis; + idVec3 tmp; + idProjectile *lastProjectile; + + //HUMANHEAD mdc - added to support multiple projectiles + if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile + int projIndex = FindProjectileInfo( desiredProjectileDef ); + if( projIndex >= 0 ) { + SetCurrentProjectile( projIndex ); + } + } + //HUMANHEAD END + + + if ( !projectileDef ) { + gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); + return NULL; + } + + attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); + attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); + projectile_spread = spawnArgs.GetFloat( "projectile_spread", "0" ); + num_projectiles = spawnArgs.GetInt( "num_projectiles", "1" ); + + GetMuzzle( jointname, muzzle, axis ); + + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, axis[ 0 ] ); + } + + lastProjectile = projectile.GetEntity(); + + if ( target != NULL ) { + tmp = target->GetPhysics()->GetAbsBounds().GetCenter() - muzzle; + tmp.Normalize(); + axis = tmp.ToMat3(); + } else { + axis = viewAxis; + } + + // rotate it because the cone points up by default + tmp = axis[2]; + axis[2] = axis[0]; + axis[0] = -tmp; + + // make sure the projectile starts inside the monster bounding box + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + projClip = lastProjectile->GetPhysics()->GetClipModel(); + projBounds = projClip->GetBounds().Rotate( axis ); + + // check if the owner bounds is bigger than the projectile bounds + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { + start = muzzle + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); + muzzle = tr.endpos; + + // set aiming direction + GetAimDir( muzzle, target, this, dir ); + ang = dir.ToAngles(); + + // adjust his aim so it's not perfect. uses sine based movement so the tracers appear less random in their spread. + float t = MS2SEC( gameLocal.time + entityNumber * 497 ); + ang.pitch += idMath::Sin16( t * 5.1 ) * attack_accuracy; + ang.yaw += idMath::Sin16( t * 6.7 ) * attack_accuracy; + + if ( clampToAttackCone ) { + // clamp the attack direction to be within monster's attack cone so he doesn't do + // things like throw the missile backwards if you're behind him + diff = idMath::AngleDelta( ang.yaw, current_yaw ); + if ( diff > attack_cone ) { + ang.yaw = current_yaw + attack_cone; + } else if ( diff < -attack_cone ) { + ang.yaw = current_yaw - attack_cone; + } + } + + axis = ang.ToMat3(); + + float spreadRad = DEG2RAD( projectile_spread ); + for( i = 0; i < num_projectiles; i++ ) { + // spread the projectiles out + angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); + spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); + //dir = axis[ 0 ] + axis[ 2 ] * ( 0 * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir = axis[ 0 ] - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir.Normalize(); + + // launch the projectile + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, dir ); + } + lastProjectile = projectile.GetEntity(); + lastProjectile->Launch( muzzle, dir, vec3_origin ); + projectile = NULL; + } + + TriggerWeaponEffects( muzzle, axis ); + + lastAttackTime = gameLocal.time; + +//HUMANHEAD mdc - added to support multiple projectiles + projectile = NULL; + SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side +//HUMANHEAD END + + return lastProjectile; +} + +void hhKeeperSimple::Event_GetTriggerEntity() { + idThread::ReturnEntity( triggerEntity.GetEntity() ); +} + +void hhKeeperSimple::Event_TriggerEntity( idEntity *ent ) { + if ( !ent ) { + return; + } + triggerEntity = ent; + const function_t *newstate = NULL; + newstate = GetScriptFunction( "state_TriggerEntity" ); + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +} + +void hhKeeperSimple::Event_GetThrowEntity() { + idThread::ReturnEntity( throwEntity.GetEntity() ); +} + +void hhKeeperSimple::Event_ThrowEntity( idEntity *ent ) { + if ( !ent || !ent->IsType( idMoveable::Type ) ) { + return; + } + throwEntity = ent; + const function_t *newstate = NULL; + newstate = GetScriptFunction( "state_ThrowEntity" ); + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +} + +void hhKeeperSimple::Hide() { + Event_DisableShield(); + hhMonsterAI::Hide(); +} + +void hhKeeperSimple::Show() { + hhMonsterAI::Show(); + if ( spawnArgs.GetInt( "always_shield", "0" ) ) { + Event_EnableShield(); + } +} + +void hhKeeperSimple::HideNoDormant() { + //overridden to hide shield fx + if ( shieldFx.IsValid() ) { + shieldFx->Hide(); + } + idAI::Hide(); +} + +/* +===================== +hhKeeperSimple::Save +===================== +*/ +void hhKeeperSimple::Save( idSaveGame *savefile ) const { + beamTelepathic.Save( savefile ); + beamAttack.Save( savefile ); + shieldFx.Save( savefile ); + headFx.Save( savefile ); + triggerEntity.Save( savefile ); + throwEntity.Save( savefile ); + savefile->WriteInt( nextShieldImpact ); + savefile->WriteBool( bThrowing ); +} + +/* +===================== +hhKeeperSimple::Restore +===================== +*/ +void hhKeeperSimple::Restore( idRestoreGame *savefile ) { + beamTelepathic.Restore( savefile ); + beamAttack.Restore( savefile ); + shieldFx.Restore( savefile ); + headFx.Restore( savefile ); + triggerEntity.Restore( savefile ); + throwEntity.Restore( savefile ); + savefile->ReadInt( nextShieldImpact ); + savefile->ReadBool( bThrowing ); +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_keeper_simple.h b/src/Prey/ai_keeper_simple.h new file mode 100644 index 0000000..94c97bf --- /dev/null +++ b/src/Prey/ai_keeper_simple.h @@ -0,0 +1,98 @@ + +#ifndef __PREY_AI_KEEPER_SIMPLE_H__ +#define __PREY_AI_KEEPER_SIMPLE_H__ + +class hhKeeperSimple : public hhMonsterAI { + +public: + CLASS_PROTOTYPE(hhKeeperSimple); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_GetTriggerEntity() {}; + void Event_TriggerEntity( idEntity *ent ) {}; + void Event_GetThrowEntity() {}; + void Event_ThrowEntity( idEntity *ent ) {}; + void Event_StartBlast( idEntity *ent ) {}; + void Event_EndBlast() {}; + void Event_BeginTelepathicThrow(idEntity *throwMe) {}; + void Event_UpdateTelepathicThrow() {}; + void Event_TelepathicThrow() {}; + void Event_StartTeleport() {}; + void Event_EndTeleport() {}; + void Event_TeleportExit() {}; + void Event_TeleportEnter() {}; + void Event_CreatePortal() {}; + void Event_EnableShield() {}; + void Event_DisableShield() {}; + void Event_ForceDisableShield() {}; + void Event_AssignShieldFx( hhEntityFx* fx ) {}; + void Event_StartHeadFx() {}; + void Event_EndHeadFx() {}; + void Event_AssignHeadFx( hhEntityFx* fx ) {}; + void Event_KeeperTrigger() {}; +#else + + + hhKeeperSimple(); + ~hhKeeperSimple(); + void Spawn( void ); + virtual void Think(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void LinkScriptVariables(); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + idProjectile* LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ); + void Hide(); + void Show(); + void HideNoDormant(); + idScriptBool AI_LANDED; + idScriptBool AI_SHIELD; +protected: + void Event_GetTriggerEntity(); + void Event_TriggerEntity( idEntity *ent ); + void Event_GetThrowEntity(); + void Event_ThrowEntity( idEntity *ent ); + + // Attack blast + void Event_StartBlast( idEntity *ent ); + void Event_EndBlast(); + + // Telepathic Throwing + void Event_BeginTelepathicThrow(idEntity *throwMe); + void Event_UpdateTelepathicThrow(); + void Event_TelepathicThrow(); + + // Teleporting + void Event_StartTeleport(); + void Event_EndTeleport(); + void Event_TeleportExit(); + void Event_TeleportEnter(); + void Event_CreatePortal(); + + // Shield + void Event_EnableShield(); + void Event_DisableShield(); + void Event_ForceDisableShield(); + void Event_AssignShieldFx( hhEntityFx* fx ); + void Event_StartHeadFx(); + void Event_EndHeadFx(); + void Event_AssignHeadFx( hhEntityFx* fx ); + + // Telepathic Triggering + void Event_KeeperTrigger(); + + float GetTeleportDestRating(const idVec3 &pos); + idEntityPtr beamTelepathic; // Beam used when we are telepathically controlling an object + idEntityPtr beamAttack; + idEntityPtr shieldFx; + idEntityPtr headFx; + idEntityPtr triggerEntity; + idEntityPtr throwEntity; + int nextShieldImpact; + float shieldAlpha; + bool bThrowing; +#endif +}; + +#endif \ No newline at end of file diff --git a/src/Prey/ai_mutate.cpp b/src/Prey/ai_mutate.cpp new file mode 100644 index 0000000..5ac9ceb --- /dev/null +++ b/src/Prey/ai_mutate.cpp @@ -0,0 +1,63 @@ + + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +CLASS_DECLARATION(hhMonsterAI, hhMutate) + EVENT( MA_OnProjectileLaunch, hhMutate::Event_OnProjectileLaunch ) +END_CLASS + +void hhMutate::Event_OnProjectileLaunch(hhProjectile *proj) { + const function_t *newstate = NULL; + if ( !enemy.IsValid() || !AI_COMBAT ) { + return; + } + + float min = spawnArgs.GetFloat( "dda_dodge_min", "0.3" ); + float max = spawnArgs.GetFloat( "dda_dodge_max", "0.8" ); + float dodgeChance = 0.6f; + + dodgeChance = (min + (max-min)*gameLocal.GetDDAValue() ); + + if ( ai_debugBrain.GetBool() ) { + gameLocal.Printf( "%s dodge chance %f\n", GetName(), dodgeChance ); + } + if ( gameLocal.random.RandomFloat() > dodgeChance ) { + return; + } + + //determine which side to dodge to + idVec3 povPos, targetPos; + povPos = enemy->GetPhysics()->GetOrigin(); + targetPos = GetPhysics()->GetOrigin(); + idVec3 povToTarget = targetPos - povPos; + povToTarget.z = 0.f; + povToTarget.Normalize(); + idVec3 povLeft, povUp; + povToTarget.OrthogonalBasis(povLeft, povUp); + povLeft.Normalize(); + + idVec3 projVel = proj->GetPhysics()->GetLinearVelocity(); + projVel.Normalize(); + float dot = povLeft * projVel; + if ( dot < 0 ) { + newstate = GetScriptFunction( "state_DodgeBackRight" ); + } else { + newstate = GetScriptFunction( "state_DodgeBackLeft" ); + } + + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +} + +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhMutate::LinkScriptVariables(void) { + hhMonsterAI::LinkScriptVariables(); + LinkScriptVariable( AI_COMBAT ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_mutate.h b/src/Prey/ai_mutate.h new file mode 100644 index 0000000..431bc97 --- /dev/null +++ b/src/Prey/ai_mutate.h @@ -0,0 +1,15 @@ + +#ifndef __PREY_AI_MUTATE_H__ +#define __PREY_AI_MUTATE_H__ + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +class hhMutate : public hhMonsterAI { +public: + CLASS_PROTOTYPE(hhMutate); +protected: + void Event_OnProjectileLaunch(hhProjectile *proj); + void LinkScriptVariables(); + idScriptBool AI_COMBAT; +}; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#endif \ No newline at end of file diff --git a/src/Prey/ai_mutilatedhuman.cpp b/src/Prey/ai_mutilatedhuman.cpp new file mode 100644 index 0000000..5a488f6 --- /dev/null +++ b/src/Prey/ai_mutilatedhuman.cpp @@ -0,0 +1,390 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef MH_AlertFriends("alertFriends", NULL, NULL); +const idEventDef MH_DropBinds("dropBinds"); +const idEventDef MH_DropProjectiles("" ); + +CLASS_DECLARATION(hhMonsterAI, hhMutilatedHuman) + EVENT( MH_AlertFriends, hhMutilatedHuman::Event_AlertFriends ) + EVENT( MH_DropBinds, hhMutilatedHuman::Event_DropBinds ) + EVENT( MH_DropProjectiles, hhMutilatedHuman::Event_DropProjectiles ) +END_CLASS + +void hhMutilatedHuman::Spawn() { + damageFlag = 0; +} + +void hhMutilatedHuman::Event_DropBinds( ) { + idEntity *ent; + idEntity *next; + for( ent = teamChain; ent != NULL; ent = next ) { + next = ent->GetTeamChain(); + if ( ent && !ent->IsType( hhProjectile::Type ) && !ent->IsType( idEntityFx::Type ) && ent->GetBindMaster() == this && ent != head.GetEntity() ) { + ent->Unbind(); + next = teamChain; + } + } +} + +void hhMutilatedHuman::Event_DropProjectiles() { + idEntity *ent; + idEntity *next; + for( ent = teamChain; ent != NULL; ent = next ) { + next = ent->GetTeamChain(); + if ( ent && ent->IsType( hhProjectile::Type ) ) { + ent->Unbind(); + ent->Hide(); + ent->PostEventSec( &EV_Remove, 5 ); + next = teamChain; + } + } +} + +void hhMutilatedHuman::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if (!fl.takedamage) { + return; + } + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + + if ( AI_LIMB_FELL ) { + PostEventSec( &MH_DropProjectiles, 0.1f ); + } + + if ( AI_DEAD ) { + return; + } + + idDict args; + idVec3 bonePos; + idMat3 boneAxis; + idStr debrisName; + idStr damageGroup = GetDamageGroup( location ); + + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef && damageDef->GetBool( "ice" ) ) { + return; + } + + if ( !( damageGroup == "head" && damageDef->GetInt( "damage" ) >= spawnArgs.GetFloat( "head_falloff_damage", "70" ) ) ) { + if ( gameLocal.random.RandomFloat() >= spawnArgs.GetFloat( "chanceLimbsWillFallOff", "0.1" ) ) { // CJR: Build in a chance that the limbs will fall off + return; + } + } + + if ( damageGroup == "left_arm" && !(damageFlag & BIT(0)) ) { + damageFlag |= BIT(0); + debrisName = spawnArgs.GetString( "def_debris_arm" ); + if ( !debrisName.IsEmpty() ) { + idEntity *debris = gameLocal.SpawnObject( debrisName, &args ); + if ( debris ) { + AI_LIMB_FELL = true; + GetJointWorldTransform( spawnArgs.GetString( "bone_arm_left" ), bonePos, boneAxis ); + debris->SetOrigin( bonePos ); + debris->GetPhysics()->SetLinearVelocity( spawnArgs.GetVector( "debris_arm_velocity" ) * GetAxis() ); + debris->GetPhysics()->SetAngularVelocity( 60 * hhUtils::RandomVector() ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_left_arm_blood"), spawnArgs.GetString("bone_arm_left") ); + } + } + + } else if ( damageGroup == "right_arm" && !(damageFlag & BIT(1)) ) { + damageFlag |= BIT(1); + debrisName = spawnArgs.GetString( "def_debris_arm" ); + if ( !debrisName.IsEmpty() ) { + idEntity *debris = gameLocal.SpawnObject( debrisName, &args ); + if ( debris ) { + AI_LIMB_FELL = true; + GetJointWorldTransform( spawnArgs.GetString( "bone_arm_right" ), bonePos, boneAxis ); + debris->SetOrigin( bonePos ); + debris->GetPhysics()->SetLinearVelocity( spawnArgs.GetVector( "debris_arm_velocity" ) * GetAxis() ); + debris->GetPhysics()->SetAngularVelocity( 60 * hhUtils::RandomVector() ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_right_arm_blood"), spawnArgs.GetString("bone_arm_right") ); + } + } + } else if ( damageGroup == "chest" && !(damageFlag & BIT(2)) ) { + damageFlag |= BIT(2); + damageFlag |= BIT(3); + debrisName = spawnArgs.GetString( "def_debris_chest" ); + if ( !debrisName.IsEmpty() ) { + idEntity *debris = gameLocal.SpawnObject( debrisName, &args ); + if ( debris ) { + AI_LIMB_FELL = true; + GetJointWorldTransform( spawnArgs.GetString( "bone_chest" ), bonePos, boneAxis ); + debris->SetOrigin( bonePos ); + debris->GetPhysics()->SetLinearVelocity( spawnArgs.GetVector( "debris_chest_velocity" ) * GetAxis() ); + debris->GetPhysics()->SetAngularVelocity( 60 * hhUtils::RandomVector() ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_chest_blood"), spawnArgs.GetString("bone_chest") ); + } + } + } else if ( damageGroup == "head" ) { + if ( head.IsValid() && !head->IsHidden() ) { + debrisName = spawnArgs.GetString( "def_debris_head" ); + if ( !debrisName.IsEmpty() ) { + idEntity *debris = gameLocal.SpawnObject( debrisName, &args ); + if ( debris ) { + AI_LIMB_FELL = true; + GetJointWorldTransform( spawnArgs.GetString( "bone_head" ), bonePos, boneAxis ); + debris->SetOrigin( bonePos ); + debris->GetPhysics()->SetLinearVelocity( spawnArgs.GetVector( "debris_head_velocity" ) * GetAxis() ); + debris->GetPhysics()->SetAngularVelocity( 60 * hhUtils::RandomVector() ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_head_blood"), spawnArgs.GetString("bone_head") ); + } + } + head->Hide(); + } + } + + idStr skinName = spawnArgs.GetString("damage_skin"); + skinName += damageFlag; + if ( damageFlag > 0 ) { + SetSkin( declManager->FindSkin( skinName ) ); + UpdateVisuals(); + } +} + +void hhMutilatedHuman::Event_AlertFriends() { + hhMonsterAI *check; + if ( !enemy.IsValid() ) { + return; + } + + for( int i = 0; i < targets.Num(); i++ ) { + check = static_cast(targets[ i ].GetEntity()); + if ( !check || check->IsHidden() || !check->IsType( hhMutilatedHuman::Type ) ) { + continue; + } + check->SetEnemy( enemy.GetEntity() ); + } +} + +void hhMutilatedHuman::AnimMove( void ) { + //overridden to set enemy if blockedEnt can be an enemy + idVec3 goalPos; + idVec3 delta; + idVec3 goalDelta; + float goalDist; + monsterMoveResult_t moveResult; + idVec3 newDest; + + idVec3 oldorigin = physicsObj.GetOrigin(); +#ifdef HUMANHEAD //jsh wallwalk + idMat3 oldaxis = GetGravViewAxis(); +#else + idMat3 oldaxis = viewAxis; +#endif + + AI_BLOCKED = false; + + if ( move.moveCommand < NUM_NONMOVING_COMMANDS ){ + move.lastMoveOrigin.Zero(); + move.lastMoveTime = gameLocal.time; + } + + move.obstacle = NULL; + if ( ( move.moveCommand == MOVE_FACE_ENEMY ) && enemy.GetEntity() ) { + TurnToward( lastVisibleEnemyPos ); + goalPos = oldorigin; + } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + goalPos = oldorigin; + } else if ( GetMovePos( goalPos ) ) { + if ( move.moveCommand != MOVE_WANDER ) { + CheckObstacleAvoidance( goalPos, newDest ); + TurnToward( newDest ); + } else { + TurnToward( goalPos ); + } + } + + Turn(); + + if ( move.moveCommand == MOVE_SLIDE_TO_POSITION ) { + if ( gameLocal.time < move.startTime + move.duration ) { + goalPos = move.moveDest - move.moveDir * MS2SEC( move.startTime + move.duration - gameLocal.time ); + delta = goalPos - oldorigin; + delta.z = 0.0f; + } else { + delta = move.moveDest - oldorigin; + delta.z = 0.0f; + StopMove( MOVE_STATUS_DONE ); + } + } else if ( allowMove ) { +#ifdef HUMANHEAD //jsh wallwalk + GetMoveDelta( oldaxis, GetGravViewAxis(), delta ); +#else + GetMoveDelta( oldaxis, viewAxis, delta ); +#endif + } else { + delta.Zero(); + } + + if ( move.moveCommand == MOVE_TO_POSITION ) { + goalDelta = move.moveDest - oldorigin; + goalDist = goalDelta.LengthFast(); + if ( goalDist < delta.LengthFast() ) { + delta = goalDelta; + } + } + + physicsObj.SetDelta( delta ); + physicsObj.ForceDeltaMove( disableGravity ); + + RunPhysics(); + + if ( ai_debugMove.GetBool() ) { + // HUMANHEAD JRM - so we can see if grav is on or off + if(disableGravity) { + gameRenderWorld->DebugLine( colorRed, oldorigin, physicsObj.GetOrigin(), 5000 ); + } else { + gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 5000 ); + } + } + + moveResult = physicsObj.GetMoveResult(); + if ( !af_push_moveables && attack.Length() && TestMelee() ) { + DirectDamage( attack, enemy.GetEntity() ); + } else { + idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); + if ( blockEnt && blockEnt->IsType( idActor::Type ) && ReactionTo( blockEnt ) != ATTACK_IGNORE ) { + SetEnemy( static_cast(blockEnt) ); + } + if ( blockEnt && blockEnt->IsType( hhPod::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + blockEnt->Damage( this, this, vec3_zero, spawnArgs.GetString( "kick_damage" ), 1.0f, INVALID_JOINT ); + } + if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + } + } + + BlockedFailSafe(); + + AI_ONGROUND = physicsObj.OnGround(); + + idVec3 org = physicsObj.GetOrigin(); + if ( oldorigin != org ) { + TouchTriggers(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), org, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); + gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); + DrawRoute(); + } +} + +int hhMutilatedHuman::ReactionTo( const idEntity *ent ) { + const idActor *actor = static_cast( ent ); + if( actor && actor->IsType(hhDeathProxy::Type) ) { + return ATTACK_IGNORE; + } + + if ( ent->health <= 0 ) { + return ATTACK_IGNORE; + } + + if ( ent->fl.hidden ) { + // ignore hidden entities + return ATTACK_IGNORE; + } + + if ( !ent->IsType( idActor::Type ) ) { + return ATTACK_IGNORE; + } + + actor = static_cast( ent ); + if ( actor->IsType( idPlayer::Type ) && static_cast(actor)->noclip ) { + // ignore players in noclip mode + return ATTACK_IGNORE; + } + + //only attack spiritwalking players if they hurt me + if ( ent->IsType( hhPlayer::Type ) ) { + const hhPlayer *player = static_cast( ent ); + if ( nextSpiritProxyCheck == 0 && player && player->IsSpiritWalking() ) { + nextSpiritProxyCheck = gameLocal.time + SEC2MS(2); + return ATTACK_ON_DAMAGE; + } + } + + if ( ent->IsType( hhSpiritProxy::Type ) ) { + if ( gameLocal.time > nextSpiritProxyCheck ) { + nextSpiritProxyCheck = 0; + //attack spiritproxy on sight if we have no enemy or if its closer than our current enemy + if ( enemy.IsValid() && enemy->IsType( hhPlayer::Type ) ) { + float distToEnemy = (enemy->GetOrigin() - GetOrigin()).LengthSqr(); + float distToProxy = (ent->GetOrigin() - GetOrigin()).LengthSqr(); + if ( distToProxy < distToEnemy ) { + return ATTACK_ON_SIGHT; + } + } else { + return ATTACK_ON_SIGHT; + } + } else { + return ATTACK_IGNORE; + } + } + + if ( ent && ent->spawnArgs.GetBool( "never_target" ) ) { + return ATTACK_IGNORE; + } + + //if we're hurt, its probably because of the player, so keep him as an enemy + if ( actor->IsType( idPlayer::Type ) && health < spawnHealth ) { + return ATTACK_ON_DAMAGE | ATTACK_ON_ACTIVATE | ATTACK_ON_SIGHT; + } + + // actors on different teams will always fight each other + if ( actor->team != team ) { + if ( actor->fl.notarget ) { + // don't attack on sight when attacker is notargeted + if ( actor->IsType( idPlayer::Type ) ) { + return ATTACK_ON_DAMAGE; + } else { + return ATTACK_ON_DAMAGE | ATTACK_ON_ACTIVATE; + } + } + //force players to only alert through damage + if ( actor->IsType( idPlayer::Type ) ) { + return ATTACK_ON_DAMAGE | ATTACK_ON_ACTIVATE; + } else if ( actor->IsType( idAI::Type ) ) { + //only allow ai actors to otherwise alert them + return ATTACK_ON_SIGHT | ATTACK_ON_DAMAGE | ATTACK_ON_ACTIVATE; + } + } + + // monsters will fight when attacked by lower ranked monsters. rank 0 never fights back. + if ( rank && ( actor->rank < rank ) ) { + return ATTACK_ON_DAMAGE; + } + + // don't fight back + return ATTACK_IGNORE; +} + +/* +===================== +hhMutilatedHuman::Save +===================== +*/ +void hhMutilatedHuman::Save( idSaveGame *savefile ) const { + savefile->WriteInt( damageFlag ); +} + +/* +===================== +hhMutilatedHuman::Restore +===================== +*/ +void hhMutilatedHuman::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( damageFlag ); +} + +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhMutilatedHuman::LinkScriptVariables(void) { + hhMonsterAI::LinkScriptVariables(); + LinkScriptVariable( AI_LIMB_FELL ); +} \ No newline at end of file diff --git a/src/Prey/ai_mutilatedhuman.h b/src/Prey/ai_mutilatedhuman.h new file mode 100644 index 0000000..fcde7d1 --- /dev/null +++ b/src/Prey/ai_mutilatedhuman.h @@ -0,0 +1,23 @@ +#ifndef __PREY_AI_MUTILATEDHUMAN_H__ +#define __PREY_AI_MUTILATEDHUMAN_H__ + +class hhMutilatedHuman : public hhMonsterAI { +public: + CLASS_PROTOTYPE(hhMutilatedHuman); + void Event_AlertFriends(); + void Event_DropBinds(); + void Event_DropProjectiles(); + int ReactionTo( const idEntity *ent ); + void Spawn(); + void AnimMove(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + void LinkScriptVariables(); +protected: + idScriptBool AI_LIMB_FELL; + int damageFlag; +}; + + +#endif //__PREY_AI_MUTILATEDHUMAN_H__ \ No newline at end of file diff --git a/src/Prey/ai_passageway.cpp b/src/Prey/ai_passageway.cpp new file mode 100644 index 0000000..b744d4e --- /dev/null +++ b/src/Prey/ai_passageway.cpp @@ -0,0 +1,144 @@ +// +// ai_passageway.cpp +// +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_EnablePassageway("enable", NULL); +const idEventDef EV_DisablePassageway("disable", NULL); + +// hhAIPassageway +CLASS_DECLARATION(hhAnimated, hhAIPassageway) + EVENT(EV_AnimDone, hhAIPassageway::Event_AnimDone) + EVENT(EV_Activate, hhAIPassageway::Event_Trigger) + EVENT(EV_PostSpawn, hhAIPassageway::PostSpawn) + EVENT(EV_EnablePassageway, hhAIPassageway::Event_EnablePassageway) + EVENT(EV_DisablePassageway, hhAIPassageway::Event_DisablePassageway) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +// +// Spawn() +// +void hhAIPassageway::Spawn() { + timeLastEntered = 0; + lastEntered = NULL; + enabled = TRUE; + hasEntered = FALSE; + + GetPhysics()->SetContents(CONTENTS_SOLID); + + PostEventMS( &EV_PostSpawn, 0 ); +} + +// +// PostSpawn() +// +void hhAIPassageway::PostSpawn() { + SetEnablePassageway(spawnArgs.GetBool("enabled", "1")); +} + +// +// SetEnablePassageway() +// +void hhAIPassageway::SetEnablePassageway(bool tf) { + + // Ignore if we're already in that state + if(enabled == tf) + return; + + fl.refreshReactions = tf; + enabled = tf; +} + +// +// Event_AnimDone() +// +void hhAIPassageway::Event_AnimDone( int animIndex ) { + //When we are done we want to pass ourselves as the activator + activator = this; + + hhAnimated::Event_AnimDone( animIndex ); +} + +// +// Event_Trigger() +// +void hhAIPassageway::Event_Trigger( idEntity *activator ) { + + if(!activator) + return; + + // Ignore being activated by another passage node - they link to eachother, + // so they must be ignored otherwise it will continuously trigger + if( activator->IsType(hhAIPassageway::Type) ) { + return; + } + + // Play our anim + hhAnimated::Event_Activate( activator ); + HH_ASSERT(anim != NULL); + + if( !activator->IsType(hhMonsterAI::Type) ) { + return; + } + + hhMonsterAI *ai = static_cast(activator); + + // Can't enter if dead + if(ai->health <= 0) + return; + + if(!ai->IsInsidePassageway()) + { + //HUMANHEAD PCF mdl 04/29/06 - Added lastEntered check + if(hasEntered && lastEntered.GetEntity() == ai) { + ai->ProcessEvent(&MA_EnterPassageway, this); + hasEntered = FALSE; + } else { + lastEntered = ai; + timeLastEntered = gameLocal.GetTime(); + hasEntered = TRUE; + } + + } + else + { + ai->PostEventMS(&MA_ExitPassageway, 32, this); + hasEntered = FALSE; + } +} + +// +// GetExitPos() +// +idVec3 hhAIPassageway::GetExitPos(void) { + idVec3 pos = GetOrigin(); + idVec3 exitOffset; + spawnArgs.GetVector("exit_offset", "0 0 0", exitOffset); + exitOffset *= GetAxis(); + pos += exitOffset; + pos.z += 0.2f; + return pos; +} + +void hhAIPassageway::Save(idSaveGame *savefile) const { + savefile->WriteInt( timeLastEntered - gameLocal.time ); + lastEntered.Save( savefile ); + savefile->WriteBool( hasEntered ); + savefile->WriteBool( enabled ); +} + +void hhAIPassageway::Restore(idRestoreGame *savefile) { + savefile->ReadInt( timeLastEntered ); + timeLastEntered += gameLocal.time; + + lastEntered.Restore( savefile ); + savefile->ReadBool( hasEntered ); + savefile->ReadBool( enabled ); +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_passageway.h b/src/Prey/ai_passageway.h new file mode 100644 index 0000000..2d4f00f --- /dev/null +++ b/src/Prey/ai_passageway.h @@ -0,0 +1,49 @@ +#ifndef __PREY_AI_PASSAGENODE_H__ +#define __PREY_AI_PASSAGENODE_H__ + + + +// +// hhAIPassageway +// +class hhAIPassageway : public hhAnimated +{ +public: + CLASS_PROTOTYPE(hhAIPassageway); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + virtual void PostSpawn() {}; + void SetEnablePassageway(bool tf) {}; + void Event_AnimDone( int animIndex ) {}; + void Event_Trigger( idEntity *activator ) {}; + void Event_EnablePassageway(void) {} + void Event_DisablePassageway(void) {} +#else + + hhAIPassageway() {timeLastEntered = 0; lastEntered = NULL;} + virtual void Spawn(); + virtual void PostSpawn(); + + void Save(idSaveGame *savefile) const; + void Restore(idRestoreGame *savefile); + + idVec3 GetExitPos(void); // The point that a monster should be spawned upon exiting this passageway + + void SetEnablePassageway(bool tf); + bool IsPassagewayEnabled(void) const {return enabled;} + + int timeLastEntered; // The time someone last entered this passage node + idEntityPtr lastEntered; // The ai that last entered this passage node + bool hasEntered; // FALSE if the AI has not fully entered yet - waiting for one more trigger + +protected: + void Event_AnimDone( int animIndex ); + void Event_Trigger( idEntity *activator ); + void Event_EnablePassageway(void) {SetEnablePassageway(TRUE);} + void Event_DisablePassageway(void) {SetEnablePassageway(FALSE);} + + bool enabled; // TRUE if we are a valid passagway +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif diff --git a/src/Prey/ai_possessedTommy.cpp b/src/Prey/ai_possessedTommy.cpp new file mode 100644 index 0000000..c220c47 --- /dev/null +++ b/src/Prey/ai_possessedTommy.cpp @@ -0,0 +1,120 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhMonsterAI, hhPossessedTommy ) +END_CLASS + +// TODO: Set up a rhythmic damaging of this entity. + +void hhPossessedTommy::Spawn() { + physicsObj.SetContents( CONTENTS_CORPSE | CONTENTS_MONSTERCLIP | CONTENTS_RENDERMODEL ); + nextDrop = 0; +} + +void hhPossessedTommy::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if ( attacker->IsType( hhPlayer::Type ) ) { // Being attacked by a player, must be a spiritwalking player + if ( possessedProxy.IsValid() ) { + hhSpiritProxy *proxy = possessedProxy.GetEntity(); + + Hide(); + + proxy->SetOrigin( GetOrigin() ); + proxy->SetAxis( GetAxis() ); + proxy->Show(); + proxy->GetPhysics()->SetContents( CONTENTS_CORPSE | CONTENTS_MONSTERCLIP | CONTENTS_RENDERMODEL ); + + if ( proxy->GetPlayer() ) { + proxy->GetPlayer()->Unpossess(); + } + } + + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + PostEventMS( &EV_Remove, 0 ); + + return; + } + + // OTHERWISE, KILL THIS ENTITY AND KILL THE PLAYER -- create a death proxy here, etc + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + + // Kill the player + if (possessedProxy.IsValid() ) { + if ( possessedProxy->GetPlayer() ) { + possessedProxy->GetPlayer()->PossessKilled(); + } + } +} + +void hhPossessedTommy::Event_Remove(void) { + if (!fl.hidden && possessedProxy.IsValid()) { + Hide(); + + hhSpiritProxy *proxy = possessedProxy.GetEntity(); + + proxy->SetOrigin( GetOrigin() ); + proxy->SetAxis( GetAxis() ); + proxy->Show(); + proxy->GetPhysics()->SetContents( CONTENTS_CORPSE | CONTENTS_MONSTERCLIP | CONTENTS_RENDERMODEL ); + + if ( proxy->GetPlayer() ) { + proxy->GetPlayer()->Unpossess(); + } + } + hhMonsterAI::Event_Remove(); +} + +void hhPossessedTommy::Think(void) { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { + return; + } + + hhMonsterAI::Think(); + if (possessedProxy.IsValid() && gameLocal.time > nextDrop) { + hhPlayer *player = possessedProxy->GetPlayer(); + if (player) { + player->SetHealth(player->GetHealth() - 1); + if (player->GetHealth() <= 0) { + Hide(); + + possessedProxy->SetOrigin( GetOrigin() ); + possessedProxy->SetAxis( GetAxis() ); + possessedProxy->Show(); + possessedProxy->GetPhysics()->SetContents( CONTENTS_CORPSE | CONTENTS_MONSTERCLIP | CONTENTS_RENDERMODEL ); + + player->Unpossess(); + + player->Killed(this, this, 1, vec3_origin, INVALID_JOINT); + PostEventMS(&EV_Remove, 0); + } + } + + float min = spawnArgs.GetFloat( "dda_drain_min" ); + float max = spawnArgs.GetFloat( "dda_drain_max" ); + nextDrop = gameLocal.time + min + (max - min) * (1.0f - gameLocal.GetDDAValue()); + } +} + +void hhPossessedTommy::Save( idSaveGame *savefile ) const { + possessedProxy.Save( savefile ); +} + +void hhPossessedTommy::Restore( idRestoreGame *savefile ) { + possessedProxy.Restore( savefile ); + nextDrop = 0; +} + +void hhPossessedTommy::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + // Turn to a different direction + float adjust = 45.0f + (gameLocal.random.RandomFloat() * 30.0f); + if (gameLocal.random.RandomInt(100) > 50) { + adjust = -adjust; + } + gameLocal.Printf("Adjusting by %f\n", adjust); + TurnToward(current_yaw + adjust); + move.nextWanderTime = 0; +} + diff --git a/src/Prey/ai_possessedTommy.h b/src/Prey/ai_possessedTommy.h new file mode 100644 index 0000000..9d365ab --- /dev/null +++ b/src/Prey/ai_possessedTommy.h @@ -0,0 +1,30 @@ +#ifndef __PREY_AI_POSSESSED_TOMMY_H__ +#define __PREY_AI_POSSESSED_TOMMY_H__ + +/*********************************************************************** + hhPossessedTommy. +***********************************************************************/ +class hhPossessedTommy : public hhMonsterAI { + +public: + CLASS_PROTOTYPE( hhPossessedTommy ); + +public: + void Spawn(); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void Think(void); + + void SetPossessedProxy( hhSpiritProxy *newProxy ) { possessedProxy = newProxy; } + + virtual void Event_Remove(void); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); +protected: + idEntityPtr possessedProxy; + int nextDrop; +}; + +#endif //__PREY_AI_POSSESSED_TOMMY_H__ \ No newline at end of file diff --git a/src/Prey/ai_reaction.cpp b/src/Prey/ai_reaction.cpp new file mode 100644 index 0000000..baf5151 --- /dev/null +++ b/src/Prey/ai_reaction.cpp @@ -0,0 +1,639 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +/////////////////////////////////////////// +// +// hhReactionDesc +// +/////////////////////////////////////////// + + +// +// CausedToStr() +// +idStr hhReactionDesc::CauseToStr(Cause c) { + switch(c) { + case Cause_Damage: + return idStr("Damage"); + case Cause_Touch: + return idStr("Touch"); + case Cause_Use: + return idStr("Use"); + case Cause_None: + return idStr("None"); + case Cause_PlayAnim: + return idStr("PlayAnim"); + case Cause_Telepathic_Trigger: + return idStr("TelepathicTrigger"); + case Cause_Telepathic_Throw: + return idStr("TelepathicThrow"); + } + + return idStr("!!UNDEFINED!!"); +} + +// +// EffectToStr() +// +idStr hhReactionDesc::EffectToStr(Effect e) { + switch(e) { + case Effect_Damage: + return idStr("Damage"); + case Effect_DamageEnemy: + return idStr("DamageEnemy"); + case Effect_VehicleDock: + return idStr("VehicleDock"); + case Effect_Vehicle: + return idStr("Vehicle"); + case Effect_Heal: + return idStr("Heal"); + case Effect_HaveFun: + return idStr("HaveFun"); + case Effect_ProvideCover: + return idStr("ProvideCover"); + case Effect_BroadcastTargetMsgs: + return idStr("BroadcastTargetMsgs"); + case Effect_Climb: + return idStr("Climb"); + case Effect_Passageway: + return idStr("Passageway"); + case Effect_CallBackup: + return idStr("CallBackup"); + } + + return idStr("!!UNDEFINED!!"); +} + +// +// StrToCause() +// +hhReactionDesc::Cause hhReactionDesc::StrToCause(const char* str) { + for(int i=0;i 1) { + flags |= flagReq_KeyValue; + int firstSpace = keyString.Find(' '); + if( firstSpace >= 0 ) { + key = keyString.Left( firstSpace); + keyVal = keyString.Right( keyString.Length() - firstSpace -1); + } + } + else + gameLocal.Error("Invalide key for 'reaction_req_ley' value: %s", (const char*)keyString); + } +//MDC begin + keyString.Empty(); + if( keys.GetString( "finish_key", "", keyString)) { + if( keyString.Length() > 1 ) { + int firstSpace = keyString.Find(' '); + if( firstSpace >= 0 ) { + finish_key = keyString.Left( firstSpace ); + finish_val = keyString.Right( keyString.Length() - firstSpace - 1 ); + } + } + } +//MDC end + + idStr animString(""); + if(keys.GetString("req_anim", "", animString)) { + flags |= flagReq_Anim; + anim = animString; + } + + if(keys.GetBool("req_rangeattack")) { + flags |= flagReq_RangeAttack; + } + if(keys.GetBool("req_meleeattack")) { + flags |= flagReq_MeleeAttack; + } + if(keys.GetBool("req_can_see")) { + flags |= flagReq_CanSee; + } + + if(keys.GetBool("req_telepathic") || cause == hhReactionDesc::Cause_Telepathic_Throw || cause == hhReactionDesc::Cause_Telepathic_Trigger) { + flags |= flagReq_Telepathic; + } + + + // Effect volumes + effectVolumes.Clear(); + const idKeyValue *kv = keys.MatchPrefix("effect_volume", NULL); + while(kv) { + idStr effectVolName = kv->GetValue(); + if(effectVolName.Length() > 0) { + idEntity *e = gameLocal.FindEntity(effectVolName.c_str()); + if(!e) { + gameLocal.Error("Failed to find effect_volume named %s", effectVolName.c_str()); + } + if(!e->IsType(hhReactionVolume::Type)) { + gameLocal.Error("effect_volume named %s was of incorrect spawn type. Must be hhReactionVolume", effectVolName.c_str()); + } + + effectVolumes.AddUnique(static_cast(e)); + } + + // Move to next volume + kv = keys.MatchPrefix("effect_volume", kv); + } + + listenerRadius = keys.GetFloat("listener_radius", "-1.0f"); + listenerMinRadius = keys.GetFloat("listener_min_radius", "-1.0f"); + + // Store listener volumes + listenerVolumes.Clear(); + kv = keys.MatchPrefix("listener_volume", NULL); + while(kv) { + idStr listenVolName = kv->GetValue(); + if(listenVolName.Length() > 0) { + idEntity *e = gameLocal.FindEntity(listenVolName.c_str()); + if(!e) { + gameLocal.Error("Failed to find listener_volume named %s", listenVolName.c_str()); + } + if(!e->IsType(hhReactionVolume::Type)) { + gameLocal.Error("listener_volume named %s was of incorrect spawn type. Must be hhReactionVolume", listenVolName.c_str()); + } + + listenerVolumes.AddUnique(static_cast(e)); + } + + // Move to next volume + kv = keys.MatchPrefix("listener_volume", kv); + } + + // Store touchdir + touchDir = keys.GetString("touchdir",""); + + // Extract out touch offsets + kv = keys.MatchPrefix("touchoffset_", NULL); + touchOffsets.SetGranularity(1); + touchOffsets.Clear(); + idStr tmpStr; + idStr keyName; + int usIndex; + while(kv) { + tmpStr = kv->GetKey(); + usIndex = tmpStr.FindChar("touchoffset_", '_'); + keyName = tmpStr.Mid(usIndex+1, strlen(kv->GetKey())-usIndex-1); + touchOffsets.Set(keyName, kv->GetValue()); + + // Move to the next + kv = keys.MatchPrefix("touchoffset_", kv); + } + + // Extract out touch bounds + kv = keys.MatchPrefix("safebound_", NULL ); + while(kv) { + tmpStr = kv->GetKey(); + usIndex = tmpStr.FindChar("safebound_", '_'); + keyName = tmpStr.Mid(usIndex+1, strlen(kv->GetKey())-usIndex-1); + touchOffsets.Set(keyName, kv->GetValue()); + kv = keys.MatchPrefix("safebound_", kv); + } +} + +/* +================ +hhReactionDesc::Save +================ +*/ +void hhReactionDesc::Save(idSaveGame *savefile) const { + savefile->WriteString(name); + savefile->WriteInt(effect); + savefile->WriteInt(cause); + savefile->WriteInt(flags); + savefile->WriteFloat(effectRadius); + savefile->WriteFloat(effectMinRadius); + + int num = effectVolumes.Num(); + savefile->WriteInt(num); + for(int i = 0; i < num; i++) { + savefile->WriteObject( effectVolumes[i] ); + } + + savefile->WriteString(key); + savefile->WriteString(keyVal); + savefile->WriteString(anim); + savefile->WriteFloat(listenerRadius); + savefile->WriteFloat(listenerMinRadius); + + num = listenerVolumes.Num(); + savefile->WriteInt(num); + for(int i = 0; i < num; i++) { + savefile->WriteObject(listenerVolumes[i]); + } + + savefile->WriteDict(&touchOffsets); + savefile->WriteString(touchDir); + savefile->WriteString(finish_key); + savefile->WriteString(finish_val); +} + +/* +================ +hhReactionDesc::Restore +================ +*/ +void hhReactionDesc::Restore( idRestoreGame *savefile ) { + savefile->ReadString(name); + savefile->ReadInt(reinterpret_cast (effect)); + savefile->ReadInt(reinterpret_cast (cause)); + savefile->ReadInt(reinterpret_cast (flags)); + savefile->ReadFloat(effectRadius); + savefile->ReadFloat(effectMinRadius); + + int num; + savefile->ReadInt(num); + effectVolumes.SetNum(num); + for(int i = 0; i < num; i++) { + savefile->ReadObject(reinterpret_cast (effectVolumes[i])); + } + + savefile->ReadString(key); + savefile->ReadString(keyVal); + savefile->ReadString(anim); + savefile->ReadFloat(listenerRadius); + savefile->ReadFloat(listenerMinRadius); + + savefile->ReadInt(num); + listenerVolumes.SetNum(num); + for(int i = 0; i < num; i++) { + savefile->ReadObject(reinterpret_cast (listenerVolumes[i])); + } + + savefile->ReadDict(&touchOffsets); + savefile->ReadString(touchDir); + savefile->ReadString(finish_key); + savefile->ReadString(finish_val); +} + +/////////////////////////////////////////// +// +// hhReactionVolume +// +/////////////////////////////////////////// +CLASS_DECLARATION(idEntity, hhReactionVolume) +END_CLASS + +// +// Spawn() +// +void hhReactionVolume::Spawn() { + + flags = 0; + + if(spawnArgs.GetBool("players", "1")) { + flags |= flagPlayers; + } + + if(spawnArgs.GetBool("monsters", "1")) { + flags |= flagMonsters; + } + + if(flags == 0) { + gameLocal.Error("Reaction volume %s is not targetting players or monsters - Why even have this volume?", (const char*)name); + } + +} + +bool hhReactionVolume::IsValidEntity( idEntity *ent ) { + assert(GetPhysics() != NULL); + assert(flags != 0); // have to be looking for something - else whats the point? + + if ( !ent || ent->IsHidden() ) { + return false; + } + if(flags & flagMonsters && !ent->IsType(idAI::Type)) { + if(flags & flagPlayers && !ent->IsType(hhPlayer::Type)) { + return false; + } + } + idEntity * entityList[ MAX_GENTITIES ]; + int numEnts = gameLocal.clip.EntitiesTouchingBounds(GetPhysics()->GetAbsBounds(), CONTENTS_BODY, entityList, MAX_GENTITIES); + for(int i=0;i &outValidEnts) { + + assert(GetPhysics() != NULL); + assert(flags != 0); // have to be looking for something - else whats the point? + + idEntity * entityList[ MAX_GENTITIES ]; + + int numEnts = gameLocal.clip.EntitiesTouchingBounds(GetPhysics()->GetAbsBounds(), CONTENTS_BODY, entityList, MAX_GENTITIES); + for(int i=0;iIsHidden()) + continue; + if(flags & flagPlayers && entityList[i]->IsType(hhPlayer::Type)) { + outValidEnts.Append(entityList[i]); + if(ai_debugBrain.GetInteger()) { + gameRenderWorld->DebugArrow(colorMagenta, GetOrigin(), entityList[i]->GetOrigin(), 10, 1000); + } + continue; + } + } + + if(ai_debugBrain.GetInteger()) { + gameRenderWorld->DebugBounds(colorWhite, GetPhysics()->GetBounds(), GetOrigin(), 1000); + } +} + +/* +================ +hhReactionVolume::Save +================ +*/ +void hhReactionVolume::Save(idSaveGame *savefile) const { + savefile->WriteInt(flags); +} + +/* +================ +hhReactionVolume::Restore +================ +*/ +void hhReactionVolume::Restore(idRestoreGame *savefile) { + savefile->ReadInt(flags); +} + +/////////////////////////////////////////// +// +// hhReactionHandler +// +/////////////////////////////////////////// + + +// +// construction +// +hhReactionHandler::hhReactionHandler() { +} + + +// +// destruction +// +hhReactionHandler::~hhReactionHandler() { + reactionDescs.DeleteContents(TRUE); +} + +// +// LoadReactionDesc() +// +const hhReactionDesc* hhReactionHandler::LoadReactionDesc(const char* defName, const idDict *uniqueDescKeys) { + + // Ignore null reactions + if ( !defName || !defName[0] ) { + return NULL; + } + + const hhReactionDesc *ret = NULL; + + // If we do not have unqiue keys, we can just use a cached version + if(!uniqueDescKeys) { + ret = FindReactionDesc(defName); + if(ret) { + return ret; + } + } + + // Get the def dict + const idDict *reactDefDict = gameLocal.FindEntityDefDict(defName); + if(!reactDefDict) { + gameLocal.Error("Failed to find reaction dict named %s", defName); + } + + // Build the keys that will be passed onto the creation + idDict finalDict; + finalDict.Copy(*reactDefDict); + StripReactionPrefix(finalDict); + if(uniqueDescKeys) { + idDict uniqueCopy(*uniqueDescKeys); + StripReactionPrefix(uniqueCopy); + finalDict.Copy(uniqueCopy); + } + + hhReactionDesc* react = CreateReactionDesc(defName, finalDict); + return react; +} + +// +// createReactionDesc() +// +hhReactionDesc* hhReactionHandler::CreateReactionDesc(const char *desiredName, const idDict &keys) { + + idStr finalName = GetUniqueReactionDescName(desiredName); + hhReactionDesc *desc = new hhReactionDesc(); + + desc->name = finalName; + desc->Set(keys); + + reactionDescs.Append(desc); + return desc; +} + +// +// getUniqueReactionDescName() +// +idStr hhReactionHandler::GetUniqueReactionDescName(const char *name) { + + int count = 0; + idStr finalName(name); + while(1) { + if(count > 0) { + finalName = idStr(name) + idStr("_") + idStr(count); + } + + // Did we find a name that was not taken? + if(FindReactionDesc(finalName.c_str()) == NULL) { + return finalName; + } +#ifdef _DEBUG + if(count > 128) { + assert(FALSE); // How the hell did we get so many copies? + } +#endif + + count++; + } +} + +// +// findReactionDesc() +// +const hhReactionDesc* hhReactionHandler::FindReactionDesc(const char *name) { + + for(int i=0;iname.c_str(), name) == 0) { + return reactionDescs[i]; + } + } + + return NULL; +} + +// +// stripReactionPrefix() +// +void hhReactionHandler::StripReactionPrefix(idDict &dict) { + const idKeyValue *kv = NULL;//dict.MatchPrefix("reaction"); + + idDict newDict; + + for(int i=0;iGetKey(); + + // Do we have a reaction token to strip out? + if(idStr::FindText(key.c_str(), "reaction") != -1) { + + int endPrefix = idStr::FindChar(key.c_str(), '_'); + if(endPrefix == -1) { + gameLocal.Error("reactionX_ prefix not found."); + } + idStr realKey(key); + realKey = key.Mid(endPrefix+1, key.Length() - endPrefix - 1); + key = realKey; + } + + //dict.Delete(kv->GetKey().c_str()); + newDict.Set(key.c_str(), kv->GetValue()); + + kv = dict.MatchPrefix("reaction", kv); + } + + + dict.Clear(); + dict.Copy(newDict); +} + +void hhReactionHandler::Save(idSaveGame *savefile) const { + savefile->WriteInt(reactionDescs.Num()); + for (int i = 0; i < reactionDescs.Num(); i++) { + reactionDescs[i]->Save(savefile); + } +} + +void hhReactionHandler::Restore( idRestoreGame *savefile ) { + reactionDescs.DeleteContents(true); + int num; + savefile->ReadInt(num); + reactionDescs.SetNum(num); + for (int i = 0; i < num; i++) { + reactionDescs[i] = new hhReactionDesc(); + reactionDescs[i]->Restore(savefile); + } +} + +void hhReaction::Save(idSaveGame *savefile) const { + savefile->WriteBool(active); + exclusiveOwner.Save(savefile); + savefile->WriteInt(exclusiveUntil); + if (desc) { + savefile->WriteString(desc->name); + } else { + savefile->WriteString(""); + } + causeEntity.Save(savefile); + int num = effectEntity.Num(); + savefile->WriteInt(num); + for(int i = 0; i < num; i++) { + effectEntity[i].ent.Save(savefile); + savefile->WriteFloat(effectEntity[i].weight); + } +} + +void hhReaction::Restore(idRestoreGame *savefile) { + savefile->ReadBool(active); + exclusiveOwner.Restore(savefile); + savefile->ReadInt(exclusiveUntil); + + idStr name; + savefile->ReadString(name); + desc = gameLocal.GetReactionHandler()->FindReactionDesc(name); + causeEntity.Restore(savefile); + int num; + savefile->ReadInt(num); + hhReactionTarget ent; + for(int i = 0; i < num; i++) { + ent.ent.Restore(savefile); + savefile->ReadFloat(ent.weight); + effectEntity.Append(ent); + } +} + diff --git a/src/Prey/ai_reaction.h b/src/Prey/ai_reaction.h new file mode 100644 index 0000000..5385afe --- /dev/null +++ b/src/Prey/ai_reaction.h @@ -0,0 +1,261 @@ +#ifndef __HH_REACTION_H +#define __HH_REACTION_H + + +// +// hhReactionTarget +// +class hhReactionTarget +{ +public: + hhReactionTarget() { + weight = 1.0f; + ent = NULL; + } + + hhReactionTarget(idEntity *e, float w = 1.0f) { + ent = e; + weight = w; + } + + idEntityPtr ent; // The entity that is effected + float weight; // The degree ent is effected +}; + +// +// hhReactionVolume +// +class hhReactionVolume : public idEntity { +public: + CLASS_PROTOTYPE( hhReactionVolume ); + + enum { + flagPlayers = (1<<0), + flagMonsters = (1<<1), + flagActors = (flagPlayers|flagMonsters), + }; + + void Spawn(void); + + // Gets all of the entities (filtered by validFlags) that are inside this volume + void GetValidEntitiesInside(idList &outValidEnts); + bool IsValidEntity( idEntity *ent ); + + void Save(idSaveGame *savefile) const; + void Restore(idRestoreGame *savefile); + +protected: + int flags; // Flags for this volume +}; + +// +// hhReactionDesc +// +// Holds the unchanging properties of a reaction +// +class hhReactionDesc +{ +public: + friend class hhReactionHandler; + hhReactionDesc() { + name = idStr(""); + effect = Effect_Invalid; + cause = Cause_Invalid; + flags = 0; + effectRadius = -1.0f; + effectMinRadius = -1.0f; + effectVolumes.SetGranularity(1); + key = idStr(""); + keyVal = idStr(""); + anim = idStr(""); + listenerRadius = -1.0f; + listenerMinRadius = -1.0f; + listenerVolumes.SetGranularity(1); + } + + enum Cause { + Cause_Invalid = -1, + Cause_Damage, // AI can damage this to get reaction + Cause_Touch, // AI can touch this to get reaction + Cause_Use, // AI can 'use' this to get reaction + Cause_None, // AI does nothing, and the effect is always felt + Cause_PlayAnim, // AI must play an anim to get the desired effect + Cause_Telepathic_Trigger, // AI must use their telepathy to trigger this entity + Cause_Telepathic_Throw, // AI must use their telepathy to physically throw this entity + + Cause_Total + }; + + enum Effect { + Effect_Invalid = -1, + Effect_Damage, // Causes damage (reduces health) + Effect_DamageEnemy, // Causes damage (reduces health) + Effect_VehicleDock, // Causes damage (reduces health) + Effect_Vehicle, // Causes damage (reduces health) + Effect_Heal, // Heals entity (increases health) + Effect_HaveFun, // Something 'fun' for this entity to do + Effect_ProvideCover, // Indicates this entity can possibily provide 'cover' for a monster + Effect_BroadcastTargetMsgs, // This entity will have its target's msg's sent + Effect_Climb, // This entity will let the monster climb it. + Effect_Passageway, // This entity is a harvester passageway + Effect_CallBackup, // Calls for help + + Effect_Total + }; + + enum { + flag_EffectAllPlayers = (1<<0), // This entity automatically targets ALL entities of type hhPlayer + flag_EffectAllMonsters = (1<<1), // This entity automatically targets ALL entities of typehhAI + flag_Exclusive = (1<<2), // This msg is exclusive, and once a monster has committed to it - NO one else should listen to it + flag_EffectListener = (1<<3), // The effect of this reaction is applied to the listener + flag_AnimFaceCauseDir = (1<<4), // When playing an anim for this reaction, face the direction of our cause + flag_AnimTriggerCause = (1<<5), // When playing an anim for this reaction, trigger the cause entity + flag_SnapToPoint = (1<<6), // This msg is only for monsters that are telepathic + flagReq_MeleeAttack = (1<<8), // This msg is for monsters that can melee attack + flagReq_RangeAttack = (1<<9), // This msg is for monsters that can range attack + flagReq_Anim = (1<<10), // This msg is for monsters that have a given anim (anim) + flagReq_KeyValue = (1<<11), // This msg is for monsters that have a given key set (key, keyVal) + flagReq_NoVehicle = (1<<12), // This msg is for monsters that are NOT in vehicles + flagReq_CanSee = (1<<13), // This msg is only for monsters that can see the cause entity + flagReq_Telepathic = (1<<14), // This msg is only for monsters that are telepathic + }; + + + static idStr CauseToStr(Cause c); // Converts activate enum into readable form + static idStr EffectToStr(Effect e); // Converts reaction enum to readable form + static Cause StrToCause(const char* str); // Converts string to caused by enum or Caused_Invalid if not found + static Effect StrToEffect(const char* str); // Converts string to effect enum or Effect_Invalid if not found + void Set(const idDict &keys); // Sets this description to the given key values + bool CauseRequiresPathfinding(void) const {return cause == Cause_Touch || cause == Cause_Use || cause == Cause_PlayAnim;} + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + idStr name; // unique name for each reaction desc + Effect effect; // pdm: for reaction system + Cause cause; // pdm: for reaction system + unsigned int flags; // jrm: reaction flags for reaction system + float effectRadius; // jrm: If > 0.0f then ONLY target entities that are at most this far away + float effectMinRadius; // jrm: if > 0.0f then ONLY target entities that are at least this far away + idList effectVolumes; // jrm: if valid, then only entities that are inside one of these volumes are valid 'effect' entities + idStr key; // jrm: the key required for a monster to be eligable for this reaction msg (ReactionFlag_KeyReq must be set) + idStr keyVal; // jrm: the value of the reaction key required + idStr anim; // jrm: the value of the reaction key required + float listenerRadius; // jrm: listeners farther away than this distance will not receive this reaction + float listenerMinRadius; // jrm: listeners closer than this dist will not receive this reaction + idList listenerVolumes; // jrm: only listeners that are inside one of these volumes will receive this reaction + idDict touchOffsets; // Example: Use "monster_hunter" as key to get touchOffset values + idStr touchDir; // Indicates how a monster should approach the cause entity + + idStr finish_key; // mdc: used to set a key on monster after finishing a reaction + idStr finish_val; // mdc: used to set a key on monster after finishing a reaction +}; + +// +// hhReaction +// +// Describes a reaction ready to be processed by the AI. Holds a description of the reaction, as well as the cause/effect entities. +// Do the cause describe by to to get the effect in applied to +// +class hhReaction +{ +public: + hhReaction() + { + desc = NULL; + causeEntity = NULL; + active = TRUE; + exclusiveOwner = NULL; + exclusiveUntil = -1; + effectEntity.Clear(); + }; + hhReaction(const hhReactionDesc *reactDesc, idEntity *cause, const hhReactionTarget &effect) + { + active = TRUE; + desc = reactDesc; + causeEntity = cause; + exclusiveUntil = -1; + exclusiveOwner = NULL; + effectEntity.Append(effect); + } + + bool IsActive() const {return active;} + bool IsActiveForListener(idEntity *listener) const { + if(!active) { + return FALSE; + } + + // If someone has claimed this, and their claim is still valid... Only active for that particular AI + if(exclusiveOwner.GetEntity() && gameLocal.GetTime() < exclusiveUntil) { + return exclusiveOwner.GetEntity() == listener; + } + + // No one has claimed us, or time has expired, so anyone can claim us now + return TRUE; + }; + + bool ClaimExclusivity(idEntity *claimer, int ms=1000) { + + // Cannot claim, someone else already has it + if(exclusiveOwner.GetEntity() && gameLocal.GetTime() < exclusiveUntil) { + return FALSE; + } + + // They claimed it now.... + exclusiveOwner = claimer; + exclusiveUntil = gameLocal.GetTime() + ms; + return TRUE; + } + + void Save(idSaveGame *savefile) const; + void Restore(idRestoreGame *savefile); + + bool active; // TRUE to be considered for sending + idEntityPtr exclusiveOwner; // The AI that has 'claimed' this reaction + int exclusiveUntil; // Until gameLocal.time has reached this value, this reaction does not send msgs (except for exclusive reactions) + const hhReactionDesc* desc; // Info about the kind of reaction this is + idEntityPtr causeEntity; // The entity to apply the cause to + idList effectEntity; // The effect of doing the cause is felt by these entities +}; + + +// +// hhReactionHandler +// +// Manages all of the reaction descs currently loaded +// +class hhReactionHandler +{ +public: + hhReactionHandler(); + virtual ~hhReactionHandler(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + // Loads the given reaction def name. If uniqueDescKeys are specified, a unique hhReactionDesc* is guarenteed to be returned. + // Otherwise, if no override keys are specified and a reaction of the given name is found, it is returned without creating a new one + // The returned desc is ONLY a desc, it is not meant to be modified in any way + const hhReactionDesc* LoadReactionDesc(const char* defName, const idDict &uniqueDescKeys) { + if(uniqueDescKeys.GetNumKeyVals() <= 0) { + return LoadReactionDesc(defName, NULL); + } else { + return LoadReactionDesc(defName, &uniqueDescKeys); + } + } + const hhReactionDesc* LoadReactionDesc(const char* defName) {return LoadReactionDesc(defName, NULL);} + +protected: + friend class hhReaction; + + // Creates a new reaction desc with the given name and keys to describe the reaction + hhReactionDesc* CreateReactionDesc(const char *name, const idDict &keys); + idStr GetUniqueReactionDescName(const char *name); + const hhReactionDesc* FindReactionDesc(const char *name); + void StripReactionPrefix(idDict &dict); // Removes any "reaction_" or "reaction1_" prefix from the given dict + const hhReactionDesc* LoadReactionDesc(const char* defName, const idDict *uniqueDescKeys); + + + idList reactionDescs; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/ai_spawncase.cpp b/src/Prey/ai_spawncase.cpp new file mode 100644 index 0000000..105b7be --- /dev/null +++ b/src/Prey/ai_spawncase.cpp @@ -0,0 +1,227 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// +// hhAISpawnCase +// + +const idEventDef EV_CreateAI( "", NULL); +const idEventDef EV_AutoTrigger( "", NULL); + +CLASS_DECLARATION( hhAnimated, hhAISpawnCase ) + EVENT( EV_Activate, hhAISpawnCase::Event_Trigger ) + EVENT( EV_CreateAI, hhAISpawnCase::Event_CreateAI ) + EVENT( EV_AutoTrigger, hhAISpawnCase::Event_AutoTrigger ) +END_CLASS + +// +// Spawn() +// +void hhAISpawnCase::Spawn( void ) { + + triggerToggle = FALSE; + aiSpawnCount = 0; + waitingForAutoTrigger = FALSE; + triggerQueue = 0; + + CreateEntSpawnArgs(); + + GetPhysics()->SetContents(CONTENTS_SHOOTABLE|CONTENTS_PLAYERCLIP|CONTENTS_MOVEABLECLIP|CONTENTS_IKCLIP|CONTENTS_SHOOTABLEBYARROW); + + // Next frame, create the AI to attach to this case - Wait a frame so that we can copy targets + PostEventMS(&EV_CreateAI, 0); +} + +// +// Event_CreateAI() +// +void hhAISpawnCase::Event_CreateAI( void ) { + + // Spawn the monster + const char *monsterName = spawnArgs.GetString("def_monster", NULL); + int i; + if ( aiSpawnCount < spawnArgs.GetInt("max_monsters", "1") && monsterName && monsterName[0]) { + idDict args = entSpawnArgs; + + idVec3 offset = spawnArgs.GetVector("monster_offset", "0 0 0"); + offset *= GetPhysics()->GetAxis(); + + args.SetVector( "origin", GetPhysics()->GetOrigin() + offset); + args.SetBool("encased", TRUE); + args.Set("encased_idle", spawnArgs.GetString("anim_encased_monster_idle", "encased_idle")); + args.Set("encased_exit", spawnArgs.GetString("anim_encased_monster_exit", "encased_exit")); + args.Set( "encased_exit_offset", spawnArgs.GetString( "encased_exit_offset", "0 0 0" ) ); + args.SetMatrix("rotation", GetAxis()); + encasedAI = static_cast(gameLocal.SpawnObject(monsterName, &args)); + if(!encasedAI.GetEntity()) { + //gameLocal.Warning("No monster specified for case"); + return; + } + + // Copy the targets that our case has to our newly spawned monster + for( i = 0; i < targets.Num(); i++ ) { + encasedAI->targets.AddUnique(targets[i]); + } + if ( spawnArgs.GetBool( "rotated" ) ) { + encasedAI->SetAxis( GetAxis() ); + encasedAI->GetPhysics()->SetAxis( GetAxis() ); + encasedAI->GetRenderEntity()->axis = GetAxis(); + encasedAI->viewAxis = GetAxis(); + encasedAI->Bind(this, true); + } else { + encasedAI->viewAxis = GetAxis(); + encasedAI->Bind(this, FALSE); + } + aiSpawnCount++; + + if(spawnArgs.GetBool("monster_hide", "0")) { + encasedAI->ProcessEvent(&EV_Hide); + } + } +} + +// +// Event_Trigger() +// +void hhAISpawnCase::Event_Trigger( idEntity *activator ) { + bool triggeredAI = false; + bool showedAI = false; + + // We are waiting for an auto-trigger, so don't fire now - but remember that we need to later + if(waitingForAutoTrigger) { + triggerQueue++; + //gameLocal.Printf("\nQUEUED TRIGGERS: %i", triggerQueue); + return; + } + + //gameLocal.Printf("\n * TRIGGERED *"); + if(encasedAI.GetEntity()) { + if(encasedAI->IsHidden()) { + encasedAI->ProcessEvent(&EV_Show); + showedAI = true; + } + + //if triggered_spawn is set, spawn the encasedAI on the first trigger, + //and but don't activate it until a second trigger + if ( !showedAI || !spawnArgs.GetBool( "triggered_spawn", "0" ) ) { + encasedAI->Unbind(); + // removed since monster_offset should take care of this + //encasedAI->SetOrigin(encasedAI->GetOrigin() + encasedAI->viewAxis[0] * 64); + encasedAI.GetEntity()->ProcessEvent(&EV_Activate, activator); + triggeredAI = true; + } + } + + // Start the anim playing + hhAnimated::Event_Activate(activator); + + if ( spawnArgs.GetInt( "no_anim", "0" ) ) { + ProcessEvent(&EV_CreateAI); + //if triggered_spawn is set, dont show the next AI until the next trigger + if ( encasedAI.IsValid() && triggeredAI && spawnArgs.GetBool( "triggered_spawn", "0" ) ) { + encasedAI->ProcessEvent(&EV_Hide); + } + } + triggerToggle = !triggerToggle; +} + +// +// Event_AutoTrigger() +// +void hhAISpawnCase::Event_AutoTrigger( void ) { + waitingForAutoTrigger = FALSE; + ProcessEvent(&EV_Activate, this); +} + +// +// Event_AnimDone() +// +void hhAISpawnCase::Event_AnimDone( int animIndex ) { + + // Call back first + hhAnimated::Event_AnimDone(animIndex); + + const char *n = NULL; + + // Door is OPEN, now we queue up the CLOSE anim + if(triggerToggle) { + n = spawnArgs.GetString("anim_retrigger"); + + // Should we automatically retrigger? (ie. close the door after it was opened?) + if(spawnArgs.GetFloat("auto_retrigger_delay", "-1") >= 0.0f ) { + int delay = SEC2MS(spawnArgs.GetFloat("auto_retrigger_delay", "-1")); + waitingForAutoTrigger = TRUE; + PostEventMS(&EV_AutoTrigger, delay); + } + } + // Door is CLOSED, now we queue up the OPEN anim + else { + + // If we have queued triggers saved up - then lets fire one now since we are now closed + if(triggerQueue > 0) { + triggerQueue--; + PostEventMS(&EV_Activate, 0, this); + //gameLocal.Printf("\nQUEUED TRIGGERS: %i", triggerQueue); + } + n = spawnArgs.GetString("anim"); + int maxMonsters = spawnArgs.GetInt("max_monsters", "1"); + if(maxMonsters < 0 || aiSpawnCount < maxMonsters) { + Event_CreateAI(); + } else { + //gameLocal.Printf("\nMax monsters reached."); + } + } + + anim = GetAnimator()->GetAnim( n ); + HH_ASSERT( anim ); +} + +/* +================ +hhAISpawnCase::Save +================ +*/ +void hhAISpawnCase::Save( idSaveGame *savefile ) const { + encasedAI.Save( savefile ); + savefile->WriteBool( triggerToggle ); + savefile->WriteInt( aiSpawnCount ); + savefile->WriteBool( waitingForAutoTrigger ); + savefile->WriteInt( triggerQueue ); +} + +/* +================ +hhAISpawnCase::Restore +================ +*/ +void hhAISpawnCase::Restore( idRestoreGame *savefile ) { + encasedAI.Restore( savefile ); + savefile->ReadBool( triggerToggle ); + savefile->ReadInt( aiSpawnCount ); + savefile->ReadBool( waitingForAutoTrigger ); + savefile->ReadInt( triggerQueue ); + + CreateEntSpawnArgs(); +} + +/* +================ +hhAISpawnCase::CreateEntSpawnArgs +================ +*/ +void hhAISpawnCase::CreateEntSpawnArgs( void ) { + // Create list of spawn args, copied to encasedAI in Event_CreateAI + entSpawnArgs.Clear(); + idStr tmpStr, realKeyName; + const idKeyValue *kv = spawnArgs.MatchPrefix("ent_", NULL); + while(kv) { + tmpStr = kv->GetKey(); + int usIndex = tmpStr.FindChar("ent_", '_'); + realKeyName = tmpStr.Mid(usIndex+1, strlen( kv->GetKey())-usIndex-1); + entSpawnArgs.Set(realKeyName, kv->GetValue()); + kv = spawnArgs.MatchPrefix("ent_", kv); + } +} diff --git a/src/Prey/ai_spawncase.h b/src/Prey/ai_spawncase.h new file mode 100644 index 0000000..59fb264 --- /dev/null +++ b/src/Prey/ai_spawncase.h @@ -0,0 +1,34 @@ +#ifndef ai_spawncase_H +#define ai_spawncase_H + +// +// hhAISpawnCase +// +// Spawns a specific monster, attaches to model - waits for trigger before releasing monster +// +class hhAISpawnCase : public hhAnimated { + +public: + CLASS_PROTOTYPE(hhAISpawnCase); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Event_Trigger( idEntity *activator ); + virtual void Event_AnimDone( int animIndex ); + +protected: + void Event_CreateAI( void ); + void Event_AutoTrigger( void ); + + void CreateEntSpawnArgs( void ); + + idEntityPtr encasedAI; // The AI that is currently encased and waiting to come out + bool triggerToggle; // Toggles back and forth to indicated which anim to play (open or close) + int aiSpawnCount; // Current number of monsters spawned + bool waitingForAutoTrigger; // True if we are waiting for an auto-trigger msg to come in (from auto_retrigger_delay setting) + int triggerQueue; // Number of triggers that have occurred during the delay period that must be re-sent + idDict entSpawnArgs; // Spawn args to copy to spawned entities +}; + +#endif \ No newline at end of file diff --git a/src/Prey/ai_speech.cpp b/src/Prey/ai_speech.cpp new file mode 100644 index 0000000..86c6158 --- /dev/null +++ b/src/Prey/ai_speech.cpp @@ -0,0 +1,403 @@ +// +// ai_speech.cpp +// +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +extern idCVar ai_skipSpeech; + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +////////////////////////////////////////////////////////////////////// +// +// hhAISpeech +// +////////////////////////////////////////////////////////////////////// + +// jrm: none yet + + + +////////////////////////////////////////////////////////////////////// +// +// hhAISpeechHandler +// +////////////////////////////////////////////////////////////////////// + +float hhAISpeechHandler::TokenMatchWeight[] = {3.0f, 2.0f, 1.0f}; +float hhAISpeechHandler::TokenWildcardMatchWeight[] = {1.5f, 1.0f, 0.5f}; +#define TokenWildCard "*" +#define SpeechShaderPrefix "snd_speech_" // IF THIS CHANGES YOU MUST CHANGE THE LENGTH DEFINE! +#define SpeechShaderPrefixLen 11 +#define FreqToken ':' +#define FreqTokenStr ":" + +// +// Construction +// +hhAISpeechHandler::hhAISpeechHandler() +{ + firstSpeech = NULL; + lastSpeech = NULL; + nextSpeechTime = 0; +}; + +// +// Update() +// +void hhAISpeechHandler::Update() +{ + hhAISpeech *currSpeech = firstSpeech; + hhAISpeech *nextSpeech = NULL; + + if(ai_skipSpeech.GetBool()) + return; + + //for( currSpeech = speechQueue.Next(); currSpeech != NULL; currSpeech = nextSpeech ) + while(currSpeech != NULL) + { + // We can't play any more speech yet! + if(gameLocal.GetTime() < nextSpeechTime) + return; + + nextSpeech = currSpeech->next; + + // Has this speech expired? + if(gameLocal.GetTime() > currSpeech->expireTime) + { + RemoveSpeechFromQueue(currSpeech); + currSpeech = nextSpeech; + continue; + } + + // Play the speech! + currSpeech->speaker->StartSound(currSpeech->snd, SND_CHANNEL_VOICE, 0, true, NULL); + + // Debug Start + /* + idStr speechStr; + sprintf(speechStr, "\"%s\"", (const char*)currSpeech->snd); + speechStr.ToLower(); + gameRenderWorld->DrawText(speechStr, currSpeech->speaker->GetOrigin() + idVec3(0.0f, 0.0f, 12.0f), 0.5f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3(),1,1000); + gameLocal.Printf("\n%.01f, %s says %s", (float)MS2SEC(gameLocal.GetTime()), (const char*)currSpeech->speaker->name, (const char*)speechStr); + */ + // Debug End + + RemoveSpeechFromQueue(currSpeech); + nextSpeechTime = gameLocal.GetTime() + MinSpeechWaitTime; + + currSpeech = nextSpeech; + } + + + +}; + +// +// PlaySpeech() +// +bool hhAISpeechHandler::PlaySpeech(idAI *source, idStr sndShd, int expireDelay, bool allowQueue) +{ + if(ai_skipSpeech.GetBool()) + return FALSE; + + if(!allowQueue) { + // just play it and return + HH_ASSERT(FALSE); // Code not done yet! + return TRUE; + } + + // Do we have more slots availble? + hhAISpeech *freeSpeech = GetFreeSpeech(); + if(!freeSpeech) + return FALSE; + + // Setup the new speech + freeSpeech->speaker = source; + freeSpeech->expireTime = gameLocal.GetTime() + expireDelay; + freeSpeech->snd = sndShd; + AddSpeechToQueue(freeSpeech); + return TRUE; +}; + +// +// ClearSpeech() +// +void hhAISpeechHandler::ClearSpeech() +{ + firstSpeech = NULL; + lastSpeech = NULL; + + for(int i=0;inext = NULL; + speech->prev = NULL; + firstSpeech = speech; + lastSpeech = speech; + return; + } + + // Add it to the end of the list + HH_ASSERT(lastSpeech && lastSpeech->next == NULL); + lastSpeech->next = speech; + speech->prev = lastSpeech; + speech->next = NULL; + lastSpeech = speech; +} + +// +// RemoveSpeechFromQueue() +// +void hhAISpeechHandler::RemoveSpeechFromQueue(hhAISpeech *speech) +{ + speech->Clear(); + + // Remove from one of the ends? + if(speech == firstSpeech || speech == lastSpeech) { + if(speech == firstSpeech) { + firstSpeech = firstSpeech->next; + if(firstSpeech) + firstSpeech->prev = NULL; + } + + if(speech == lastSpeech) { + lastSpeech = lastSpeech->prev; + if(lastSpeech) + lastSpeech->next = NULL; + } + return; + } + + + // Middle? + if(speech->next) + speech->next->prev = speech->prev; + if(speech->prev) + speech->prev->next = speech->next; +} + +// +// GetSpeechFrequency() +// +float hhAISpeechHandler::GetSpeechFrequency(idStr sndShader) +{ + if(ai_skipSpeech.GetBool()) + return 0.0f; + + int firstBreak = sndShader.Find(FreqToken); + if(firstBreak == -1) + return 1.0f; + + int secondBreak = sndShader.Find(FreqTokenStr, true, firstBreak); + if(secondBreak == -1) + secondBreak = sndShader.Length(); + + idStr freqStr = sndShader.Mid(firstBreak, secondBreak - firstBreak); + + int f = atoi((const char*)freqStr); + return idMath::ClampFloat(0.0f, 1.0f, (float(f) * 0.01f)); +}; + +// +// GetSpeechResponseDelay() +// +int hhAISpeechHandler::GetSpeechResponseDelay(idStr sndShader) +{ + // TODO: Fill this code in when we actually need it. Does 1 second work all the time? + + return 500; +}; + +// +// GetSpeechSoundShader() +// +bool hhAISpeechHandler::GetSpeechSoundShader(idAI *source, idStr speechDesc, idStr &outClosestSndShd) +{ + HH_ASSERT(source != NULL); + + const idKeyValue *kv = NULL; + + if(ai_skipSpeech.GetBool()) + return FALSE; + + // Quick out - do we have this EXACT snd shader? + idStr sndShader = SpeechShaderPrefix + speechDesc; + kv = source->spawnArgs.MatchPrefix(sndShader); + if(kv) { + outClosestSndShd = kv->GetKey(); + return TRUE; + } + + // Extrace the tokens for our 'desired' speech pattern + SpeechTokens desired; + if(!ExtractSpeechTokens(speechDesc, desired)) + return FALSE; + + // Find our best potential speech pattern match + float bestRating = 0.0f; + float currRating = -1.0f; + const idKeyValue *bestKey = NULL; + SpeechTokens currPotential; + + kv = source->spawnArgs.MatchPrefix(SpeechShaderPrefix); + + while(kv) + { + idStr speechStr; + speechStr = kv->GetKey().Mid(SpeechShaderPrefixLen, kv->GetKey().Length() - SpeechShaderPrefixLen); + + if(!ExtractSpeechTokens(speechStr, currPotential)) + currRating = -1.0f; + else + currRating = GetMatchRating(desired, currPotential); + + if(currRating > bestRating) + { + bestRating = currRating; + bestKey = kv; + } + + kv = source->spawnArgs.MatchPrefix(SpeechShaderPrefix,kv); + } + + // We've found a match to some degree + if(bestKey && bestRating > 0.0f) + { + outClosestSndShd = bestKey->GetKey(); + return TRUE; + } + + return FALSE; +}; + +// +// GetMatchRating() +// +float hhAISpeechHandler::GetMatchRating(SpeechTokens &desired, SpeechTokens &potential) +{ + float rating = 0.0f; + + for(int i=0;i= 0) + outTokens.tokens[TT_What] = outTokens.tokens[TT_What].Mid(0, outTokens.tokens[TT_What].Length() - (findFreqBreak+1)); + + return true; +}; + +/* +================ +hhAISpeechHandler::Save +================ +*/ +void hhAISpeechHandler::Save(idSaveGame *savefile) const { + for (int i = 0; i < SpeechPoolSize; i++) { + savefile->WriteObject(speechPool[i].speaker); + savefile->WriteString(speechPool[i].snd); + savefile->WriteInt(speechPool[i].expireTime); + savefile->WriteInt(GetPoolIndex(speechPool[i].next)); + savefile->WriteInt(GetPoolIndex(speechPool[i].prev)); + } + + savefile->WriteInt(GetPoolIndex(firstSpeech)); + savefile->WriteInt(GetPoolIndex(lastSpeech)); + savefile->WriteInt(nextSpeechTime); +} + +/* +================ +hhAISpeechHandler::Restore +================ +*/ +void hhAISpeechHandler::Restore(idRestoreGame *savefile) { + int tmp; + for (int i = 0; i < SpeechPoolSize; i++) { + savefile->ReadObject(reinterpret_cast (speechPool[i].speaker)); + savefile->ReadString(speechPool[i].snd); + savefile->ReadInt(speechPool[i].expireTime); + savefile->ReadInt(tmp); + speechPool[i].next = (tmp == -1 ? NULL : &speechPool[tmp]); + savefile->ReadInt(tmp); + speechPool[i].prev = (tmp == -1 ? NULL : &speechPool[tmp]); + } + + savefile->ReadInt(tmp); + firstSpeech = (tmp == -1 ? NULL : &speechPool[tmp]); + savefile->ReadInt(tmp); + lastSpeech = (tmp == -1 ? NULL : &speechPool[tmp]); + savefile->ReadInt(nextSpeechTime); +} + + +int hhAISpeechHandler::GetPoolIndex(hhAISpeech *obj) const { + if (!obj) { + return -1; + } + + for (int i = 0; i < SpeechPoolSize; i++) { + if (obj == &speechPool[i]) { + return i; + } + } + + HH_ASSERT(!"Couldn't find pool index!"); + return -1; +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_speech.h b/src/Prey/ai_speech.h new file mode 100644 index 0000000..8aa2e63 --- /dev/null +++ b/src/Prey/ai_speech.h @@ -0,0 +1,102 @@ +#ifndef __HH_AISPEECH_H +#define __HH_AISPEECH_H + + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +// +// hhAISpeech +// +class hhAISpeech +{ +public: + hhAISpeech() {Clear(); next = NULL; prev = NULL; } + + void Clear(void) {speaker = NULL; expireTime = -1;} + bool IsCleared(void) {return speaker == NULL;} // Assumes speech will NEVER come from a 'null' speaker, NULL = empty + + idAI* speaker; + idStr snd; + int expireTime; + + hhAISpeech *next; + hhAISpeech *prev; +}; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +// +// hhAISpeechHandler +// +class hhAISpeechHandler +{ +public: +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + enum + { + SpeechPoolSize = 8, + MinSpeechWaitTime = 2000, // We have to wait at LEAST this long before we can play ANY more speech + }; + + hhAISpeechHandler(); + virtual ~hhAISpeechHandler() { } + virtual void Update(); // note: not named Think() on purpose, because we are NOT an entity + + // Play speech from a given AI - returns true if speech was queued - DOES NOT mean it is guarenteed to play + bool PlaySpeech(idAI *source, idStr sndShader, int expireDelay = 1000, bool allowQueue = true); + + // Finds the matching snd shader to the speechDesc given, as well as 0->1 of the frequency for this speech + // Returns FALSE if no possible match could be found. + bool GetSpeechSoundShader(idAI *source, idStr speechDesc, idStr &outClosestSndShd); + + // From a sound shader name, return the frequency from 0->1 + float GetSpeechFrequency(idStr sndShader); + + // From a sound shader name, return the delay in MS of how long it takes to 'comprehend' the given shader. + // Example: Someone says "Take the shuttle!" and we want the person taking the shuttle to wait at least 1 second before they do it + // so it looks like they comprehended the command + int GetSpeechResponseDelay(idStr sndShader); + + // Remove all queued speech + void ClearSpeech(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + hhAISpeech speechPool[SpeechPoolSize]; // Pool of all possible speech slots + hhAISpeech* GetFreeSpeech(); // Returns the first empty speech obj available in the pool + int ClearExpiredSpeech(); // Clears out any expired speech in the pool + + enum TokenType + { + TT_Desire = 0, // The desire we are going to satisfy + TT_How, // How we are going to satisfy it + TT_What, // What entity we will use to satisfy it + + TT_Total, + }; + + struct SpeechTokens + { + idStr tokens[TT_Total]; + }; + + static float TokenMatchWeight[TT_Total]; + static float TokenWildcardMatchWeight[TT_Total]; + + // Returns a 0+ rating of 'how' close the potential speech matches the desired speech + float GetMatchRating(SpeechTokens &desired, SpeechTokens &potential); + bool ExtractSpeechTokens(idStr speechDesc, SpeechTokens &outTokens); + + // because I hate id's linked list + hhAISpeech *firstSpeech; + hhAISpeech *lastSpeech; + void AddSpeechToQueue(hhAISpeech *speech); + void RemoveSpeechFromQueue(hhAISpeech *speech); + int GetPoolIndex(hhAISpeech *obj) const; + + int nextSpeechTime; // The time we are next allowed to play speech +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + + +#endif \ No newline at end of file diff --git a/src/Prey/ai_sphereboss.cpp b/src/Prey/ai_sphereboss.cpp new file mode 100644 index 0000000..6a3b606 --- /dev/null +++ b/src/Prey/ai_sphereboss.cpp @@ -0,0 +1,533 @@ + + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_UpdateTarget( "" ); +const idEventDef EV_DirectMoveToPosition("directMoveToPosition", "v" ); +const idEventDef MA_GetCircleNode("getCircleNode", NULL, 'e' ); +const idEventDef MA_SpinClouds("spinClouds", "f" ); +const idEventDef MA_SetSeekScale("setSeekScale", "f" ); + +CLASS_DECLARATION(hhMonsterAI, hhSphereBoss) + EVENT( EV_UpdateTarget, hhSphereBoss::Event_UpdateTarget ) + EVENT( EV_DirectMoveToPosition, hhSphereBoss::Event_DirectMoveToPosition ) + EVENT( MA_GetCircleNode, hhSphereBoss::Event_GetCircleNode ) + EVENT( MA_SpinClouds, hhSphereBoss::Event_SpinClouds ) + EVENT( MA_SetSeekScale, hhSphereBoss::Event_SetSeekScale ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +void hhSphereBoss::Spawn() { + for ( int i=0;i<10;i++ ) { + lastTargetPos.Append( vec3_zero ); + } + nextShieldImpact = 0; + lastNodeIndex = -1; + PostEventSec( &EV_UpdateTarget, spawnArgs.GetFloat( "target_period", "0.1" ) ); +} + +void hhSphereBoss::Event_UpdateTarget() { + for ( int i=0;i<10;i++ ) { + if ( i == 9 ) { + if ( enemy.IsValid() ) { + lastTargetPos[i] = enemy->GetOrigin(); + } else { + lastTargetPos[i] = lastTargetPos[i - 1]; + } + } else { + lastTargetPos[i] = lastTargetPos[i + 1]; + } + } + PostEventSec( &EV_UpdateTarget, spawnArgs.GetFloat( "target_period", "0.1" ) ); +} + +//overridden to allow custom accuracy/numbers per projectile def +idProjectile *hhSphereBoss::LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ) { + idVec3 muzzle; + idVec3 dir; + idVec3 start; + trace_t tr; + idBounds projBounds; + float distance; + const idClipModel *projClip; + float attack_accuracy; + float attack_cone; + float projectile_spread; + float diff; + float angle; + float spin; + idAngles ang; + int num_projectiles; + int i; + idMat3 axis; + idVec3 tmp; + idProjectile *lastProjectile; + + //HUMANHEAD mdc - added to support multiple projectiles + if( desiredProjectileDef ) { //try to set our projectile to the desiredProjectile + int projIndex = FindProjectileInfo( desiredProjectileDef ); + if( projIndex >= 0 ) { + SetCurrentProjectile( projIndex ); + } + } + //HUMANHEAD END + + + if ( !projectileDef ) { + gameLocal.Warning( "%s (%s) doesn't have a projectile specified", name.c_str(), GetEntityDefName() ); + return NULL; + } + + if ( projectileDef->GetFloat( "attack_accuracy" ) ) { + attack_accuracy = projectileDef->GetFloat( "attack_accuracy", "7" ); + } else { + attack_accuracy = spawnArgs.GetFloat( "attack_accuracy", "7" ); + } + attack_cone = spawnArgs.GetFloat( "attack_cone", "70" ); + if ( projectileDef->GetFloat( "projectile_spread" ) ) { + projectile_spread = projectileDef->GetFloat( "projectile_spread", "0" ); + } else { + projectile_spread = spawnArgs.GetFloat( "projectile_spread", "0" ); + } + if ( projectileDef->GetFloat( "num_projectiles" ) ) { + num_projectiles = projectileDef->GetFloat( "num_projectiles", "1" ); + } else { + num_projectiles = spawnArgs.GetFloat( "num_projectiles", "1" ); + } + + GetMuzzle( jointname, muzzle, axis ); + + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, axis[ 0 ] ); + } + + lastProjectile = projectile.GetEntity(); + + if ( target != NULL ) { + tmp = target->GetPhysics()->GetAbsBounds().GetCenter() - muzzle; + tmp.Normalize(); + axis = tmp.ToMat3(); + } else { + axis = viewAxis; + } + + // rotate it because the cone points up by default + tmp = axis[2]; + axis[2] = axis[0]; + axis[0] = -tmp; + + // make sure the projectile starts inside the monster bounding box + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + projClip = lastProjectile->GetPhysics()->GetClipModel(); + projBounds = projClip->GetBounds().Rotate( axis ); + + // check if the owner bounds is bigger than the projectile bounds + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( muzzle, viewAxis[ 0 ], distance ) ) { + start = muzzle + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, muzzle, projClip, axis, MASK_SHOT_RENDERMODEL, this ); + muzzle = tr.endpos; + + // set aiming direction + if ( spawnArgs.GetBool( "lag_target", "0" ) ) { + dir = (lastTargetPos[0] - muzzle).ToNormal(); + } else { + GetAimDir( muzzle, target, this, dir ); + } + ang = dir.ToAngles(); + + // adjust his aim so it's not perfect. uses sine based movement so the tracers appear less random in their spread. + float t = MS2SEC( gameLocal.time + entityNumber * 497 ); + ang.pitch += idMath::Sin16( t * 5.1 ) * attack_accuracy; + ang.yaw += idMath::Sin16( t * 6.7 ) * attack_accuracy; + + if ( clampToAttackCone ) { + // clamp the attack direction to be within monster's attack cone so he doesn't do + // things like throw the missile backwards if you're behind him + diff = idMath::AngleDelta( ang.yaw, current_yaw ); + if ( diff > attack_cone ) { + ang.yaw = current_yaw + attack_cone; + } else if ( diff < -attack_cone ) { + ang.yaw = current_yaw - attack_cone; + } + } + + axis = ang.ToMat3(); + + float spreadRad = DEG2RAD( projectile_spread ); + for( i = 0; i < num_projectiles; i++ ) { + // spread the projectiles out + angle = idMath::Sin( spreadRad * gameLocal.random.RandomFloat() ); + spin = (float)DEG2RAD( 360.0f ) * gameLocal.random.RandomFloat(); + dir = axis[ 0 ] + axis[ 2 ] * ( angle * idMath::Sin( spin ) ) - axis[ 1 ] * ( angle * idMath::Cos( spin ) ); + dir.Normalize(); + + // launch the projectile + if ( !projectile.GetEntity() ) { + CreateProjectile( muzzle, dir ); + } + lastProjectile = projectile.GetEntity(); + lastProjectile->Launch( muzzle, dir, vec3_origin ); + projectile = NULL; + } + + TriggerWeaponEffects( muzzle,axis ); + + lastAttackTime = gameLocal.time; + +//HUMANHEAD mdc - added to support multiple projectiles + projectile = NULL; + SetCurrentProjectile( projectileDefaultDefIndex ); //set back to our default projectile to be on the safe side +//HUMANHEAD END + + return lastProjectile; +} + +void hhSphereBoss::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + HandleNoGore(); + if ( !AI_DEAD ) { + AI_DEAD = true; + state = GetScriptFunction( "state_SphereDeath" ); + SetState( state ); + SetWaitState( "" ); + } +} + +void hhSphereBoss::FlyTurn( void ) { + if ( AI_FACE_ENEMY ) { + TurnToward( enemy->GetOrigin() ); + } else { + if ( move.moveCommand == MOVE_FACE_ENEMY ) { + TurnToward( lastVisibleEnemyPos ); + } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + } else if ( move.speed > 0.0f ) { + const idVec3 &vel = physicsObj.GetLinearVelocity(); + if ( vel.ToVec2().LengthSqr() > 0.1f ) { + TurnToward( vel.ToYaw() ); + } + } + } + Turn(); +} + +void hhSphereBoss::Event_SetSeekScale( float new_scale ) { + fly_seek_scale = new_scale; +} + +void hhSphereBoss::Event_GetCircleNode() { + if ( !enemy.IsValid() ) { + idThread::ReturnEntity( NULL ); + return; + } + idEntity *ent = NULL; + idEntity *bestEnt = NULL; + lastNodeIndex++; + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if ( !ent || !ent->IsType( hhAINode::Type ) ) { + continue; + } + if ( !ent->spawnArgs.GetBool( "circle_node" ) ) { + continue; + } + if ( lastNodeIndex == ent->spawnArgs.GetInt( "circle_node_index" ) ) { + bestEnt = ent; + break; + } + } + if ( !bestEnt ) { + lastNodeIndex = 0; + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if ( !ent || !ent->IsType( hhAINode::Type ) ) { + continue; + } + if ( !ent->spawnArgs.GetBool( "circle_node" ) ) { + continue; + } + if ( lastNodeIndex == ent->spawnArgs.GetInt( "circle_node_index" ) ) { + bestEnt = ent; + break; + } + } + } + if ( bestEnt ) { + idThread::ReturnEntity( bestEnt ); + } else { + idThread::ReturnEntity( NULL ); + } +} + +void hhSphereBoss::Event_GetCombatNode() { + idEntity *ent = NULL; + float bestDist = 0.0f; + float dist; + idEntity *bestEnt = NULL; + idList list; + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if ( !ent || !ent->spawnArgs.GetBool( "ainode" ) ) { + continue; + } + dist = (ent->GetOrigin() - GetOrigin()).Length(); + if ( dist < spawnArgs.GetFloat( "node_dist", "400" ) ) { + continue; + } + list.Append( ent ); + } + if ( list.Num() > 0 ) { + idThread::ReturnEntity( list[gameLocal.random.RandomInt(list.Num())] ); + } else { + idThread::ReturnEntity( NULL ); + } +} + +void hhSphereBoss::Event_DirectMoveToPosition(const idVec3 &pos) { + StopMove(MOVE_STATUS_DONE); + DirectMoveToPosition(pos); +} + +void hhSphereBoss::FlyMove( void ) { + idVec3 goalPos; + idVec3 oldorigin; + idVec3 newDest; + + AI_BLOCKED = false; + if ( ( move.moveCommand != MOVE_NONE ) && ReachedPos( move.moveDest, move.moveCommand ) ) { + if ( AI_FACE_ENEMY ) { + StopMove( MOVE_STATUS_DONE ); + } else { + AI_MOVE_DONE = true; + } + } + + idVec3 vel = physicsObj.GetLinearVelocity(); + goalPos = move.moveDest; + if ( ReachedPos( move.moveDest, move.moveCommand ) ) { + StopMove( MOVE_STATUS_DONE ); + } + + if ( move.speed ) { + FlySeekGoal( vel, goalPos ); + } + AddFlyBob( vel ); + AdjustFlySpeed( vel ); + physicsObj.SetLinearVelocity( vel ); + + // turn + FlyTurn(); + + // run the physics for this frame + oldorigin = physicsObj.GetOrigin(); + physicsObj.UseFlyMove( true ); + physicsObj.UseVelocityMove( false ); + physicsObj.SetDelta( vec3_zero ); + physicsObj.ForceDeltaMove( disableGravity ); + RunPhysics(); + + monsterMoveResult_t moveResult = physicsObj.GetMoveResult(); + if ( !af_push_moveables && attack.Length() && TestMelee() ) { + DirectDamage( attack, enemy.GetEntity() ); + } else { + idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); + if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + } else if ( moveResult == MM_BLOCKED ) { + move.blockTime = gameLocal.time + 500; + AI_BLOCKED = true; + } + } + + idVec3 org = physicsObj.GetOrigin(); + if ( oldorigin != org ) { + TouchTriggers(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 4000 ); + gameRenderWorld->DebugBounds( colorOrange, physicsObj.GetBounds(), org, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); + gameRenderWorld->DebugLine( colorRed, org, org + physicsObj.GetLinearVelocity(), gameLocal.msec, true ); + gameRenderWorld->DebugLine( colorBlue, org, goalPos, gameLocal.msec, true ); + gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); + DrawRoute(); + } +} + +void hhSphereBoss::AdjustFlySpeed( idVec3 &vel ) { + float speed; + + // apply dampening + float damp = spawnArgs.GetFloat( "fly_dampening", "0.01" ); + vel -= vel * damp * MS2SEC( gameLocal.msec ); + + // gradually speed up/slow down to desired speed + speed = vel.Normalize(); + speed += ( move.speed - speed ) * MS2SEC( gameLocal.msec ); + if ( speed < 0.0f ) { + speed = 0.0f; + } else if ( move.speed && ( speed > move.speed ) ) { + speed = move.speed; + } + + vel *= speed; +} + +bool hhSphereBoss::ReachedPos( const idVec3 &pos, const moveCommand_t moveCommand ) const { + if ( move.moveType == MOVETYPE_SLIDE ) { + idBounds bnds( idVec3( -4, -4.0f, -8.0f ), idVec3( 4.0f, 4.0f, 64.0f ) ); + bnds.TranslateSelf( physicsObj.GetOrigin() ); + if ( bnds.ContainsPoint( pos ) ) { + return true; + } + } else { + if ( ( moveCommand == MOVE_TO_ENEMY ) || ( moveCommand == MOVE_TO_ENTITY ) ) { + if ( physicsObj.GetAbsBounds().IntersectsBounds( idBounds( pos ).Expand( 8.0f ) ) ) { + return true; + } + } else { + idBounds bnds( idVec3( -64.0, -64.0f, -64.0f ), idVec3( 64.0, 64.0f, 64.0f ) ); + bnds.TranslateSelf( physicsObj.GetOrigin() ); + if ( bnds.ContainsPoint( pos ) ) { + return true; + } + } + } + return false; +} + +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhSphereBoss::LinkScriptVariables() { + hhMonsterAI::LinkScriptVariables(); + LinkScriptVariable( AI_FACE_ENEMY ); + LinkScriptVariable( AI_CAN_DAMAGE ); +} + +void hhSphereBoss::Event_SpinClouds( float shouldSpin ) { + if ( shouldSpin == 0.0f ) { + const idKeyValue *kv = spawnArgs.MatchPrefix( "cloud" ); + while( kv ) { + idEntity *ent = gameLocal.FindEntity( kv->GetValue() ); + if ( ent ) { + ent->SetShaderParm( spawnArgs.GetInt( "spin_parm", "4" ), ent->spawnArgs.GetFloat( "shaderparm4", "0" ) ); + } + kv = spawnArgs.MatchPrefix( "cloud", kv ); + } + } else { + const idKeyValue *kv = spawnArgs.MatchPrefix( "cloud" ); + while( kv ) { + idEntity *ent = gameLocal.FindEntity( kv->GetValue() ); + if ( ent ) { + ent->SetShaderParm( spawnArgs.GetInt( "spin_parm", "4" ), spawnArgs.GetFloat( "spin_value", "0.5" ) ); + } + kv = spawnArgs.MatchPrefix( "cloud", kv ); + } + } +} + +void hhSphereBoss::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + //overridden to allow custom wound code + if ( !AI_CAN_DAMAGE && inflictor && spawnArgs.MatchPrefix("mineDamage") ) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + if ( gameLocal.time >= nextShieldImpact ) { + nextShieldImpact = gameLocal.time + int(spawnArgs.GetFloat( "shield_impact_freq", "0.4" ) * 1000); + idVec3 offset; + if ( attacker && inflictor ) { + offset = (attacker->GetOrigin() - inflictor->GetOrigin()).ToNormal() * spawnArgs.GetFloat( "shield_impact_offset", "150" ); + const char *defName = spawnArgs.GetString( "fx_shield_impact" ); + idEntityFx *impactFx = SpawnFxLocal( defName, inflictor->GetOrigin() + offset, mat3_identity, &fxInfo, gameLocal.isClient ); + if ( impactFx ) { + impactFx->Bind( this, true ); + } + } + } + StartSound( "snd_shield_impact", SND_CHANNEL_BODY ); + if ( inflictor && inflictor->IsType( idProjectile::Type ) ) { + inflictor->PostEventMS( &EV_Remove, 0 ); + } + } else { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + if ( gameLocal.time >= nextShieldImpact ) { + nextShieldImpact = gameLocal.time + int(spawnArgs.GetFloat( "shield_impact_freq", "0.4" ) * 1000); + idVec3 offset; + if ( attacker && inflictor ) { + offset = (attacker->GetOrigin() - inflictor->GetOrigin()).ToNormal() * spawnArgs.GetFloat( "shield_impact_offset", "150" ); + BroadcastFxInfoPrefixed( "fx_pain_impact", inflictor->GetOrigin() + offset, mat3_identity, &fxInfo ); + } + } + } + + bool mine_damage = false; + if ( !AI_CAN_DAMAGE && spawnArgs.MatchPrefix("mineDamage") != NULL ) { + const idKeyValue *kv = spawnArgs.MatchPrefix("mineDamage"); + while( kv && kv->GetValue().Length() ) { + if ( !kv->GetValue().Icmp(damageDefName) ) { + mine_damage = true; + break; + } + kv = spawnArgs.MatchPrefix("mineDamage", kv); + } + if (!mine_damage) { + return; + } + } + + if ( !mine_damage ) { + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + } + + if ( !AI_CAN_DAMAGE && health > 0 ) { + SetState( GetScriptFunction( "state_Pain" ) ); + SetWaitState( "" ); + } +} + +/* +===================== +hhSphereBoss::Save +===================== +*/ +void hhSphereBoss::Save( idSaveGame *savefile ) const { + savefile->WriteInt( lastTargetPos.Num() ); + savefile->WriteInt( nextShieldImpact ); + for ( int i=0;iWriteVec3( lastTargetPos[i] ); + } +} + +/* +===================== +hhSphereBoss::Restore +===================== +*/ +void hhSphereBoss::Restore( idRestoreGame *savefile ) { + int num = 0; + savefile->ReadInt( num ); + savefile->ReadInt( nextShieldImpact ); + lastTargetPos.SetNum( num ); + for ( int i=0;iReadVec3( lastTargetPos[i] ); + } +} + +void hhSphereBoss::AddLocalMatterWound( jointHandle_t jointNum, const idVec3 &localOrigin, const idVec3 &localNormal, const idVec3 &localDir, int damageDefIndex, const idMaterial *collisionMaterial ) { + //overridden to remove woundmanager hook and use custom wound code is Damage() + if ( AI_CAN_DAMAGE ) { + return hhMonsterAI::AddLocalMatterWound( jointNum, localOrigin, localNormal, localDir, damageDefIndex, collisionMaterial ); + } +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/ai_sphereboss.h b/src/Prey/ai_sphereboss.h new file mode 100644 index 0000000..e00422e --- /dev/null +++ b/src/Prey/ai_sphereboss.h @@ -0,0 +1,45 @@ + +#ifndef __PREY_AI_SPHEREBOSS_H__ +#define __PREY_AI_SPHEREBOSS_H__ + +class hhSphereBoss : public hhMonsterAI { + +public: + CLASS_PROTOTYPE(hhSphereBoss); +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_UpdateTarget() {}; + void Event_GetCombatNode() {}; + void Event_GetCircleNode() {}; + void Event_DirectMoveToPosition(const idVec3 &pos) {}; + void Event_SpinClouds( float shouldSpin ) {}; + void Event_SetSeekScale( float new_scale ) {}; +#else + void Spawn(); + void FlyTurn(); + void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + idProjectile *LaunchProjectile( const char *jointname, idEntity *target, bool clampToAttackCone, const idDict* desiredProjectileDef ); + void AdjustFlySpeed( idVec3 &vel ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void FlyMove( void ); + bool ReachedPos( const idVec3 &pos, const moveCommand_t moveCommand ) const; + void LinkScriptVariables(); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + void AddLocalMatterWound( jointHandle_t jointNum, const idVec3 &localOrigin, const idVec3 &localNormal, const idVec3 &localDir, int damageDefIndex, const idMaterial *collisionMaterial ); + + void Event_UpdateTarget(); + void Event_GetCombatNode(); + void Event_GetCircleNode(); + void Event_DirectMoveToPosition(const idVec3 &pos); + void Event_SpinClouds( float shouldSpin ); + void Event_SetSeekScale( float new_scale ); +protected: + idScriptBool AI_FACE_ENEMY; + idScriptBool AI_CAN_DAMAGE; + idList lastTargetPos; + int lastNodeIndex; + int nextShieldImpact; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif \ No newline at end of file diff --git a/src/Prey/anim_baseanim.cpp b/src/Prey/anim_baseanim.cpp new file mode 100644 index 0000000..b2b85fa --- /dev/null +++ b/src/Prey/anim_baseanim.cpp @@ -0,0 +1,104 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" +#include "../renderer/Model_local.h" // Hackish. Needed to access certain classes. Imitates game/anim/Blend.cpp +/* +=============== +hhMD5Anim::hhMD5Anim +=============== +*/ +hhMD5Anim::hhMD5Anim(void) { + + start = 0.0f; + end = 1.0f; +} + + +/* +============================== +hhMD5Anim::setLimits + Set the limits of the animation to play. + start and end should vary from 0 to 1 +============================== +*/ +void hhMD5Anim::SetLimits(float start, float end) const { + + if (start <= end) { + this->start = start; + this->end = end; + } + else { + this->start = end; + this->end = start; + } + +} + + +/* +============================== +hhMD5Anim::ConvertTimeToFrame +============================== +*/ +void hhMD5Anim::ConvertTimeToFrame( int time, int cycleCount, + frameBlend_t &frame ) const { + float newTime; + float timeOffset; + + + timeOffset = animLength * start; + + if (time < 0) { + newTime = timeOffset; + } + else { + newTime = time + timeOffset; + } + + idMD5Anim::ConvertTimeToFrame(newTime, cycleCount, frame); + + +} + + +/* +============================== +hhMD5Anim::Length +============================== +*/ +int hhMD5Anim::Length(void) const { + int intLength; + + + intLength = animLength * (end - start); + if ( ( intLength == 0 ) && ( end != start ) ) { + intLength = 1; + } + + + return( intLength ); + +} + +/* +================ +hhMD5Anim::Save +================ +*/ +void hhMD5Anim::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( start ); + savefile->WriteFloat( end ); +} + +/* +================ +hhMD5Anim::Restore +================ +*/ +void hhMD5Anim::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( start ); + savefile->ReadFloat( end ); +} + diff --git a/src/Prey/anim_baseanim.h b/src/Prey/anim_baseanim.h new file mode 100644 index 0000000..0b123ba --- /dev/null +++ b/src/Prey/anim_baseanim.h @@ -0,0 +1,29 @@ + +#ifndef __PREY_ANIM_MD5ANIM_H__ +#define __PREY_ANIM_MD5ANIM_H__ + +// Forward declar +class idMD5Bone; + +class hhMD5Anim : public idMD5Anim { + +public: + + hhMD5Anim(void); + void SetLimits(float start, float end) const; + virtual void ConvertTimeToFrame( int time, int cycleCount, frameBlend_t &frame ) const; + virtual int Length( void ) const; + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + mutable float start; + mutable float end; + + +}; + + +#endif /* __PREY_ANIM_MD5ANIM_H__ */ + diff --git a/src/Prey/force_converge.cpp b/src/Prey/force_converge.cpp new file mode 100644 index 0000000..4a81156 --- /dev/null +++ b/src/Prey/force_converge.cpp @@ -0,0 +1,173 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idForce, hhForce_Converge ) +END_CLASS + + +hhForce_Converge::hhForce_Converge( void ) { + restoreFactor = 1.0f; + restoreForceSlack = 1.0f; + restoreTime = 0.1f; + ent = NULL; + physics = NULL; + axisEnt = NULL; + bShuttle = false; +} + +hhForce_Converge::~hhForce_Converge( void ) { +} + +void hhForce_Converge::Save(idSaveGame *savefile) const { + ent.Save(savefile); + axisEnt.Save(savefile); + savefile->WriteVec3( target ); + savefile->WriteVec3( offset ); + savefile->WriteInt( bodyID ); + savefile->WriteFloat( restoreTime ); + savefile->WriteFloat( restoreFactor ); + savefile->WriteFloat( restoreForceSlack ); + savefile->WriteBool( bShuttle ); +} + +void hhForce_Converge::Restore( idRestoreGame *savefile ) { + ent.Restore(savefile); + axisEnt.Restore(savefile); + savefile->ReadVec3( target ); + savefile->ReadVec3( offset ); + savefile->ReadInt( bodyID ); + savefile->ReadFloat( restoreTime ); + savefile->ReadFloat( restoreFactor ); + savefile->ReadFloat( restoreForceSlack ); + savefile->ReadBool( bShuttle ); + + physics = NULL; // HUMANHEAD mdl: Updated each frame +} + +void hhForce_Converge::Evaluate( int time ) { + + // Get the physics of ent each frame, in case their physics object was changed. + physics = ent.IsValid() ? ent->GetPhysics() : NULL; + + // Apply convergent force + if (physics) { + idVec3 p = physics->GetOrigin( bodyID ) + offset * physics->GetAxis( bodyID ); + idVec3 x = p - target; + + if (axisEnt.IsValid()) { + idBounds bounds = ent->GetPhysics()->GetBounds(); + idVec3 rightPt = ent->GetOrigin() + (ent->GetAxis()[0] * bounds[0].x); + idVec3 axisRightPt = axisEnt->GetOrigin() + (axisEnt->GetAxis()[1] * bounds[1].x); + + idVec3 leftPt = ent->GetOrigin() + (ent->GetAxis()[0] * bounds[1].x); + idVec3 axisLeftPt = axisEnt->GetOrigin() + (axisEnt->GetAxis()[1] * bounds[0].x); + + //gameRenderWorld->DebugLine(idVec4(1, 0, 0, 1), rightPt, axisRightPt, 100); + + p = rightPt; + x = p - axisRightPt; + + x *= restoreForceSlack; //allow a linear buildup based on the distance of the target point + + idVec3 v = physics->GetLinearVelocity(); + float m = physics->GetMass(); + float b = idMath::Sqrt(4*restoreFactor*m); + idVec3 force = -restoreFactor*x - b*v; // use with addforce, k=200000 + physics->AddForce(bodyID, p, force * 0.5f ); // Great to show mass variations + + //gameRenderWorld->DebugLine(idVec4(1, 0, 0, 1), leftPt, axisLeftPt, 100); + + p = leftPt; + x = p - axisLeftPt; + + x *= restoreForceSlack; //allow a linear buildup based on the distance of the target point + + force = axisLeftPt - leftPt; + force *= m * 4; + physics->AddForce(bodyID, p, force ); // Great to show mass variations + return; + } + + x *= restoreForceSlack; //allow a linear buildup based on the distance of the target point + + if (ent->IsType(idAFEntity_Base::Type)) { // Actors use velocity method since their masses are unrealistic, ragdolls need it too + if (!ent->fl.tooHeavyForTractor) { + // Velocity method: cover distance in fixed time + idVec3 velocity = -x / restoreTime; // Use with SetLinearVelocity() + if (ent->IsType(idActor::Type)) { //if a player is affected by this force.. + //..do not allow him to build up velocity into his ground plane + if (ent->GetPhysics()->IsType(idPhysics_Player::Type) && ent->GetPhysics()->HasGroundContacts()) { + velocity.x *= -ent->GetPhysics()->GetGravityNormal().x; + velocity.y *= -ent->GetPhysics()->GetGravityNormal().y; + velocity.z *= -ent->GetPhysics()->GetGravityNormal().z; + } + } + physics->SetLinearVelocity(velocity, bodyID); + } + else { + // Entities that are too heavy get no force, just give feedback force + } + } else { + float m = physics->GetMass(); + if (bShuttle && m < 1500.0f) { // Different equation for picking up low mass objects with the shuttle + idVec3 v = physics->GetLinearVelocity(); + float springFactor = restoreFactor*0.0002f; + float dampFactor = idMath::Sqrt(springFactor); + idVec3 force = (-x * springFactor * m) - (v * dampFactor * m); + physics->AddForce(bodyID, p, force ); + } else { // Critically Damped spring: m*a = -k*x - b*v ; b = 2 * sqrt(m*k) + idVec3 v = physics->GetLinearVelocity(); + float m = physics->GetMass(); + float b = idMath::Sqrt(4.0f*restoreFactor*m); + idVec3 force = -restoreFactor*x - b*v; // use with addforce, k=200000 + physics->AddForce(bodyID, p, force ); // Great to show mass variations + } + } +/* else { // Impulse method + idVec3 v = physics->GetLinearVelocity(); + float b = idMath::Sqrt(4*restoreFactor); + impulse = -restoreFactor*x - b*v; // Use with applyimpulse, k=50000 + physics->ApplyImpulse(bodyID, p, impulse ); // Worked for all but some moveables (k=50000) + } + else { // Mass scaled force method, applies varying force to keep constant velocity + idVec3 v = physics->GetLinearVelocity(); + float m = physics->GetMass(); + float b = idMath::Sqrt(4*restoreFactor); + impulse = (-restoreFactor*x - b*v) * m; // use with addforce, k=500 + physics->AddForce(bodyID, p, impulse ); // Works for all but ragdolls (k=500) + } + }*/ + } +} + +void hhForce_Converge::RemovePhysics( const idPhysics *phys ) { + // physics of ent is stored each evaluate so we can compare pointers directly, rather than querying + // ent about it's physics. This is done because typically, physics objects are reported as removed + // during deconstruction of the entities. + if (ent.IsValid() && phys == physics) { + SetEntity(NULL); + } +} + +void hhForce_Converge::SetRestoreTime( float time ) { + restoreTime = idMath::ClampFloat(0.02f, time, time); +} + +void hhForce_Converge::SetTarget(idVec3 &newTarget) { + target = newTarget; +} + +void hhForce_Converge::SetEntity(idEntity *entity, int id, const idVec3 &point) { + ent = entity; + bodyID = id; + offset = point; + physics = NULL; +} + +void hhForce_Converge::SetAxisEntity(idEntity *entity) { + axisEnt = entity; +} + diff --git a/src/Prey/force_converge.h b/src/Prey/force_converge.h new file mode 100644 index 0000000..e35ecda --- /dev/null +++ b/src/Prey/force_converge.h @@ -0,0 +1,44 @@ +#ifndef __FORCE_CONVERGE_H__ +#define __FORCE_CONVERGE_H__ + +//=============================================================================== +// +// Convergent force +// +//=============================================================================== + +class hhForce_Converge : public idForce { + CLASS_PROTOTYPE( hhForce_Converge ); + +public: + hhForce_Converge( void ); + virtual ~hhForce_Converge( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void SetTarget(idVec3 &newTarget); + void SetEntity(idEntity *entity, int id=0, const idVec3 &point=vec3_origin); + idEntity * GetEntity() const { return ent.GetEntity(); } + void SetRestoreTime( float time ); + void SetRestoreFactor( float factor ){ restoreFactor = factor; } + void SetRestoreSlack(float slack) { restoreForceSlack = slack; } + +public: // common force interface + virtual void Evaluate( int time ); + virtual void RemovePhysics( const idPhysics *phys ); + void SetAxisEntity(idEntity *entity); + void SetShuttle(bool shuttle) { bShuttle = shuttle; } + +private: + idEntityPtr ent; // Entity to apply forces to + idEntityPtr axisEnt; + idPhysics * physics; // physics object of entity during last evaluate + idVec3 target; // Point of convergence + idVec3 offset; // Entity local offset for force application + int bodyID; // Body ID for force application + float restoreTime; // Ideal convergence time + float restoreFactor; // Spring constant for restorative force + float restoreForceSlack; //rww - allow a linear buildup based on the distance of the target point + bool bShuttle; //mdl: Use an alternate spring equation for shuttle tractor beam +}; + +#endif diff --git a/src/Prey/game_afs.cpp b/src/Prey/game_afs.cpp new file mode 100644 index 0000000..df8ce09 --- /dev/null +++ b/src/Prey/game_afs.cpp @@ -0,0 +1,100 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +/*********************************************************************** + + hhAFEntity + +***********************************************************************/ + +// nla - Added to allow the mappers to do huge translations w/out too much bouncing +const idEventDef EV_EnableRagdoll( "enableRagdoll", "" ); +const idEventDef EV_DisableRagdoll( "disableRagdoll", "" ); + +CLASS_DECLARATION( idAFEntity_Generic, hhAFEntity ) + EVENT( EV_EnableRagdoll, hhAFEntity::Event_EnableRagdoll ) + EVENT( EV_DisableRagdoll, hhAFEntity::Event_DisableRagdoll ) +END_CLASS + +void hhAFEntity::Spawn( void ) { + fl.takedamage = !spawnArgs.GetBool("noDamage"); + if (spawnArgs.FindKey("gravity")) { + PostEventMS(&EV_ResetGravity, 100); // Post after first think, when gravity is reset by UpdateGravity() + } +} + +void hhAFEntity::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if ( CheckRagdollDamage( inflictor, attacker, dir, damageDefName, location ) ) { + return; + } + idAFEntity_Generic::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} + +//This is so ragdolls shutdown when attached to movers that are outside of the pvs. +void hhAFEntity::DormantBegin() { + idEntity::DormantBegin(); + Event_DisableRagdoll(); +} + +void hhAFEntity::DormantEnd() { + idEntity::DormantEnd(); + Event_EnableRagdoll(); +} + +void hhAFEntity::Event_EnableRagdoll() { + af.GetPhysics()->Thaw(); +} + +void hhAFEntity::Event_DisableRagdoll() { + af.GetPhysics()->Freeze(); +} + + + + +/*********************************************************************** + + hhAFEntity_WithAttachedHead + +***********************************************************************/ + +CLASS_DECLARATION( idAFEntity_WithAttachedHead, hhAFEntity_WithAttachedHead ) + EVENT( EV_EnableRagdoll, hhAFEntity_WithAttachedHead::Event_EnableRagdoll ) + EVENT( EV_DisableRagdoll, hhAFEntity_WithAttachedHead::Event_DisableRagdoll ) +END_CLASS + +void hhAFEntity_WithAttachedHead::Spawn( void ) { + fl.takedamage = !spawnArgs.GetBool("noDamage"); + if (spawnArgs.FindKey("gravity")) { + PostEventMS(&EV_ResetGravity, 100); // Post after first think, when gravity is reset by UpdateGravity() + } +} + +void hhAFEntity_WithAttachedHead::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if ( CheckRagdollDamage( inflictor, attacker, dir, damageDefName, location ) ) { + return; + } + idAFEntity_WithAttachedHead::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} + +//This is so ragdolls shutdown when attached to movers that are outside of the pvs. +void hhAFEntity_WithAttachedHead::DormantBegin() { + idEntity::DormantBegin(); + Event_DisableRagdoll(); +} + +void hhAFEntity_WithAttachedHead::DormantEnd() { + idEntity::DormantEnd(); + Event_EnableRagdoll(); +} + +void hhAFEntity_WithAttachedHead::Event_EnableRagdoll() { + af.GetPhysics()->Thaw(); +} + +void hhAFEntity_WithAttachedHead::Event_DisableRagdoll() { + af.GetPhysics()->Freeze(); +} diff --git a/src/Prey/game_afs.h b/src/Prey/game_afs.h new file mode 100644 index 0000000..113d499 --- /dev/null +++ b/src/Prey/game_afs.h @@ -0,0 +1,49 @@ +#ifndef __GAME_AFS_H__ +#define __GAME_AFS_H__ + +extern const idEventDef EV_CursorDrop; +extern const idEventDef EV_CursorDone; + +/*********************************************************************** + +hhAFEntity + +***********************************************************************/ + +class hhAFEntity : public idAFEntity_Generic { + +public: + CLASS_PROTOTYPE( hhAFEntity ); + + void Spawn(); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void DormantBegin(); + virtual void DormantEnd(); + + void Event_EnableRagdoll( void ); + void Event_DisableRagdoll( void ); +}; + + +/*********************************************************************** + +hhAFEntity_WithAttachedHead + +***********************************************************************/ + +class hhAFEntity_WithAttachedHead : public idAFEntity_WithAttachedHead { + +public: + CLASS_PROTOTYPE( hhAFEntity_WithAttachedHead ); + + void Spawn(); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void DormantBegin(); + virtual void DormantEnd(); + + void Event_EnableRagdoll(); + void Event_DisableRagdoll(); +}; + + +#endif // __GAME_AFS_H__ diff --git a/src/Prey/game_alarm.cpp b/src/Prey/game_alarm.cpp new file mode 100644 index 0000000..bf0d609 --- /dev/null +++ b/src/Prey/game_alarm.cpp @@ -0,0 +1,76 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//========================================================================== +// +// hhAlarmLight +// +//========================================================================== +CLASS_DECLARATION(idLight, hhAlarmLight) + EVENT(EV_Activate, hhAlarmLight::Event_Activate) +END_CLASS + +void hhAlarmLight::Spawn() { + bAlarmOn = false; + fl.takedamage = false; // Never take damage + + /* Shader parms set up like this (so the editor shows the light being on): + parm6 parm7 + editor(on): 0 0 + spawn(off): 1 0 + trigger(on): 1 1 + broken(off): 1 0 + */ + SetParmState(1.0f, 0.0f); + + // setup the clipModel + GetPhysics()->SetContents( CONTENTS_SOLID ); +} + +void hhAlarmLight::Save(idSaveGame *savefile) const { + savefile->WriteBool( bAlarmOn ); +} + +void hhAlarmLight::Restore( idRestoreGame *savefile ) { + savefile->ReadBool( bAlarmOn ); +} + +void hhAlarmLight::TurnOn() { + if (!bAlarmOn) { + bAlarmOn = true; + + StartSound("snd_alarm", SND_CHANNEL_BODY2, 0, true, NULL); + + SetParmState(1.0f, 1.0f); + } +} + +void hhAlarmLight::TurnOff() { + if (bAlarmOn) { + bAlarmOn = false; + StopSound(SND_CHANNEL_BODY2, true); + + SetParmState(0.0f, 1.0f); + } +} + +void hhAlarmLight::SetParmState(float value6, float value7) { + SetShaderParm(6, value6); + SetShaderParm(7, value7); + SetLightParm(6, value6); + SetLightParm(7, value7); +} + +void hhAlarmLight::Event_Activate(idEntity *activator) { + if (bAlarmOn) { + if (activator && activator->IsType(hhPlayer::Type)) { + TurnOff(); + } + } + else { + TurnOn(); + } +} diff --git a/src/Prey/game_alarm.h b/src/Prey/game_alarm.h new file mode 100644 index 0000000..84ef812 --- /dev/null +++ b/src/Prey/game_alarm.h @@ -0,0 +1,21 @@ +#ifndef __GAME_ALARMLIGHT_H__ +#define __GAME_ALARMLIGHT_H__ + +class hhAlarmLight : public idLight { + CLASS_PROTOTYPE( hhAlarmLight ); + +public: + void Spawn(); + void TurnOn(); + void TurnOff(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void SetParmState(float value6, float value7); + void Event_Activate(idEntity *activator); + + bool bAlarmOn; +}; + +#endif diff --git a/src/Prey/game_anim.cpp b/src/Prey/game_anim.cpp new file mode 100644 index 0000000..4986df0 --- /dev/null +++ b/src/Prey/game_anim.cpp @@ -0,0 +1,244 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +/* +============ +hhAnim::AddFrameCommandExtra +============ +*/ +bool hhAnim::AddFrameCommandExtra( idToken &token, frameCommand_t &fc, idLexer &src, idStr &errorText ) { + errorText = ""; + + + if ( token == "stopSnd" ) { + fc.type = FC_STOPSND; + return( true ); + } else if ( token == "stopSnd_voice" ) { + fc.type = FC_STOPSND_VOICE; + return( true ); + } else if ( token == "stopSnd_voice2" ) { + fc.type = FC_STOPSND_VOICE2; + return( true ); + } else if ( token == "stopSnd_body" ) { + fc.type = FC_STOPSND_BODY; + return( true ); + } else if ( token == "stopSnd_body2" ) { + fc.type = FC_STOPSND_BODY2; + return( true ); + } else if ( token == "stopSnd_body3" ) { + fc.type = FC_STOPSND_BODY3; + return( true ); + } else if ( token == "stopSnd_weapon" ) { + fc.type = FC_STOPSND_WEAPON; + return( true ); + } else if ( token == "stopSnd_item" ) { + fc.type = FC_STOPSND_ITEM; + return( true ); + } else if ( token == "event_args" ) { + fc.type = FC_EVENT_ARGS; + src.ParseRestOfLine( token ); + idStr error = InitFrameCommandEvent( fc, token ); + if( error.Length() ) { + errorText = error.c_str(); + return( false ); + } + + return( true ); + } else if ( token == "launch_missile" ) { + /* HUMANHEAD JRM - Because we parse out the joint when launched + if ( !modelDef->FindJoint( cmd ) ) { + return va( "Joint '%s' not found", cmd.c_str() ); + } + */ + src.ParseRestOfLine( token ); + /* HUMANHEAD JRM - need the rest of the line not just one token + if( !src.ReadTokenOnLine( &token ) ) { + errorText = va( "Unexpected end of line" ); + return( true ); + } + */ + fc.type = FC_LAUNCHMISSILE; + fc.string = new idStr( token ); + + return( true ); + } else if( token == "launch_altMissile" ) { + if( !src.ReadTokenOnLine( &token ) ) { + errorText = va( "Unexpected end of line" ); + return( true ); + } + fc.type = FC_LAUNCHALTMISSILE; + fc.string = new idStr( token ); + + return( true ); + } else if ( token == "launch_missile_bonedir" ) { + /* HUMANHEAD JRM - we want the whole token + if( !src.ReadTokenOnLine( &token ) ) { + errorText = va( "Unexpected end of line" ); + return( true ); + } + */ + src.ParseRestOfLine( token ); + fc.type = FC_LAUNCHMISSILE_BONEDIR; + fc.string = new idStr( token ); + + return( true ); + } else if ( token == "leftfootprint" ) { + fc.type = FC_LEFTFOOTPRINT; + + return( true ); + } else if ( token == "rightfootprint" ) { + fc.type = FC_RIGHTFOOTPRINT; + + return( true ); + } else if ( token == "mood" ) { + fc.type = FC_MOOD; + if( !src.ReadTokenOnLine( &token ) ) { + fc.string = new idStr( "" ); + return( true ); + } + fc.string = new idStr( token ); + + return( true ); + } else if ( token == "kick_obstacle" ) { + if( !src.ReadTokenOnLine( &token ) ) { + errorText = va( "Unexpected end of line" ); + return( true ); + } + fc.type = FC_KICK_OBSTACLE; + fc.string = new idStr( token ); + + return( true ); + } else if ( token == "trigger_anim_ent" ) { + /* + if( !src.ReadTokenOnLine( &token ) ) { + errorText = va( "Unexpected end of line" ); + return( true ); + } + */ + fc.type = FC_TRIGGER_ANIM_ENT; + return( true ); + } else if ( token == "set_key" ) { + fc.type = FC_SETKEY; + if( !src.ParseRestOfLine( token ) ) { + errorText = va( "Unexpected end of line" ); + return ( true ); + } + if( !fc.parmList ) { + fc.parmList = new idList; + } + hhUtils::SplitString( token, *fc.parmList, ' ' ); + if( fc.parmList->Num() != 2 ) { + errorText = va( "Invalid number of arguments for setkey frame-command" ); + return ( true ); + } + return ( true ); + } + + + return( false ); +} + +/* +============ +hhAnim::CallFrameCommandsExtra +============ +*/ +bool hhAnim::CallFrameCommandsExtra( const frameCommand_t &command, idEntity *ent ) const { + switch( command.type ) { + case FC_EVENT_ARGS: { + if ( command.function && command.parmList && command.function->eventdef ) { + ent->ProcessEvent( command.function->eventdef, (int)command.parmList ); + } + return( true ); + } + case FC_LAUNCHALTMISSILE: { + //ent->ProcessEvent( &AI_AttackAltMissile, command.string->c_str(), NULL ); + return( true ); + } + case FC_STOPSND: + case FC_STOPSND_VOICE: + case FC_STOPSND_VOICE2: + case FC_STOPSND_BODY: + case FC_STOPSND_BODY2: + case FC_STOPSND_BODY3: + case FC_STOPSND_WEAPON: + case FC_STOPSND_ITEM: { + ent->StopSound( s_channelType(command.type - FC_STOPSND), false ); //rww - do not broadcast + return( true ); + } + case FC_HIDE: { + ent->ProcessEvent( &EV_Hide ); + return( true ); + } + case FC_MOOD: { + if(command.string) { + ent->ProcessEvent( &AI_SetAnimPrefix, command.string->c_str()); + } else { + ent->ProcessEvent( &AI_SetAnimPrefix, " "); + } + return( true ); + } + case FC_LEFTFOOTPRINT: { + ent->ProcessEvent( &EV_FootprintLeft ); + return( true ); + } + case FC_RIGHTFOOTPRINT: { + ent->ProcessEvent( &EV_FootprintRight ); + return( true ); + } + case FC_LAUNCHMISSILE: { // JRM: changed to AttackMissileEx (so we can pass in projectile) + ent->ProcessEvent( &MA_AttackMissileEx, command.string->c_str(),0 ); + return( true ); + } + case FC_LAUNCHMISSILE_BONEDIR: { // JRM: need so we can launch missiles + ent->ProcessEvent( &MA_AttackMissileEx, command.string->c_str(),1 ); + return( true ); + } + case FC_SETKEY: {//mdc: added to be able to set keys from frame commands + if( command.parmList && command.parmList->Num() == 2 ) { + ent->spawnArgs.Set( (*command.parmList)[0].c_str(), (*command.parmList)[1].c_str() ); + } + return ( true ); + } + } + + return( false ); + +} + + +/* +===================== +hhAnim::InitFrameCommandEvent +===================== +*/ +idStr hhAnim::InitFrameCommandEvent( frameCommand_t &command, const idStr& cmdStr ) const { + idStr error; + + if( !command.parmList ) { + command.parmList = new idList; + } + + hhUtils::SplitString( cmdStr, *command.parmList, ' ' ); + + // Find the function + command.function = gameLocal.program.FindFunction( (*command.parmList)[0].c_str(), gameLocal.program.FindType((*command.parmList)[0].c_str()) ); + if( !command.function || !command.function->eventdef || command.function->eventdef->GetNumArgs() != 1 ) { + error = va("Invalid event('%s') for frameCommands event or event_args\n", (*command.parmList)[0].c_str() ); + } + + //Remove function name from the list + command.parmList->RemoveIndex( 0 ); + + //If no parms are needed, no need to waste memory + if( command.parmList->Num() <= 0 ) { + SAFE_DELETE_PTR( command.parmList ); + } + + return error; +} + diff --git a/src/Prey/game_anim.h b/src/Prey/game_anim.h new file mode 100644 index 0000000..b7d27af --- /dev/null +++ b/src/Prey/game_anim.h @@ -0,0 +1,23 @@ + +#ifndef __PREY_GAME_ANIM_H__ +#define __PREY_GAME_ANIM_H__ + +class hhAnim : public idAnim { + +public: + + hhAnim() : idAnim() { exactMatch = false; } + hhAnim( const idDeclModelDef *modelDef, const idAnim *anim ) : idAnim( modelDef, anim ) { } + + virtual bool AddFrameCommandExtra( idToken &token, frameCommand_t &fc, idLexer &src, idStr &errorText ); + virtual bool CallFrameCommandsExtra( const frameCommand_t &command, idEntity *ent ) const; + + bool exactMatch; + +protected: + virtual idStr InitFrameCommandEvent( frameCommand_t &command, const idStr& cmdStr ) const; + +}; + + +#endif /* __PREY_GAME_ANIM_H__ */ diff --git a/src/Prey/game_animBlend.cpp b/src/Prey/game_animBlend.cpp new file mode 100644 index 0000000..28eacce --- /dev/null +++ b/src/Prey/game_animBlend.cpp @@ -0,0 +1,276 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +/* +============ +hhAnimBlend::hhAnimBlend +============ +*/ +hhAnimBlend::hhAnimBlend() { + + frozen = false; + freezeStart = -1; + freezeEnd = -1; + freezeCurrent = -1; + rotateTime = -1; + rotateEvent = NULL; + +} + + +/* +===================== +hhAnimBlend::FrameHasChanged +===================== +*/ +bool hhAnimBlend::FrameHasChanged( int currentTime ) const { + + + UpdateFreezeTime( currentTime ); + + if ( frozen ) { + return false; + } + + return( idAnimBlend::FrameHasChanged( currentTime ) ); + +} + + +/* +===================== +hhAnimBlend::AnimTime +===================== +*/ +int hhAnimBlend::AnimTime( int currentTime ) const { + + + if ( animNum ) { + UpdateFreezeTime( currentTime ); + } + + return( idAnimBlend::AnimTime( currentTime ) ); + +} + + +/* +===================== +hhAnimBlend::GetFrameNumber +===================== +*/ +int hhAnimBlend::GetFrameNumber( int currentTime ) const { + + //gameLocal.Printf( "In FRAME NUM\n" ); + + UpdateFreezeTime( currentTime ); + + return( idAnimBlend::GetFrameNumber( currentTime ) ); + +} + + +/* +===================== +hhAnimBlend::CallFrameCommands +===================== +*/ +void hhAnimBlend::CallFrameCommands( idEntity *ent, int fromtime, int totime ) const { + + + if ( frozen ) { + return; + } + + idAnimBlend::CallFrameCommands( ent, fromtime, totime ); + +} + +/* +===================== +hhAnimBlend::BlendAnim +===================== +*/ +bool hhAnimBlend::BlendAnim( int currentTime, int channel, int numJoints, idJointQuat *blendFrame, float &blendWeight, bool removeOriginOffset, bool overrideBlend, bool printInfo ) const { + + + UpdateFreezeTime( currentTime ); + + return( idAnimBlend::BlendAnim( currentTime, channel, numJoints, blendFrame, blendWeight, removeOriginOffset, overrideBlend, printInfo ) ); + +} + + +/* +===================== +hhAnimBlend::BlendOrigin +===================== +*/ +void hhAnimBlend::BlendOrigin( int currentTime, idVec3 &blendPos, float &blendWeight, bool removeOriginOffset ) const { + + //gameLocal.Printf( "BLEND ORIGIN\n" ); + + UpdateFreezeTime( currentTime ); + + if ( frozen ) { + return; + } + + idAnimBlend::BlendOrigin( currentTime, blendPos, blendWeight, removeOriginOffset ); + +} + + +/* +===================== +hhAnimBlend::BlendDelta +===================== +*/ +void hhAnimBlend::BlendDelta( int fromtime, int totime, idVec3 &blendDelta, float &blendWeight ) const { + + + if ( frozen ) { + return; + } + + idAnimBlend::BlendDelta( fromtime, totime, blendDelta, blendWeight ); + +} + + +/* +===================== +hhAnimBlend::AddBounds +===================== +*/ +bool hhAnimBlend::AddBounds( int currentTime, idBounds &bounds, bool removeOriginOffset ) const { + + + UpdateFreezeTime( currentTime ); + + if ( frozen ) { + return false; + } + + return( idAnimBlend::AddBounds( currentTime, bounds, removeOriginOffset ) ); + +} + + +/* +=============== +hhAnimBlend::UpdateFreezeTime +=============== +*/ +void hhAnimBlend::UpdateFreezeTime( int currentTime ) const { + + if ( !frozen ) { + return; + } + + if ( currentTime > freezeCurrent ) { + starttime += currentTime - freezeCurrent; + endtime += currentTime - freezeCurrent; + freezeCurrent = currentTime; + } + +} + +extern const idEventDef EV_CheckThaw; + +/* +================ +hhAnimBlend::Freeze + Freeze the animation + Inputs: currentTime - The current time of freeze + freezeEnd - Delta time to end the freeze. Set to -1 if will thaw manually + Outputs: true if frozen successfully, false otherwise +HUMANHEAD nla +================ +*/ +bool hhAnimBlend::Freeze( int currentTime, idEntity *owner, int aFreezeEnd ) { + + if ( frozen ) { + return( false ); + } + + frozen = true; + freezeStart = currentTime; + freezeCurrent = currentTime; + freezeEnd = currentTime + aFreezeEnd; + + // Post the thaw event if time is known + if ( owner ) { + owner->PostEventMS( &EV_CheckThaw, aFreezeEnd ); + } + else { + gameLocal.Warning( "Freeze called on an animator with no owner!" ); + } + + + return( true ); +} + + +/* +=============== +hhAnimBlend::Freeze +HUMANHEAD nla +=============== +*/ +bool hhAnimBlend::Freeze( int currentTime, idEntity *owner ) { + + return( Freeze( currentTime, owner, -1 ) ); +} + + +/* +=============== +hhAnimBlend::Thaw +HUMANHEAD nla +=============== +*/ +bool hhAnimBlend::Thaw( int currentTime ) { + + if ( !frozen ) { + return( false ); + } + + UpdateFreezeTime( currentTime ); + frozen = false; + + + return( true ); +} + + +/* +=============== +hhAnimBlend::ThawIfTime +HUMANHEAD nla +=============== +*/ +bool hhAnimBlend::ThawIfTime( int currentTime ) { + + if ( !frozen ) { + return( false ); + } + + if ( freezeEnd < 0 ) { + return( false ); + } + + if ( currentTime >= freezeEnd ) { + return( Thaw( currentTime ) ); + } + + return( false ); +} + + + + diff --git a/src/Prey/game_animBlend.h b/src/Prey/game_animBlend.h new file mode 100644 index 0000000..caf33b4 --- /dev/null +++ b/src/Prey/game_animBlend.h @@ -0,0 +1,6 @@ + +#ifndef __PREY_GAME_ANIMBLEND_H__ +#define __PREY_GAME_ANIMBLEND_H__ + + +#endif /* __PREY_GAME_ANIMBLEND_H__ */ diff --git a/src/Prey/game_animDriven.cpp b/src/Prey/game_animDriven.cpp new file mode 100644 index 0000000..3864f0b --- /dev/null +++ b/src/Prey/game_animDriven.cpp @@ -0,0 +1,287 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_PlayCycle( "", "ds" ); + +CLASS_DECLARATION( hhAnimatedEntity, hhAnimDriven ) + EVENT( EV_PlayCycle, hhAnimDriven::Event_PlayCycle ) +END_CLASS + + +//============== +// hhAnimDriven::Spawn +//============== +void hhAnimDriven::Spawn() { + + passenger = NULL; + hadPassenger = false; + spawnTime = gameLocal.time; + + // We are gonna move the guy manually, turn off having the anim move him + GetAnimator()->RemoveOriginOffset( true ); + + // How do we deal with our owner? + physicsAnim.SetSelf( this ); + + if( spawnArgs.GetBool( "solid", "0" ) ) { + fl.takedamage = true; + physicsAnim.SetClipModel( new idClipModel( GetPhysics()->GetClipModel() ), 1.0f ); + GetPhysics()->SetContents( CONTENTS_SOLID ); + } else { + fl.takedamage = false; + GetPhysics()->SetContents( 0 ); + physicsAnim.SetClipModel( new idClipModel( GetPhysics()->GetClipModel() ), 1.0f ); + GetPhysics()->UnlinkClip(); + } + + + // move up to make sure the monster is at least an epsilon above the floor + physicsAnim.SetOrigin( GetOrigin() + idVec3( 0, 0, CM_CLIP_EPSILON ) ); + + SetPhysics( &physicsAnim ); + + //? Have this start at a set time afterwards? + int animLength = PlayAnim( ANIMCHANNEL_ALL, "initial" ); + PostEventMS( &EV_PlayCycle, animLength, ANIMCHANNEL_ALL, "move" ); + + float delta_min = spawnArgs.GetFloat( "delta_scale_min", "1.0" ); + float delta_max = spawnArgs.GetFloat( "delta_scale_max", "1.0" ); + if ( delta_min != 1.0f || delta_max != 1.0f ) { + deltaScale.x = idMath::ClampFloat( delta_min, delta_max, gameLocal.random.RandomFloat() ); + deltaScale.y = idMath::ClampFloat( delta_min, delta_max, gameLocal.random.RandomFloat() ); + deltaScale.z = idMath::ClampFloat( delta_min, delta_max, gameLocal.random.RandomFloat() ); + } else { + deltaScale = idVec3( 1.0f, 1.0f, 1.0f ); + } + + BecomeActive( TH_TICKER ); +} + +void hhAnimDriven::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject( physicsAnim ); + passenger.Save(savefile); + savefile->WriteInt( spawnTime ); + savefile->WriteBool( hadPassenger ); +} + +void hhAnimDriven::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject( physicsAnim ); + passenger.Restore(savefile); + savefile->ReadInt( spawnTime ); + savefile->ReadBool( hadPassenger ); +} + + +//============= +// hhAnimDriven::SetPassenger +//============= +void hhAnimDriven::SetPassenger( idEntity *newPassenger, bool orientFromPassenger ) { + idVec3 origin; + idMat3 axis; + + //?! Right now handle only 1 passenger. If we have one, warn and exit. + if ( passenger.IsValid() ) { + const char *oldName = passenger->GetName(); + const char *newName = "NULL"; + + if ( newPassenger ) { + newName = newPassenger->GetName(); + } + gameLocal.Warning( "Error: hhAnimDriven::SetPassenger tried to add passenger %s but already had %s\n", + newName, oldName ); + return; + } + + passenger = newPassenger; + hadPassenger = true; + fl.takedamage = true; + + if ( orientFromPassenger ) { + origin = passenger->GetOrigin(); + axis = passenger->GetAxis(); + } + + GetPhysics()->SetClipModel( new idClipModel( passenger->GetPhysics()->GetClipModel() ), 1.0f ); + GetPhysics()->SetContents( passenger->GetPhysics()->GetContents() ); + GetPhysics()->SetClipMask( passenger->GetPhysics()->GetClipMask() ); + + if ( orientFromPassenger ) { + SetOrigin( origin ); + SetAxis( axis ); + } + + passenger->Bind( this, true ); + +} + + +//============== +// hhAnimDriven::Think +//============== +void hhAnimDriven::Think() { + idVec3 delta; + + // Done move unless we have a passenger. (Give us like 10 secs to get one) + if ( !passenger.IsValid() ) { + // If we had a passenger, they removed themselves, and so should we :) + if ( hadPassenger ) { + PostEventMS( &EV_Remove, 0 ); + } + // If never had a passenger for a second, remove ourselves + else if ( gameLocal.time - spawnTime > 1000 ) { + PostEventMS( &EV_Remove, 0 ); + gameLocal.Warning( "hhAnimDriven %s existed for a second w/out a passenger. Removing.", GetName() ); + } + return; + } + + // Move them based on their anim + GetAnimator()->GetDelta( gameLocal.time - gameLocal.msec, gameLocal.time, delta ); + delta *= GetAxis(); + delta.x *= deltaScale.x; + delta.y *= deltaScale.y; + delta.z *= deltaScale.z; + + physicsAnim.SetDelta( delta ); + + // gameLocal.Printf( "%d Doing %s\n", gameLocal.GetTime(), delta.ToString() ); + + hhAnimatedEntity::Think(); + +} + + +//============ +// +//============ +int hhAnimDriven::PlayAnim( int channel, const char *animName ) { + int animIndex; + int length; + + + animIndex = GetAnimator()->GetAnim( animName ); + if ( !animIndex ) { + return( 0 ); + } + + //gameLocal.Printf( "Playing Anim %s\n", animName ); + + GetAnimator()->PlayAnim( channel, animIndex, gameLocal.GetTime(), 0 ); + + length = GetAnimator()->CurrentAnim( channel )->GetEndTime() - gameLocal.GetTime(); + + return( length ); + +} + + +//============ +// hhAnimDrive::Event_PlayCycle() +//============ +void hhAnimDriven::Event_PlayCycle( int channel, const char *animName ) { + + PlayCycle( channel, animName ); + +} + + +//============ +// +//============ +bool hhAnimDriven::PlayCycle( int channel, const char *animName ) { + int animIndex; + + animIndex = GetAnimator()->GetAnim( animName ); + if ( !animIndex ) { + return( false ); + } + + //gameLocal.Printf( "Cycling Anim %s\n", animName ); + + GetAnimator()->CycleAnim( channel, animIndex, gameLocal.GetTime(), 0 ); + + return( true ); +} + + +/* +================ +hhAnimDriven::ClearAllAnims +================ +*/ +void hhAnimDriven::ClearAllAnims() { + + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); + +} + + +//=========== +// +//============ +bool hhAnimDriven::Collide( const trace_t &collision, const idVec3 &velocity ) { + // If we hit something, and have a passenger, pass along the message + if( passenger.IsValid() ) { + if( !passenger->Collide(collision, velocity) ) { + return( false ); + } + + passenger->Unbind(); + } //. We have a valid passenger + + + // If we made it this far, assume the collision is for real, and + // remove ourselves from interacting with the world + fl.takedamage = false; + GetPhysics()->SetContents( 0 ); + GetPhysics()->UnlinkClip(); + ClearAllAnims(); + PostEventMS( &EV_Remove, 0 ); + + return( true ); +} + +bool hhAnimDriven::AllowCollision( const trace_t& collision ) { + if ( !spawnArgs.GetBool( "projectile_collision", "1" ) ) { + idEntity* ent = gameLocal.entities[ collision.c.entityNum ]; + if ( !ent || ent->IsType( idProjectile::Type ) ) { + return false; + } + } + + return true; +} + +void hhAnimDriven::UpdateAnimation( void ) { + PROFILE_SCOPE("Animation", PROFMASK_NORMAL); // HUMANHEAD pdm + + // don't do animations if they're not enabled + if ( !( thinkFlags & TH_ANIMATE ) ) { + return; + } + + // is the model an MD5? + if ( !animator.ModelHandle() ) { + // no, so nothing to do + return; + } + + // call any frame commands that have happened in the past frame + if ( !fl.hidden ) { + animator.ServiceAnims( gameLocal.previousTime, gameLocal.time ); + } + + // if the model is animating then we have to update it + if ( !animator.FrameHasChanged( gameLocal.time ) ) { + // still fine the way it was + return; + } + + // update the renderEntity + UpdateVisuals(); + + // the animation is updated + animator.ClearForceUpdate(); +} diff --git a/src/Prey/game_animDriven.h b/src/Prey/game_animDriven.h new file mode 100644 index 0000000..cf47d32 --- /dev/null +++ b/src/Prey/game_animDriven.h @@ -0,0 +1,40 @@ +#ifndef __PREY_GAME_ANIMDRIVEN_H__ +#define __PREY_GAME_ANIMDRIVEN_H__ + +// Class name by Jimmy! :) +class hhAnimDriven : public hhAnimatedEntity { + public: + CLASS_PROTOTYPE( hhAnimDriven ); + + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think(); + + void SetPassenger( idEntity *newPassenger, bool orientFromPassenger = true ); + + + // Events + void Event_PlayCycle( int channel, const char *animName ); + + + bool Collide( const trace_t &collision, const idVec3 &velocity ); + int PlayAnim( int channel, const char *animName ); + bool PlayCycle( int channel, const char *animName ); + void ClearAllAnims(); + void UpdateAnimation(); + bool AllowCollision( const trace_t& collision ); + + protected: + hhPhysics_Delta physicsAnim; + + idEntityPtr passenger; + + int spawnTime; + bool hadPassenger; + idVec3 deltaScale; //scale anim delta movement by this +}; + +#endif /* __PREY_GAME_ANIMDRIVEN_H__ */ diff --git a/src/Prey/game_animatedentity.cpp b/src/Prey/game_animatedentity.cpp new file mode 100644 index 0000000..6825f3e --- /dev/null +++ b/src/Prey/game_animatedentity.cpp @@ -0,0 +1,424 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_CheckCycleRotate( "" ); +const idEventDef EV_CheckThaw( "" ); +const idEventDef EV_SpawnFxAlongBone( "spawnFXAlongBone", "d" ); +const idEventDef EV_Silent( "", NULL ); + +CLASS_DECLARATION( idAnimatedEntity, hhAnimatedEntity ) + EVENT( EV_CheckCycleRotate, hhAnimatedEntity::Event_CheckAnimatorCycleRotate ) + EVENT( EV_CheckThaw, hhAnimatedEntity::Event_CheckAnimatorThaw ) + EVENT( EV_SpawnFxAlongBone, hhAnimatedEntity::Event_SpawnFXAlongBone ) + EVENT( EV_Thread_SetSilenceCallback,hhAnimatedEntity::Event_SetSilenceCallback ) + EVENT( EV_Silent, hhAnimatedEntity::Event_Silent ) +END_CLASS + +/* +============== +hhAnimatedEntity::Spawn +============== +*/ +void hhAnimatedEntity::Spawn( void ) { + hasFlapInfo = false; + + // nla - Added to allow for 1 frame anims to play/proper bone initialization + if ( GetAnimator() ) { + int anim = GetAnimator()->GetAnim("init"); + if ( anim ) { + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, 0); + GetAnimator()->ForceUpdate(); + UpdateModel(); + } + } +} + +void hhAnimatedEntity::Save(idSaveGame *savefile) const { + savefile->WriteInt( waitingThread ); + savefile->WriteInt( silentTimeOffset ); + savefile->WriteInt( nextSilentTime ); + savefile->WriteFloat( lastAmplitude ); +} + +void hhAnimatedEntity::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( waitingThread ); + savefile->ReadInt( silentTimeOffset ); + savefile->ReadInt( nextSilentTime ); + savefile->ReadFloat( lastAmplitude ); + hasFlapInfo = false; +} + +/* +============== +hhAnimatedEntity::hhAnimatedEntity +============== +*/ +hhAnimatedEntity::hhAnimatedEntity() { + // WaitForSilence support + waitingThread = 0; + silentTimeOffset = 0; + nextSilentTime = 0; + lastAmplitude = 0.0f; + hasFlapInfo = false; +} + +/* +============== +hhAnimatedEntity::~hhAnimatedEntity +============== +*/ +hhAnimatedEntity::~hhAnimatedEntity() { + Event_Silent(); +} + +/* +============== +hhAnimatedEntity::FillDebugVars +============== +*/ +void hhAnimatedEntity::FillDebugVars(idDict *args, int page) { + switch(page) { + case 1: + args->SetInt("anims", GetAnimator()->NumAnims()); + break; + } + idAnimatedEntity::FillDebugVars(args, page); +} + +/* +============== +hhAnimatedEntity::Think +============== +*/ +void hhAnimatedEntity::Think() { + idAnimatedEntity::Think(); + + //HUMANHEAD: aob - moved from idActor + UpdateWounds(); + //HUMANHEAD END +} + +/* +===================== +hhAnimatedEntity::GetAnimator +===================== +*/ +hhAnimator *hhAnimatedEntity::GetAnimator( void ) { + return &animator; +} + +/* +===================== +hhAnimatedEntity::GetAnimator +===================== +*/ +const hhAnimator *hhAnimatedEntity::GetAnimator( void ) const { + return &animator; +} + +/* +===================== +hhAnimatedEntity::GetJointWorldTransform +===================== +*/ +bool hhAnimatedEntity::GetJointWorldTransform( jointHandle_t jointHandle, int currentTime, idVec3 &offset, idMat3 &axis ) { + return idAnimatedEntity::GetJointWorldTransform( jointHandle, currentTime, offset, axis ); +} + +/* +===================== +hhAnimatedEntity::GetJointWorldTransform +===================== +*/ +bool hhAnimatedEntity::GetJointWorldTransform( const char* jointName, idVec3 &offset, idMat3 &axis ) { + return GetJointWorldTransform( GetAnimator()->GetJointHandle(jointName), gameLocal.GetTime(), offset, axis ); +} + +/* +===================== +hhAnimatedEntity::GetJointWorldTransform +===================== +*/ +bool hhAnimatedEntity::GetJointWorldTransform( jointHandle_t jointHandle, idVec3 &offset, idMat3 &axis ) { + return GetJointWorldTransform( jointHandle, gameLocal.GetTime(), offset, axis ); +} + +/* +============== +hhAnimatedEntity::UpdateWounds +============== +*/ +void hhAnimatedEntity::UpdateWounds( void ) { +} + +/* +================== +idEntity::SpawnFXAlongBonePrefixLocal +================== +*/ +void hhAnimatedEntity::SpawnFxAlongBonePrefixLocal( const idDict* dict, const char* fxKeyPrefix, const char* bonePrefix, const hhFxInfo* const fxInfo, const idEventDef* eventDef ) { + idVec3 bonePos; + idMat3 boneAxis; + + for( const idKeyValue* kv = dict->MatchPrefix(bonePrefix); kv; kv = dict->MatchPrefix(bonePrefix, kv) ) { + if( !kv->GetValue().Length() ) { + continue; + } + + GetJointWorldTransform( kv->GetValue().c_str(), bonePos, boneAxis ); + SpawnFXPrefixLocal( dict, fxKeyPrefix, bonePos, boneAxis, fxInfo, eventDef ); + } +} + +/* +================= +hhAnimatedEntity::SpawnFXAlongBone +================= +*/ +void hhAnimatedEntity::BroadcastFxInfoAlongBonePrefix( const idDict* args, const char* fxKey, const char* bonePrefix, const hhFxInfo* const fxInfo, const idEventDef* eventDef, bool broadcast ) { + if( !fxKey || !fxKey[0] || !bonePrefix || !bonePrefix[0] ) { + return; + } + + idVec3 bonePos; + idMat3 boneAxis; + hhFxInfo localFxInfo( fxInfo ); + + localFxInfo.SetEntity( this ); + localFxInfo.RemoveWhenDone( true ); + + const idKeyValue* keyValue = args->MatchPrefix( bonePrefix ); + while( keyValue && keyValue->GetValue().Length() ) { + + if( GetJointWorldTransform( keyValue->GetValue().c_str(), bonePos, boneAxis ) ) { + //AOB: HACK - crashed in idBitMsg::DirToBits because we didn't appear to be normalized + localFxInfo.SetNormal( boneAxis[0].ToNormal() ); + localFxInfo.SetBindBone( keyValue->GetValue().c_str() ); + + BroadcastFxInfoPrefixed( fxKey, bonePos, boneAxis, &localFxInfo, eventDef, broadcast ); + } + + keyValue = args->MatchPrefix( bonePrefix, keyValue ); + } +} + +/* +================= +hhAnimatedEntity::SpawnFXAlongBone +mdl +================= +*/ +void hhAnimatedEntity::BroadcastFxInfoAlongBonePrefixUnique( const idDict* args, const char* fxKey, const char* bonePrefix, const hhFxInfo* const fxInfo, const idEventDef* eventDef, bool broadcast ) { + if( !fxKey || !fxKey[0] || !bonePrefix || !bonePrefix[0] ) { + return; + } + + idVec3 bonePos; + idMat3 boneAxis; + hhFxInfo localFxInfo( fxInfo ); + + localFxInfo.SetEntity( this ); + localFxInfo.RemoveWhenDone( true ); + + const idKeyValue* keyValue = args->MatchPrefix( bonePrefix ); + idStr postfix; + int prefixLen = strlen( bonePrefix ); + while( keyValue && keyValue->GetValue().Length() ) { + postfix = keyValue->GetKey().Right( keyValue->GetKey().Length() - prefixLen ); + + if( GetJointWorldTransform( keyValue->GetValue().c_str(), bonePos, boneAxis ) ) { + //AOB: HACK - crashed in idBitMsg::DirToBits because we didn't appear to be normalized + localFxInfo.SetNormal( boneAxis[0].ToNormal() ); + localFxInfo.SetBindBone( keyValue->GetValue().c_str() ); + + // Changed this because it was causing every fx listed to be bound to every bone listed. -mdl + BroadcastFxInfo( spawnArgs.GetString( fxKey + postfix ), bonePos, boneAxis, &localFxInfo, eventDef, broadcast ); + //BroadcastFxInfoPrefixed( fxKey, bonePos, boneAxis, &localFxInfo, eventDef ); + } + + keyValue = args->MatchPrefix( bonePrefix, keyValue ); + } +} + +/* +================ +hhAnimatedEntity::SpawnFxAlongBone +================ +*/ +void hhAnimatedEntity::BroadcastFxInfoAlongBone( const char* fxName, const char* boneName, const hhFxInfo* const fxInfo, const idEventDef* eventDef, bool broadcast ) { + //mdc: pass through to our newer version of the function (this is mainly done for backwards compatibility) + BroadcastFxInfoAlongBone( false, fxName, boneName, fxInfo, eventDef, broadcast ); +} + +void hhAnimatedEntity::BroadcastFxInfoAlongBone( bool bNoRemoveWhenUnbound, const char* fxName, const char* boneName, const hhFxInfo* const fxInfo, const idEventDef* eventDef, bool broadcast ) { + if( !fxName || !fxName[0] || !boneName || !boneName[0] ) { + return; + } + + idVec3 bonePos; + idMat3 boneAxis; + hhFxInfo localFxInfo( fxInfo ); + + GetJointWorldTransform( boneName, bonePos, boneAxis ); + localFxInfo.SetNormal( boneAxis[0] ); + localFxInfo.SetEntity( this ); + localFxInfo.NoRemoveWhenUnbound( bNoRemoveWhenUnbound ); //mdc: added + localFxInfo.SetBindBone( boneName ); + + BroadcastFxInfo( fxName, bonePos, boneAxis, &localFxInfo, eventDef, broadcast ); +} + +/* +================ +hhAnimatedEntity::Event_SpawnFXAlongBone +================ +*/ +void hhAnimatedEntity::Event_SpawnFXAlongBone( idList* fxParms ) { + if ( !fxParms ) { + return; + } + + bool bNoRemoveWhenUnbound = false; //default to false (we normally want to delete fx when the bound-entity is deleted) + HH_ASSERT( fxParms->Num() >= 2 && fxParms->Num() <= 3 ); //allow 2-3 parameters + if( fxParms->Num() == 3 ) { + //make sure the 3rd paramater is our special noRemoveWhenUnbound flag + if( !idStr::Icmp((*fxParms)[2].c_str(), "noremovewhenunbound") ) { + bNoRemoveWhenUnbound = true; + } + else { + gameLocal.Warning( "unrecognized 3rd paramater on frame command for SpawnFXAlongBone on entity '%s'", this->GetName() ); + } + } + BroadcastFxInfoAlongBone(bNoRemoveWhenUnbound, spawnArgs.GetString((*fxParms)[0].c_str()), (*fxParms)[1].c_str() ); +} + + +// HUMANHEAD pdm: Jaw flapping support +// This provides generalized support for any animated entity to translate voice channel sound to bone movements +void hhAnimatedEntity::JawFlap(hhAnimator *theAnimator) { + PROFILE_SCOPE("Jaw Flap", PROFMASK_NORMAL); + float amplitude = 0.0f; + + if (fl.noJawFlap || !g_jawflap.GetBool()) { + return; + } + + // Get amplitude from head or body + idEntity *headEntity = NULL; + if (IsType(idActor::Type)) { + headEntity = static_cast(this)->GetHead(); + if (headEntity && headEntity->GetSoundEmitter()) { + amplitude = headEntity->GetSoundEmitter()->CurrentVoiceAmplitude( SND_CHANNEL_VOICE ); + } + } + + if( amplitude == 0.0f && GetSoundEmitter() ) { + amplitude = GetSoundEmitter()->CurrentVoiceAmplitude( SND_CHANNEL_VOICE ); + } + + if (amplitude != 0.0f || lastAmplitude != 0.0f) { + //HUMANHEAD rww - only get the joint handles and info for jaw flapping once + //we have to do this here, since the head is not valid at hhAnimatedEntity::Spawn + if (!hasFlapInfo) { + jawFlapList.Clear(); + const char *prefix = "jawbone_"; + const idKeyValue *kv = spawnArgs.MatchPrefix(prefix); + while( kv && kv->GetValue().Length() ) { + jointHandle_t bone = theAnimator->GetJointHandle( kv->GetValue() ); + if (bone != INVALID_JOINT) { + jawFlapInfo_t flapInfo; + + idStr xformName = kv->GetKey(); + xformName.Strip(prefix); + + flapInfo.bone = bone; + flapInfo.rMagnitude = spawnArgs.GetVector( va("jawflapR_%s", xformName.c_str()) ); + flapInfo.tMagnitude = spawnArgs.GetVector( va("jawflapT_%s", xformName.c_str()) ); + flapInfo.rMinThreshold = spawnArgs.GetFloat( va("jawflapRMin_%s", xformName.c_str()) ); + flapInfo.tMinThreshold = spawnArgs.GetFloat( va("jawflapTMin_%s", xformName.c_str()) ); + jawFlapList.Append(flapInfo); + } + kv = spawnArgs.MatchPrefix(prefix, kv); + } + hasFlapInfo = true; + } + + lastAmplitude = amplitude; + //HUMANHEAD rww - changed to use a list and not do constant runtime joint lookups + for (int i = 0; i < jawFlapList.Num(); i++) { + jawFlapInfo_t &flapInfo = jawFlapList[i]; + + // Handle Rotation + idAngles angles = ang_zero; + if (amplitude > flapInfo.rMinThreshold) { + float factor = amplitude - flapInfo.rMinThreshold; + angles = idAngles(factor*flapInfo.rMagnitude[0], factor*flapInfo.rMagnitude[1], factor*flapInfo.rMagnitude[2]); + } + theAnimator->SetJointAxis( flapInfo.bone, JOINTMOD_WORLD, angles.ToMat3() ); + + // Handle translation + idVec3 translate = vec3_origin; + if (amplitude > flapInfo.tMinThreshold) { + float factor = amplitude - flapInfo.tMinThreshold; + translate = idVec3(factor*flapInfo.tMagnitude[0], factor*flapInfo.tMagnitude[1], factor*flapInfo.tMagnitude[2]); + } + theAnimator->SetJointPos( flapInfo.bone, JOINTMOD_WORLD, translate ); + } + } +} + +// WaitForSilence support: Tracks the next time the voice channel will be silent, and if any threads are doing a waitForSilence() block, calls them back. +// If a waitForSilence() call is issued while no sound is playing on the voice channel, it will wait until there is something and then wait for silence. +bool hhAnimatedEntity::StartSoundShader( const idSoundShader *shader, const s_channelType channel, int soundShaderFlags, bool broadcast, int *length ) { + int localLength = 0; + bool retVal = idAnimatedEntity::StartSoundShader(shader, channel, soundShaderFlags, broadcast, &localLength); + + // need to track 'nextSilentTime' locally also, so if a tiny sound is played after a longer sound, it doesn't think silence will follow short sound + if (channel == SND_CHANNEL_VOICE && (localLength > 0) && (gameLocal.time + localLength > nextSilentTime)) { + nextSilentTime = gameLocal.time + localLength; + + // If there's already a thread waiting for silence and this sound pushes silence out further, repost an event for it + if (waitingThread) { + CancelEvents(&EV_Silent); + int time = localLength+silentTimeOffset; + time = idMath::ClampInt(1, time, time); // Keep positive and wait at least 1ms + PostEventMS(&EV_Silent, time); + } + } + if (length) { + *length = localLength; + } + return retVal; +} + +void hhAnimatedEntity::Event_Silent() { + if (waitingThread) { + idThread::ObjectMoveDone( waitingThread, this ); + waitingThread = 0; + } +} + +void hhAnimatedEntity::Event_SetSilenceCallback(float plusOrMinusSeconds) { + + if (!waitingThread) { + waitingThread = idThread::CurrentThreadNum(); + silentTimeOffset = SEC2MS(plusOrMinusSeconds); + + if (nextSilentTime > gameLocal.time) { + // If sound already playing + int time = (nextSilentTime - gameLocal.time) + silentTimeOffset; + time = idMath::ClampInt(1, time, time); // Keep positive and wait at least 1ms + CancelEvents(&EV_Silent); + PostEventMS(&EV_Silent, time); + } + else { + // No sound playing, wait for sound to play first + } + idThread::ReturnInt( true ); + } + else { + idThread::ReturnInt( false ); + } +} diff --git a/src/Prey/game_animatedentity.h b/src/Prey/game_animatedentity.h new file mode 100644 index 0000000..7ac5a59 --- /dev/null +++ b/src/Prey/game_animatedentity.h @@ -0,0 +1,74 @@ +#ifndef __GAME_ANIMATEDENTITY_H +#define __GAME_ANIMATEDENTITY_H + +extern const idEventDef EV_CheckCycleRotate; +extern const idEventDef EV_CheckThaw; +extern const idEventDef EV_SpawnFxAlongBone; + +//HUMANHEAD rww +typedef struct jawFlapInfo_s { + jointHandle_t bone; + idVec3 rMagnitude; + idVec3 tMagnitude; + float rMinThreshold; + float tMinThreshold; +} jawFlapInfo_t; +//HUMANHEAD END + +class hhAnimatedEntity : public idAnimatedEntity { +public: + CLASS_PROTOTYPE( hhAnimatedEntity ); + + void Spawn( void ); + hhAnimatedEntity(); + virtual ~hhAnimatedEntity(); + + void Think(); + + virtual hhAnimator * GetAnimator( void ); + virtual const hhAnimator * GetAnimator( void ) const; + virtual void FillDebugVars( idDict *args, int page ); + + virtual idClipModel* GetCombatModel() const { return combatModel; } + + virtual bool GetJointWorldTransform( jointHandle_t jointHandle, int currentTime, idVec3 &offset, idMat3 &axis ); + virtual bool GetJointWorldTransform( const char* jointName, idVec3 &offset, idMat3 &axis ); + virtual bool GetJointWorldTransform( jointHandle_t jointHandle, idVec3 &offset, idMat3 &axis ); + + void JawFlap(hhAnimator *theAnimator); + + void SpawnFxAlongBonePrefixLocal( const idDict* dict, const char* fxKeyPrefix, const char* bonePrefix, const hhFxInfo* const fxInfo = NULL, const idEventDef* eventDef = NULL ); + virtual void BroadcastFxInfoAlongBonePrefix( const idDict* args, const char* fxKey, const char* bonePrefix, const hhFxInfo* const fxInfo = NULL, const idEventDef* eventDef = NULL, bool broadcast = true ); //HUMANHEAD rww - added broadcast + virtual void BroadcastFxInfoAlongBonePrefixUnique( const idDict* args, const char* fxKey, const char* bonePrefix, const hhFxInfo* const fxInfo = NULL, const idEventDef* eventDef = NULL, bool broadcast = true ); //HUMANHEAD rww - added broadcast + virtual void BroadcastFxInfoAlongBone( const char* fxName, const char* boneName, const hhFxInfo* const fxInfo = NULL, const idEventDef* eventDef = NULL, bool broadcast = true ); //HUMANHEAD rww - added broadcast + virtual void BroadcastFxInfoAlongBone( bool bNoRemoveWhenUnbound, const char* fxName, const char* boneName, const hhFxInfo* const fxInfo = NULL, const idEventDef* eventDef = NULL, bool broadcast = true ); //HUMANHEAD rww - added broadcast + + // WaitForSilence support + virtual bool StartSoundShader( const idSoundShader *shader, const s_channelType channel, int soundShaderFlags = 0, bool broadcast = false, int *length = NULL ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + // WaitForSilence support + int waitingThread; // Thread that's blocking until we tell it we're silent + int silentTimeOffset; // +/- this amount + int nextSilentTime; // Time at which all sounds will be ended + void Event_Silent(); + void Event_SetSilenceCallback(float plusOrMinusSeconds); + + float lastAmplitude; + + bool hasFlapInfo; //HUMANHEAD rww + idList jawFlapList; //HUMANHEAD rww + + //HUMANHEAD: aob - moved from idActor + virtual void UpdateWounds( void ); + virtual hhWoundManagerRenderEntity* CreateWoundManager() const { if (!animator.IsAnimatedModel()) { return idEntity::CreateWoundManager(); } return new hhWoundManagerAnimatedEntity(this); } + +protected: + void Event_CheckAnimatorCycleRotate( void ) { GetAnimator()->CheckCycleRotate(); } + void Event_CheckAnimatorThaw( void ) { GetAnimator()->CheckThaw(); } + void Event_SpawnFXAlongBone( idList* fxParms ); +}; + +#endif diff --git a/src/Prey/game_animatedgui.cpp b/src/Prey/game_animatedgui.cpp new file mode 100644 index 0000000..b6b566e --- /dev/null +++ b/src/Prey/game_animatedgui.cpp @@ -0,0 +1,208 @@ +//************************************************************************** +//** +//** hhAnimatedGui +//** +//************************************************************************** + +//TODO: Could impliment a queue for open/close events, so we can wait until +// fully open before closing +// -or- +// could make animations blend out of current and into the new +// requires dynamic control of animation weights + + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const float AG_SMALL_SCALE = 0.01f; +const float AG_LARGE_SCALE = 1.0f; + +CLASS_DECLARATION( hhAnimatedEntity, hhAnimatedGui ) + EVENT(EV_PlayIdle, hhAnimatedGui::Event_PlayIdle) + EVENT(EV_Activate, hhAnimatedGui::Event_Trigger) +END_CLASS + + +//========================================================================== +// +// hhAnimatedGui::Spawn +// +//========================================================================== + +void hhAnimatedGui::Spawn(void) { + idDict args; + + GetPhysics()->SetContents( CONTENTS_BODY ); + + idleOpenAnim = GetAnimator()->GetAnim("idleopen"); + idleCloseAnim = GetAnimator()->GetAnim("idleclose"); + openAnim = GetAnimator()->GetAnim("open"); + closeAnim = GetAnimator()->GetAnim("close"); + + bOpen = false; + guiScale.Init(gameLocal.time, 0, AG_SMALL_SCALE, AG_SMALL_SCALE); + + // Spawn and bind the console on + const char *consoleName = spawnArgs.GetString("def_gui"); + if (consoleName && *consoleName) { + args.Clear(); + args.Set("gui", spawnArgs.GetString("gui_topass")); + args.Set("origin", GetOrigin().ToString()); // need the joint position + args.Set("rotation", GetAxis().ToString()); + attachedConsole = gameLocal.SpawnObject(consoleName, &args); + assert(attachedConsole); + attachedConsole->SetOrigin(GetOrigin() + GetAxis()[0]*10); + attachedConsole->Bind(this, true); + attachedConsole->Hide(); + } + + // Spawn the trigger + const char *triggerName = spawnArgs.GetString("def_trigger"); + if (triggerName && *triggerName) { + args.Clear(); + args.Set( "target", name.c_str() ); + args.Set( "mins", spawnArgs.GetString("triggerMins") ); + args.Set( "maxs", spawnArgs.GetString("triggerMaxs") ); + args.Set( "bind", name.c_str() ); + args.SetVector( "origin", GetOrigin() ); + args.SetMatrix( "rotation", GetAxis() ); + idEntity *trigger = gameLocal.SpawnObject( triggerName, &args ); + } + + if (idleOpenAnim && idleCloseAnim) { + PostEventMS(&EV_PlayIdle, 0); + } +} + +hhAnimatedGui::hhAnimatedGui() { + attachedConsole = NULL; +} + +hhAnimatedGui::~hhAnimatedGui() { +} + +void hhAnimatedGui::Save(idSaveGame *savefile) const { + savefile->WriteBool( bOpen ); + savefile->WriteInt( idleOpenAnim ); + savefile->WriteInt( idleCloseAnim ); + savefile->WriteInt( openAnim ); + savefile->WriteInt( closeAnim ); + + savefile->WriteFloat( guiScale.GetStartTime() ); // idInterpolate + savefile->WriteFloat( guiScale.GetDuration() ); + savefile->WriteFloat( guiScale.GetStartValue() ); + savefile->WriteFloat( guiScale.GetEndValue() ); + + savefile->WriteObject( attachedConsole ); +} + +void hhAnimatedGui::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadBool( bOpen ); + savefile->ReadInt( idleOpenAnim ); + savefile->ReadInt( idleCloseAnim ); + savefile->ReadInt( openAnim ); + savefile->ReadInt( closeAnim ); + + savefile->ReadFloat( set ); // idInterpolate + guiScale.SetStartTime( set ); + savefile->ReadFloat( set ); + guiScale.SetDuration( set ); + savefile->ReadFloat( set ); + guiScale.SetStartValue(set); + savefile->ReadFloat( set ); + guiScale.SetEndValue( set ); + + savefile->ReadObject( reinterpret_cast( attachedConsole ) ); +} + +//========================================================================== +// +// hhAnimatedGui::Think +// +//========================================================================== +void hhAnimatedGui::Think( void ) { + hhAnimatedEntity::Think(); + + if (thinkFlags & TH_THINK) { + float curScale = guiScale.GetCurrentValue(gameLocal.time); + attachedConsole->SetDeformation(DEFORMTYPE_SCALE, curScale); + if (guiScale.IsDone(gameLocal.time)) { + if (curScale == AG_SMALL_SCALE) { // Just finished scaling down + attachedConsole->Hide(); + } + BecomeInactive(TH_THINK); + } + } +} + +//========================================================================== +// +// hhAnimatedGui::Event_PlayIdle +// +//========================================================================== +void hhAnimatedGui::Event_PlayIdle() { + + GetAnimator()->ClearAllAnims(gameLocal.time, 0); + + if (bOpen) { + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleOpenAnim, gameLocal.time, 0); + + FadeInGui(); + } + else { + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleCloseAnim, gameLocal.time, 0); + } +} + + +//========================================================================== +// +// hhAnimatedGui::Event_Trigger +// +//========================================================================== + +void hhAnimatedGui::Event_Trigger( idEntity *activator ) { + + if (!openAnim || !closeAnim) { + return; + } + + GetAnimator()->ClearAllAnims(gameLocal.time, 0); + + int ms; + if(bOpen) { + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, closeAnim, gameLocal.time, 0); + ms = GetAnimator()->GetAnim( closeAnim )->Length(); + bOpen = false; + + FadeOutGui(); + } + else { + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, openAnim, gameLocal.time, 0); + ms = GetAnimator()->GetAnim( openAnim )->Length(); + bOpen = true; + } + + PostEventMS(&EV_PlayIdle, ms); +} + +void hhAnimatedGui::FadeInGui() { + float curScale = guiScale.GetCurrentValue(gameLocal.time); + guiScale.Init(gameLocal.time, 500, curScale, AG_LARGE_SCALE); + BecomeActive(TH_THINK); + attachedConsole->SetDeformation(DEFORMTYPE_SCALE, curScale); + attachedConsole->Show(); +} + +void hhAnimatedGui::FadeOutGui() { + float curScale = guiScale.GetCurrentValue(gameLocal.time); + guiScale.Init(gameLocal.time, 100, curScale, AG_SMALL_SCALE); + BecomeActive(TH_THINK); + attachedConsole->Show(); +} diff --git a/src/Prey/game_animatedgui.h b/src/Prey/game_animatedgui.h new file mode 100644 index 0000000..4cf3842 --- /dev/null +++ b/src/Prey/game_animatedgui.h @@ -0,0 +1,34 @@ + +#ifndef __GAME_ANIMATEDGUI_H__ +#define __GAME_ANIMATEDGUI_H__ + + +class hhAnimatedGui : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE(hhAnimatedGui); + + void Spawn(void); + hhAnimatedGui(); + virtual ~hhAnimatedGui(); + virtual void Think(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void FadeInGui(); + void FadeOutGui(); + + void Event_Trigger( idEntity *activator ); + void Event_PlayIdle(); + +protected: + bool bOpen; + int idleOpenAnim; + int idleCloseAnim; + int openAnim; + int closeAnim; + idInterpolate guiScale; + idEntity * attachedConsole; +}; + +#endif /* __GAME_ANIMATEDGUI_H__ */ diff --git a/src/Prey/game_animator.cpp b/src/Prey/game_animator.cpp new file mode 100644 index 0000000..76905ce --- /dev/null +++ b/src/Prey/game_animator.cpp @@ -0,0 +1,251 @@ +//************************************************************************** +//** +//** hhAnimated +//** +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_PositionDefaultPose(""); +const idEventDef EV_StartDefaultAnim(""); +const idEventDef EV_SetAnim( "setAnim", "s" ); // nla +const idEventDef EV_IsAnimDone( "isAnimDone", "d", 'd' ); + +CLASS_DECLARATION( idAnimated, hhAnimated ) + EVENT( EV_Activate, hhAnimated::Event_Activate ) + EVENT( EV_SetAnim, hhAnimated::Event_SetAnim ) + EVENT( EV_IsAnimDone, hhAnimated::Event_IsAnimDone ) + EVENT( EV_AnimDone, hhAnimated::Event_AnimDone ) + EVENT( EV_Animated_Start, hhAnimated::Event_Start ) + EVENT( EV_PositionDefaultPose, hhAnimated::Event_PositionDefaultPose ) + EVENT( EV_StartDefaultAnim, hhAnimated::Event_StartDefaultAnim ) + EVENT( EV_Footstep, hhAnimated::Event_Footstep ) + EVENT( EV_FootstepLeft, hhAnimated::Event_Footstep ) + EVENT( EV_FootstepRight, hhAnimated::Event_Footstep ) +END_CLASS + +/* +=============== +hhAnimated::Spawn +================ +*/ +void hhAnimated::Spawn() { + isAnimDone = true; + + fl.takedamage = health > 0; + + const char* startAnimName = spawnArgs.GetString( "start_anim" ); + if( !startAnimName || !startAnimName[0] ) { + PostEventMS( &EV_PositionDefaultPose, 0 ); + } +} + +void hhAnimated::Save(idSaveGame *savefile) const { + savefile->WriteBool( isAnimDone ); +} + +void hhAnimated::Restore( idRestoreGame *savefile ) { + savefile->ReadBool( isAnimDone ); +} + +/* +=============== +hhAnimated::Damage +================ +*/ +void hhAnimated::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if ( CheckRagdollDamage( inflictor, attacker, dir, damageDefName, location ) ) { + return; + } + + idAnimated::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} + +/* +=============== +hhAnimated::Killed +================ +*/ +void hhAnimated::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + idAnimBlend* pAnim = NULL;//GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, "death", gameLocal.time, 0, false ); + + if( !af.IsActive() ) { + PostEventMS( &EV_StartRagdoll, (pAnim) ? pAnim->Length() : 0 ); + } +} + +/* +=============== +hhAnimated::StartRagdoll +================ +*/ +bool hhAnimated::StartRagdoll( void ) { + bool parentAns = false; + float slomoStart, slomoEnd; + + // NOTE: These first two calls are ripped from idAnimated::StartRagdoll() + // if no AF loaded + if ( !af.IsLoaded() ) { + return false; + } + + // if the AF is already active + if ( af.IsActive() ) { + return true; + } + + // HUMANHEAD nla - Added to fix the issue with inproper offsets for fixed constraints. Taken from idAFEntity::LoadAF (Caused ragdolls to be 'fixed' to the center of the world/0,0,0) + af.GetPhysics()->Rotate( GetPhysics()->GetAxis().ToRotation() ); + af.GetPhysics()->Translate( GetPhysics()->GetOrigin() ); + // HUMANHEAD END + + parentAns = idAnimated::StartRagdoll(); + + // HUMANHEAD nla - Allow the ragdolls to slow into the ragdoll + slomoStart = MS2SEC( gameLocal.time ) + spawnArgs.GetFloat( "ragdoll_slomoStart", "-1.6" ); + slomoEnd = MS2SEC( gameLocal.time ) + spawnArgs.GetFloat( "ragdoll_slomoEnd", "0.8" ); + + // do the first part of the death in slow motion + af.GetPhysics()->SetTimeScaleRamp( slomoStart, slomoEnd ); + + // Allow ragdolls to be active when first started + // This logic ripped from idAFEntity::LoadAF + af.GetPhysics()->PutToRest(); + af.GetPhysics()->Activate(); + + af.UpdateAnimation(); + GetAnimator()->CreateFrame( gameLocal.time, true ); + UpdateVisuals(); + // HUMANHEAD END + + return( parentAns ); +} + +/* +================ +hhAnimated::UpdateAnimationControllers +================ +*/ +bool hhAnimated::UpdateAnimationControllers( void ) { + bool retValue = idAnimated::UpdateAnimationControllers(); + + JawFlap(GetAnimator()); + + return retValue; +} + +/* +================ +hhAnimated::Event_SetAnim +================ +*/ +void hhAnimated::Event_SetAnim( const char *animname ) { + assert( animname ); + + anim = GetAnimator()->GetAnim( animname ); + HH_ASSERT( anim ); + + ProcessEvent( &EV_Activate, this ); +} + +/* +================ +hhAnimated::Event_Start +================ +*/ +void hhAnimated::Event_Start( void ) { + idAnimated::Event_Start(); + + isAnimDone = false; +} + +/* +=============== +hhAnimated::Event_AnimDone +================ +*/ +void hhAnimated::Event_AnimDone( int animIndex ) { + idAnimated::Event_AnimDone( animIndex ); + + if( !spawnArgs.GetBool("resetDefaultAnim") ) { + isAnimDone = true; + } else { + PostEventMS( &EV_StartDefaultAnim, 0 ); + } +} + +/* +================ +hhAnimated::Event_IsAnimDone +================ +*/ +void hhAnimated::Event_IsAnimDone( int timeMS ) { + idThread::ReturnInt( (int) isAnimDone ); +} + +/* +=============== +hhAnimated::Event_PositionDefaultPose +================ +*/ +void hhAnimated::Event_PositionDefaultPose() { + const char* animName = spawnArgs.GetString( "defaultPose" ); + + if( !animName || !animName[0] ) { + return; + } + + int pAnim = GetAnimator()->GetAnim( animName ); + GetAnimator()->SetFrame( ANIMCHANNEL_ALL, pAnim, spawnArgs.GetInt("pose_frame", "1"), gameLocal.GetTime(), 0 ); + + GetAnimator()->ForceUpdate(); + UpdateModel(); + UpdateAnimation(); +} + +/* +=============== +hhAnimated::Event_StartDefaultAnim +================ +*/ +void hhAnimated::Event_StartDefaultAnim() { + const char* animName = spawnArgs.GetString( "start_anim" ); + + if( !animName || animName[0] ) { + return; + } + + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); + int pAnim = GetAnimator()->GetAnim( animName ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, pAnim, gameLocal.time, FRAME2MS(blendFrames) ); +} + + +/* +=============== +hhAnimated::Event_Activate +================ +*/ +void hhAnimated::Event_Activate( idEntity *_activator ) { + if (spawnArgs.GetBool("ragdollOnTrigger")) { + StartRagdoll(); + return; + } + + idAnimated::Event_Activate(_activator); +} + +/* +=============== +hhAnimated::Event_Footstep +================ +*/ +void hhAnimated::Event_Footstep() { + StartSound( "snd_footstep", SND_CHANNEL_BODY3, 0, false, NULL ); +} diff --git a/src/Prey/game_animator.h b/src/Prey/game_animator.h new file mode 100644 index 0000000..746c560 --- /dev/null +++ b/src/Prey/game_animator.h @@ -0,0 +1,32 @@ + +#ifndef __GAME_ANIMATED_H__ +#define __GAME_ANIMATED_H__ + +class hhAnimated : public idAnimated { + CLASS_PROTOTYPE( hhAnimated ); + + public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual bool UpdateAnimationControllers( void ); + + bool StartRagdoll( void ); + + protected: + void Event_SetAnim( const char* animname ); + void Event_IsAnimDone( int timeMS ); + virtual void Event_AnimDone( int animIndex ); + void Event_Start( void ); + void Event_PositionDefaultPose(); + void Event_StartDefaultAnim(); + virtual void Event_Activate( idEntity *_activator ); + void Event_Footstep(); + + protected: + bool isAnimDone; +}; + +#endif /* __GAME_ANIMATED_H__ */ diff --git a/src/Prey/game_arcadegame.cpp b/src/Prey/game_arcadegame.cpp new file mode 100644 index 0000000..74eb789 --- /dev/null +++ b/src/Prey/game_arcadegame.cpp @@ -0,0 +1,766 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define DEFAULT_HIGH_SCORE 76535 +#define DEFAULT_HIGH_SCORE_NAME "Teacher" +#define FRUIT_DELAY 15000 + +const idEventDef EV_GameStart("", NULL); +const idEventDef EV_PlayerRespawn("", NULL); +const idEventDef EV_MonsterRespawn("", "d"); +const idEventDef EV_ToggleFruit("", NULL); +const idEventDef EV_GameOver("", NULL); +const idEventDef EV_NextMap("", NULL); + +CLASS_DECLARATION(hhConsole, hhArcadeGame) + EVENT( EV_GameStart, hhArcadeGame::Event_GameStart) + EVENT( EV_PlayerRespawn, hhArcadeGame::Event_PlayerRespawn) + EVENT( EV_MonsterRespawn, hhArcadeGame::Event_MonsterRespawn) + EVENT( EV_ToggleFruit, hhArcadeGame::Event_ToggleFruit) + EVENT( EV_GameOver, hhArcadeGame::Event_GameOver) + EVENT( EV_NextMap, hhArcadeGame::Event_NextMap) + EVENT( EV_CallGuiEvent, hhArcadeGame::Event_CallGuiEvent) +END_CLASS + + +hhArcadeGame::hhArcadeGame() { +} + +hhArcadeGame::~hhArcadeGame() { +} + +void hhArcadeGame::Spawn() { + highscore = DEFAULT_HIGH_SCORE; + highscoreName = DEFAULT_HIGH_SCORE_NAME; + + GetMazeForLevel(1); + victoryAmount = spawnArgs.GetInt("victory"); + bPlayerMoving = false; + playerMove.Set(1,0); + bGameOver = true; + ResetMap(); + ResetPlayerAndMonsters(); + ResetGame(); + UpdateView(); +} + +void hhArcadeGame::GetMazeForLevel(int lev) { + numMazes = idMath::ClampInt(1, 999, spawnArgs.GetInt("numMazes")); + const char *maze = spawnArgs.GetString(va("maze%d", (lev-1)%numMazes+1)); + assert(idStr::Length(maze) == (ARCADE_GRID_WIDTH*ARCADE_GRID_HEIGHT)); + memset(startingGrid, 0, ARCADE_GRID_WIDTH*ARCADE_GRID_HEIGHT); + for (int y=0; yWrite(startingGrid, ARCADE_GRID_WIDTH*ARCADE_GRID_HEIGHT); + for (i=0; iWriteVec2(playerMove); + savefile->WriteBool(bPlayerMoving); + savefile->WriteBool(bSimulating); + savefile->WriteBool(bGameOver); + savefile->WriteBool(bPoweredUp); + savefile->WriteInt(score); + savefile->WriteInt(lives); + savefile->WriteInt(level); + savefile->WriteInt(nextScoreBoost); + savefile->WriteInt(nextPelletTime); + savefile->WriteInt(victoryAmount); + savefile->WriteInt(numMazes); + savefile->WriteInt(highscore); + savefile->WriteString(highscoreName); +} + +void hhArcadeGame::Restore( idRestoreGame *savefile ) { + int i,j; + savefile->Read(startingGrid, ARCADE_GRID_WIDTH*ARCADE_GRID_HEIGHT); + for (i=0; iReadVec2(playerMove); + savefile->ReadBool(bPlayerMoving); + savefile->ReadBool(bSimulating); + savefile->ReadBool(bGameOver); + savefile->ReadBool(bPoweredUp); + savefile->ReadInt(score); + savefile->ReadInt(lives); + savefile->ReadInt(level); + savefile->ReadInt(nextScoreBoost); + savefile->ReadInt(nextPelletTime); + savefile->ReadInt(victoryAmount); + savefile->ReadInt(numMazes); + savefile->ReadInt(highscore); + savefile->ReadString(highscoreName); +} + +void hhArcadeGame::ConsoleActivated() { + BecomeActive(TH_MISC3); +} + +void hhArcadeGame::StartGame() { + if (bGameOver) { + ResetMap(); + ResetPlayerAndMonsters(); + StartSound("snd_intro", SND_CHANNEL_ANY, 0, true, NULL); + ResetGame(); + StartSound("snd_coin", SND_CHANNEL_ANY, 0, true, NULL); + PostEventMS(&EV_GameStart, 2000); + bGameOver = false; + UpdateView(); + } +} + +void hhArcadeGame::FindType(int type, int &xOut, int &yOut, int skipX, int skipY) { + xOut = 0; + yOut = 0; + for (int x=0; xSetStateInt("player_x", guiPos.x); + gui->SetStateInt("player_y", guiPos.y); + gui->SetStateBool("player_vis", player.IsVisible()); + gui->SetStateBool("player_moving", bPlayerMoving); + gui->SetStateFloat("player_angle", player.angle); + gui->SetStateBool("player_dying", player.dying); + } + + for (ix=0; ix<4; ix++) { + if (monsters[ix].Changed()) { + guiPos = SmoothMovement(monsters[ix]); + gui->SetStateInt(va("monster%d_x", ix+1), guiPos.x); + gui->SetStateInt(va("monster%d_y", ix+1), guiPos.y); + gui->SetStateBool(va("monster%d_vis", ix+1), monsters[ix].IsVisible()); + gui->SetStateInt(va("monster%d_pu", ix+1), monsters[ix].vulnerableTimeRemaining); + gui->SetStateFloat(va("monster%d_angle", ix+1), monsters[ix].angle); + } + } + + // Traverse grid, telling gui about state of the pellets and powerups + int curPowerup = 1; + int curPellet = 1; + for (int x=0; xSetStateInt(va("pellet%d_x", curPellet), x * ARCADE_CELL_WIDTH); + gui->SetStateInt(va("pellet%d_y", curPellet), y * ARCADE_CELL_HEIGHT); + gui->SetStateBool(va("pellet%d_vis", curPellet), grid[x][y].IsVisible()); + curPellet++; + } + else if (grid[x][y].GetType() == ARCADE_POWERUP) { + gui->SetStateInt(va("powerup%d_x", curPowerup), x * ARCADE_CELL_WIDTH); + gui->SetStateInt(va("powerup%d_y", curPowerup), y * ARCADE_CELL_HEIGHT); + gui->SetStateBool(va("powerup%d_vis", curPowerup), grid[x][y].IsVisible()); + curPowerup++; + } + } + } + } + + if (fruit.Changed()) { + gui->SetStateInt("fruit_x", fruit.x * ARCADE_CELL_WIDTH); + gui->SetStateInt("fruit_y", fruit.y * ARCADE_CELL_HEIGHT); + gui->SetStateBool("fruit_vis", fruit.IsVisible()); + } + + if (score > highscore) { + highscore = score; + highscoreName = cvarSystem->GetCVarString("ui_name"); + } + + gui->SetStateInt("maze", (level-1)%numMazes+1); + gui->SetStateInt("level", level); + gui->SetStateInt("score", score); + gui->SetStateInt("lives", lives); + gui->SetStateInt("highscore", highscore); + gui->SetStateString("highscorename", highscoreName.c_str()); + gui->SetStateBool("gameover", bGameOver); + gui->StateChanged(gameLocal.time, true); + } +} + +bool hhArcadeGame::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + + idToken token; + + if (!src->ReadToken(&token)) { + return false; + } + + if (token == ";") { + return false; + } + + if (token.Icmp("startgame") == 0) { + // Trigger this entity, used for tip system + idEntity *agTrig = gameLocal.FindEntity( "arcadegame_used" ); + if( agTrig ) { + agTrig->PostEventMS( &EV_Activate, 0, this ); + } + + BecomeActive(TH_MISC3); + StartGame(); + } + else if (token.Icmp("reset") == 0) { + BecomeInactive(TH_MISC3); + bSimulating = false; + bGameOver = true; + bPlayerMoving = false; + playerMove.Set(1,0); + ResetMap(); + ResetPlayerAndMonsters(); + ResetGame(); + highscore = 1000; + highscoreName = DEFAULT_HIGH_SCORE_NAME; + + UpdateView(); + } + else { + src->UnreadToken(&token); + return false; + } + + return true; +} + +void hhArcadeGame::PlayerControls(usercmd_t *cmd) { + if (bGameOver && (cmd->buttons & BUTTON_ATTACK)) { + StartGame(); + } + + // These come from the player every tick + if (cmd->rightmove != 0 || cmd->forwardmove != 0) { + playerMove.x = cmd->rightmove > 0 ? 1 : cmd->rightmove < 0 ? -1 : 0; + playerMove.y = cmd->forwardmove < 0 ? 1 : cmd->forwardmove > 0 ? -1 : 0; + } +} + +idVec2 MoveForDirection(int dir) { + switch(dir) { + case PAC_MOVE_LEFT: return idVec2(-1, 0); + case PAC_MOVE_RIGHT: return idVec2(1, 0); + case PAC_MOVE_UP: return idVec2(0, -1); + case PAC_MOVE_DOWN: return idVec2(0, 1); + } + return idVec2(0,0); +} + +bool hhArcadeGame::MoveIsValid(idVec2 &move, MovingGamePiece &piece) { + int newX = piece.x + move.x; + int newY = piece.y + move.y; + int cell = grid[newX][newY].GetType(); + + if (cell == ARCADE_CLIPPLAYER) { + // Hack: disallow reentry into ghost cage. Assumes the monster start is adjacent to the playerclip + int sourceCell = grid[piece.x][piece.y].GetType(); + if (sourceCell != ARCADE_MONSTERSTART) { + return false; + } + } + return (cell != ARCADE_SOLID && (cell != ARCADE_CLIPPLAYER || !piece.isPlayer ) ); +} + +void hhArcadeGame::GrowPellets() { + if (!bSimulating) { + return; + } + + int growth = 0; + int i, j; + int x, y; + int startX = gameLocal.random.RandomInt(ARCADE_GRID_WIDTH); + int startY = gameLocal.random.RandomInt(ARCADE_GRID_HEIGHT); + + StartSound("snd_grow", SND_CHANNEL_ANY, 0, true, NULL); + + for (i=0; i 4) { + return; + } + } + } + } +} + +void hhArcadeGame::CheckForCollisions(MovingGamePiece &piece) { + int x = piece.x; + int y = piece.y; + int ix; + int type; + int destX, destY; + + switch(grid[x][y].GetType()) { + case ARCADE_TELEPORT: + FindType(ARCADE_TELEPORT, destX, destY, x, y); + piece.Set(destX, destY); + break; + + case ARCADE_PELLET: + if (piece.isPlayer && grid[x][y].IsVisible()) { + grid[x][y].SetVisible(false); + AddScore(1); + StartSound("snd_pellet", SND_CHANNEL_ANY, 0, true, NULL); + } + break; + + case ARCADE_POWERUP: + if (piece.isPlayer && grid[x][y].IsVisible()) { + AddScore(50); + grid[x][y].SetVisible(false); + monsters[0].vulnerableTimeRemaining += 5000 / level; + monsters[1].vulnerableTimeRemaining += 5000 / level; + monsters[2].vulnerableTimeRemaining += 5000 / level; + monsters[3].vulnerableTimeRemaining += 5000 / level; + bPoweredUp = true; + StopSound(SND_CHANNEL_ITEM, true); + StartSound("snd_powerup", SND_CHANNEL_ANY, 0, true, NULL); + StartSound("snd_poweruploop", SND_CHANNEL_ITEM, 0, true, NULL); + } + break; + } + + if (piece.isPlayer) { + if (fruit.IsVisible() && piece.x==fruit.x && piece.y==fruit.y) { + fruit.SetVisible(false); + AddScore(1000*level); + StartSound("snd_fruit", SND_CHANNEL_ANY, 0, true, NULL); + CancelEvents(&EV_ToggleFruit); + PostEventMS(&EV_ToggleFruit, FRUIT_DELAY); + } + } + + // player / monster collisions + for (ix=0; ix<4; ix++) { + if (monsters[ix].IsVisible() && player.x==monsters[ix].x && player.y==monsters[ix].y) { + if (monsters[ix].vulnerableTimeRemaining > 0) { + monsters[ix].SetVisible(false); + StartSound("snd_eatmonster", SND_CHANNEL_ANY, 0, true, NULL); + AddScore(500); + PostEventMS(&EV_MonsterRespawn, 3000, ix); + } + else if (!gameLocal.GetLocalPlayer()->godmode) { + bSimulating = false; + player.SetVisible(false); + player.dying = true; + StartSound("snd_die", SND_CHANNEL_ANY, 0, true, NULL); + if (lives <= 0) { + PostEventMS(&EV_GameOver, 0); + } + else { + lives--; + PostEventMS(&EV_PlayerRespawn, 2000); + } + return; + } + } + } + + // Check for life bonus + if (score >= nextScoreBoost) { + lives++; + nextScoreBoost *= 2; + StartSound("snd_lifeboost", SND_CHANNEL_ANY, 0, true, NULL); + } + + // Check for level completion + if (piece.isPlayer) { + bool levelCompleted = true; + for (x=0; levelCompleted && x 0 && (level+1 >= victoryAmount)) { + ActivateTargets( gameLocal.GetLocalPlayer() ); + StartSound( "snd_victory", SND_CHANNEL_ANY ); + victoryAmount = 0; + } + StartSound("snd_leveldone", SND_CHANNEL_ANY, 0, true, NULL); + PostEventMS(&EV_NextMap, 2000); + } + } +} + +bool hhArcadeGame::DoMove(idVec2 &move, MovingGamePiece &piece) { + + bool bMoved = move.x || move.y; + piece.Move(move); + + if (piece.isPlayer) { + // Acount for player movement direction + bPlayerMoving = bMoved; + } + + CheckForCollisions(piece); + + return bMoved; +} + +void hhArcadeGame::DoPlayerMove() { + bPlayerMoving = false; + if (!bSimulating) { + return; + } + + bool bValidXMove = playerMove.x && MoveIsValid(idVec2(playerMove.x, 0), player); + bool bValidYMove = playerMove.y && MoveIsValid(idVec2(0, playerMove.y), player); + + // Allow move direction to alternate between X and Y + if (player.lastMove.x) { + if (bValidYMove) { + DoMove(idVec2(0, playerMove.y), player); + } + else if (bValidXMove) { + DoMove(idVec2(playerMove.x, 0), player); + } + else if (MoveIsValid(player.lastMove, player)) { + DoMove(player.lastMove, player); + } + } + else { + if (bValidXMove) { + DoMove(idVec2(playerMove.x, 0), player); + } + else if (bValidYMove) { + DoMove(idVec2(0, playerMove.y), player); + } + else if (MoveIsValid(player.lastMove, player)) { + DoMove(player.lastMove, player); + } + } +} + +void hhArcadeGame::DoMonsterAI(MovingGamePiece &monster, int index) { + if (!bSimulating) { + return; + } + + if (monster.vulnerableTimeRemaining > 0) { + monster.vulnerableTimeRemaining -= monster.moveDelay; + monster.vulnerableTimeRemaining = idMath::ClampInt(0, 100000, monster.vulnerableTimeRemaining); + } + + idVec2 move; + idVec2 toPlayer; + idVec2 toPlayerMove; + int playerDirX, playerDirY; + toPlayer.Set(player.x - monster.x, player.y - monster.y); + playerDirX = toPlayer.x < 0 ? PAC_MOVE_LEFT : PAC_MOVE_RIGHT; + playerDirY = toPlayer.y < 0 ? PAC_MOVE_UP : PAC_MOVE_DOWN; + toPlayerMove.Set( + toPlayer.x > 0 ? 1 : toPlayer.x < 0 ? -1 : 0, + toPlayer.y > 0 ? 1 : toPlayer.y < 0 ? -1 : 0 + ); + idVec2 illegalMove = -monster.lastMove; + bool bGoTowardPlayer = gameLocal.random.RandomInt(4) >= index; + int choice; + int startingChoice = index; // Makes each monster unique + int j; + + // Keep moving in direction of last move until there is an intersection + // At intersection, choose between one of the directions, favoring one going towards + // the player. Never go back in the direction you came from unless at a dead end. + + // Go towards player if possible + if (bGoTowardPlayer) { + move = MoveForDirection(playerDirX); + if (MoveIsValid(move, monster) && move != illegalMove) { + DoMove(move, monster); + return; + } + move = MoveForDirection(playerDirY); + if (MoveIsValid(move, monster) && move != illegalMove) { + DoMove(move, monster); + return; + } + } + + // Otherwise move in one of the other valid directions + for (j=0; j<4; j++) { + choice = (startingChoice + j) % 4; + move = MoveForDirection(choice); + if (MoveIsValid(move, monster) && move != illegalMove) { + DoMove(move, monster); + return; + } + } + + // If still haven't found a valid move, must be at a dead end, now allow illegalMove + for (j=0; j<4; j++) { + choice = (startingChoice + j) % 4; + move = MoveForDirection(choice); + if (MoveIsValid(move, monster)) { + DoMove(move, monster); + return; + } + } +} + +void hhArcadeGame::Think() { + hhConsole::Think(); + + if (thinkFlags & TH_MISC3) { + if (bSimulating) { + // Player + if (gameLocal.time > player.nextMoveTime) { + player.nextMoveTime = gameLocal.time + player.moveDelay; + if (playerMove.LengthSqr() > 0.0f) { + DoPlayerMove(); + } + } + + // AI + for (int ix=0; ix<4; ix++) { + if (gameLocal.time > monsters[ix].nextMoveTime) { + monsters[ix].nextMoveTime = gameLocal.time + monsters[ix].moveDelay; + DoMonsterAI(monsters[ix], ix); + } + } + + // See if power up sound should end + if (bPoweredUp) { + bPoweredUp = false; + for (int ix=0; ix<4; ix++) { + if (monsters[ix].vulnerableTimeRemaining > 0) { + bPoweredUp = true; + break; + } + } + if (!bPoweredUp) { + StopSound(SND_CHANNEL_ITEM, true); + } + } + + if (gameLocal.time > nextPelletTime) { + nextPelletTime = gameLocal.time + 3500; + //GrowPellets(); + } + + UpdateView(); + } + } +} + +void hhArcadeGame::AddScore(int amount) { + if (!gameLocal.GetLocalPlayer()->godmode) { + score += amount; + } +} + +void hhArcadeGame::Event_GameStart() { + bSimulating = true; + PostEventMS(&EV_ToggleFruit, FRUIT_DELAY); +} + +void hhArcadeGame::Event_GameOver() { // Player ran out of lives + CancelEvents(&EV_ToggleFruit); + StopSound(SND_CHANNEL_ITEM, true); + bSimulating = false; + bGameOver = true; + CallNamedEvent("gameover"); + UpdateView(); +} + +void hhArcadeGame::Event_NextMap() { // Transition to next map + CancelEvents(&EV_ToggleFruit); + StopSound(SND_CHANNEL_ITEM, true); + GetMazeForLevel(++level); + ResetMap(); + ResetPlayerAndMonsters(); + StartSound("snd_intro", SND_CHANNEL_ANY, 0, true, NULL); + PostEventMS(&EV_GameStart, 2000); + UpdateView(); +} + +void hhArcadeGame::Event_PlayerRespawn() { // Player died, respawn + CancelEvents(&EV_ToggleFruit); + ResetPlayerAndMonsters(); + StartSound("snd_intro", SND_CHANNEL_ANY, 0, true, NULL); + player.SetVisible(true); + player.dying = false; + PostEventMS(&EV_GameStart, 3000); + UpdateView(); +} + +void hhArcadeGame::Event_MonsterRespawn(int index) { // Monster died, respawn + int x,y; + FindType(ARCADE_MONSTERSTART, x, y); + monsters[index].SetVisible(true); + monsters[index].vulnerableTimeRemaining = 0; + monsters[index].Set(x,y); + monsters[index].nextMoveTime = gameLocal.time + 3000; +} + +void hhArcadeGame::Event_ToggleFruit() { + if (fruit.IsVisible()) { + fruit.SetVisible(false); + PostEventMS(&EV_ToggleFruit, FRUIT_DELAY); + } + else { + fruit.SetVisible(true); + PostEventMS(&EV_ToggleFruit, 5000); + } +} + +void hhArcadeGame::Event_CallGuiEvent(const char *eventName) { + + if (!idStr::Icmp(eventName, "turnoff")) { + // Release any players routing commands to us + for( int i = 0; i < gameLocal.numClients; i++ ) { + if ( gameLocal.entities[i] && gameLocal.entities[i]->IsType(hhPlayer::Type) ) { + hhPlayer *player = static_cast(gameLocal.entities[i]); + if (player->guiWantsControls.IsValid() && player->guiWantsControls.GetEntity() == this) { + player->guiWantsControls = NULL; + } + } + } + } + + CallNamedEvent(eventName); +} + +void hhArcadeGame::LockedGuiReleased(hhPlayer *player) { + idEntity *agTrig = gameLocal.FindEntity( "arcadegame_released" ); + if( agTrig ) { + agTrig->PostEventMS( &EV_Activate, 0, this ); + } +} + diff --git a/src/Prey/game_arcadegame.h b/src/Prey/game_arcadegame.h new file mode 100644 index 0000000..4844d94 --- /dev/null +++ b/src/Prey/game_arcadegame.h @@ -0,0 +1,188 @@ + +#ifndef __GAME_ARCADEGAME_H__ +#define __GAME_ARCADEGAME_H__ + +#define ARCADE_GRID_WIDTH 19 +#define ARCADE_GRID_HEIGHT 21 +#define ARCADE_CELL_WIDTH 20 +#define ARCADE_CELL_HEIGHT 20 + +#define ARCADE_EMPTY 0 +#define ARCADE_SOLID 1 +#define ARCADE_PELLET 2 +#define ARCADE_POWERUP 3 +#define ARCADE_CLIPPLAYER 4 +#define ARCADE_TELEPORT 5 +#define ARCADE_FRUIT 6 +#define ARCADE_PLAYERSTART 7 +#define ARCADE_MONSTERSTART 8 + +enum { + PAC_MOVE_LEFT=0, + PAC_MOVE_RIGHT=1, + PAC_MOVE_UP=2, + PAC_MOVE_DOWN=3 +}; + +class GamePiece { +public: + GamePiece() { type = ARCADE_EMPTY; visible = true; changed = true; } + virtual ~GamePiece() { } + void SetType(char t) { type = t; changed = true; } + void SetVisible(bool v) { visible = v; changed = true; } + char GetType() { return type; } + bool IsVisible() { return visible; } + bool Changed() { return changed; } + virtual void Save( idSaveGame *savefile ) const { + savefile->WriteByte(type); + savefile->WriteBool(visible); + savefile->WriteBool(changed); + } + virtual void Restore( idRestoreGame *savefile ) { + savefile->ReadByte(type); + savefile->ReadBool(visible); + savefile->ReadBool(changed); + } + +protected: + byte type; + bool visible; + bool changed; +}; + +class MovingGamePiece : public GamePiece { +public: + MovingGamePiece() { + x = y = 0; isPlayer = false; dying = false; + lastMove.Zero(); + lastMoveTime = 0; + angle = 0.0f; + moveDelay = 250; + nextMoveTime = 0; + vulnerableTimeRemaining = 0; + } + void Set(int nx, int ny, bool nvis=true) { + x=nx; y=ny; visible = nvis; + lastMove.Zero(); + lastMoveTime = gameLocal.time; + changed = true; + } + void Move(idVec2 move) { + x += move.x; + y += move.y; + lastMove = move; + lastMoveTime = gameLocal.time; + angle = + lastMove.x > 0 ? 0.0f : + lastMove.x < 0 ? 180.0f : + lastMove.y < 0 ? 90.0f : + lastMove.y > 0 ? 270.0f : angle; + + changed = true; + } + virtual void Save( idSaveGame *savefile ) const { + GamePiece::Save(savefile); + savefile->WriteInt(x); + savefile->WriteInt(y); + savefile->WriteBool(isPlayer); + savefile->WriteFloat(angle); + savefile->WriteBool(dying); + savefile->WriteVec2(lastMove); + savefile->WriteInt(lastMoveTime); + savefile->WriteInt(moveDelay); + savefile->WriteInt(nextMoveTime); + savefile->WriteInt(vulnerableTimeRemaining); + } + virtual void Restore( idRestoreGame *savefile ) { + GamePiece::Restore(savefile); + savefile->ReadInt(x); + savefile->ReadInt(y); + savefile->ReadBool(isPlayer); + savefile->ReadFloat(angle); + savefile->ReadBool(dying); + savefile->ReadVec2(lastMove); + savefile->ReadInt(lastMoveTime); + savefile->ReadInt(moveDelay); + savefile->ReadInt(nextMoveTime); + savefile->ReadInt(vulnerableTimeRemaining); + } + + int x,y; + bool isPlayer; + bool dying; // used for player only + idVec2 lastMove; + float angle; + int lastMoveTime; + int moveDelay; // Delay in MS between moves + int nextMoveTime; // Next time piece is allowed to move + int vulnerableTimeRemaining; // monsters only, time before powerup wears off +}; + + +class hhArcadeGame : public hhConsole { +public: + CLASS_PROTOTYPE( hhArcadeGame ); + + hhArcadeGame(); + virtual ~hhArcadeGame(); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Think(); + virtual bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + virtual void PlayerControls(usercmd_t *cmd); + virtual void ConsoleActivated(); + virtual void LockedGuiReleased(hhPlayer *player); + + void StartGame(); + bool MoveIsValid(idVec2 &move, MovingGamePiece &piece); + bool DoMove(idVec2 &move, MovingGamePiece &piece); + void CheckForCollisions(MovingGamePiece &piece); + void ResetMap(); + void ResetPlayerAndMonsters(); + void ResetGame(); + void GrowPellets(); + void DoPlayerMove(); + void DoMonsterAI(MovingGamePiece &monster, int index); + idVec2 SmoothMovement(MovingGamePiece &piece); + void UpdateView(); + void FindType(int type, int &x, int &y, int skipX=-1, int skipY=-1); + void GetMazeForLevel(int lev); + void AddScore(int amount); + +protected: + void Event_GameStart(); + void Event_GameOver(); + void Event_PlayerRespawn(); + void Event_MonsterRespawn(int index); + void Event_ToggleFruit(); + void Event_NextMap(); + void Event_CallGuiEvent(const char *eventName); + +protected: + char startingGrid[ARCADE_GRID_WIDTH][ARCADE_GRID_HEIGHT]; + GamePiece grid[ARCADE_GRID_WIDTH][ARCADE_GRID_HEIGHT]; + MovingGamePiece monsters[4]; + MovingGamePiece player; + MovingGamePiece fruit; + idVec2 playerMove; + idStr highscoreName; + bool bPlayerMoving; + bool bSimulating; + bool bGameOver; + bool bPoweredUp; + int score; + int highscore; + int lives; + int level; + int numMazes; + int nextScoreBoost; + int nextPelletTime; + +private: + int victoryAmount; +}; + + +#endif diff --git a/src/Prey/game_barrel.cpp b/src/Prey/game_barrel.cpp new file mode 100644 index 0000000..e610f5f --- /dev/null +++ b/src/Prey/game_barrel.cpp @@ -0,0 +1,181 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +CLASS_DECLARATION( hhMoveable, hhBarrel ) +END_CLASS + +/* +================ +hhBarrel::hhBarrel +================ +*/ +hhBarrel::hhBarrel() { + radius = 1.0f; + barrelAxis = 0; + lastOrigin.Zero(); + lastAxis.Identity(); + additionalRotation = 0.0f; + additionalAxis.Identity(); + fl.networkSync = true; +} + +/* +================ +hhBarrel::Save +================ +*/ +void hhBarrel::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( radius ); + savefile->WriteInt( barrelAxis ); + savefile->WriteVec3( lastOrigin ); + savefile->WriteMat3( lastAxis ); + savefile->WriteFloat( additionalRotation ); + savefile->WriteMat3( additionalAxis ); +} + +/* +================ +hhBarrel::Restore +================ +*/ +void hhBarrel::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( radius ); + savefile->ReadInt( barrelAxis ); + savefile->ReadVec3( lastOrigin ); + savefile->ReadMat3( lastAxis ); + savefile->ReadFloat( additionalRotation ); + savefile->ReadMat3( additionalAxis ); +} + +/* +================ +hhBarrel::BarrelThink +================ +*/ +void hhBarrel::BarrelThink( void ) { + bool wasAtRest, onGround; + float movedDistance, rotatedDistance, angle; + idVec3 curOrigin, gravityNormal, dir; + idMat3 curAxis, axis; + + wasAtRest = IsAtRest(); + + // run physics + RunPhysics(); + + // only need to give the visual model an additional rotation if the physics were run + if ( !wasAtRest ) { + + // current physics state + onGround = GetPhysics()->HasGroundContacts(); + curOrigin = GetPhysics()->GetOrigin(); + curAxis = GetPhysics()->GetAxis(); + + // if the barrel is on the ground + if ( onGround ) { + gravityNormal = GetPhysics()->GetGravityNormal(); + + dir = curOrigin - lastOrigin; + dir -= gravityNormal * dir * gravityNormal; + movedDistance = dir.LengthSqr(); + + // if the barrel moved and the barrel is not aligned with the gravity direction + if ( movedDistance > 0.0f && idMath::Fabs( gravityNormal * curAxis[barrelAxis] ) < 0.7f ) { + + // barrel movement since last think frame orthogonal to the barrel axis + movedDistance = idMath::Sqrt( movedDistance ); + dir *= 1.0f / movedDistance; + movedDistance = ( 1.0f - idMath::Fabs( dir * curAxis[barrelAxis] ) ) * movedDistance; + + // get rotation about barrel axis since last think frame + angle = lastAxis[(barrelAxis+1)%3] * curAxis[(barrelAxis+1)%3]; + angle = idMath::ACos( angle ); + // distance along cylinder hull + rotatedDistance = angle * radius; + + // if the barrel moved further than it rotated about it's axis + if ( movedDistance > rotatedDistance ) { + + // additional rotation of the visual model to make it look + // like the barrel rolls instead of slides + angle = 180.0f * (movedDistance - rotatedDistance) / (radius * idMath::PI); + if ( gravityNormal.Cross( curAxis[barrelAxis] ) * dir < 0.0f ) { + additionalRotation += angle; + } else { + additionalRotation -= angle; + } + dir = vec3_origin; + dir[barrelAxis] = 1.0f; + additionalAxis = idRotation( vec3_origin, dir, additionalRotation ).ToMat3(); + } + } + } + + // save state for next think + lastOrigin = curOrigin; + lastAxis = curAxis; + } + + Present(); +} + +/* +================ +hhBarrel::Think +================ +*/ +void hhBarrel::Think( void ) { + if ( thinkFlags & TH_THINK ) { + if ( !FollowInitialSplinePath() ) { + BecomeInactive( TH_THINK ); + } + } + + BarrelThink(); +} + +/* +================ +hhBarrel::GetPhysicsToVisualTransform +================ +*/ +bool hhBarrel::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + origin = vec3_origin; + axis = additionalAxis; + return true; +} + +/* +================ +hhBarrel::Spawn +================ +*/ +void hhBarrel::Spawn( void ) { + const idBounds &bounds = GetPhysics()->GetBounds(); + + // radius of the barrel cylinder + radius = ( bounds[1][0] - bounds[0][0] ) * 0.5f; + + // always a vertical barrel with cylinder axis parallel to the z-axis + barrelAxis = 2; + + lastOrigin = GetPhysics()->GetOrigin(); + lastAxis = GetPhysics()->GetAxis(); + + additionalRotation = 0.0f; + additionalAxis.Identity(); +} + +/* +================ +hhBarrel::ClientPredictionThink +================ +*/ +void hhBarrel::ClientPredictionThink( void ) { + Think(); +} + + diff --git a/src/Prey/game_barrel.h b/src/Prey/game_barrel.h new file mode 100644 index 0000000..db21d80 --- /dev/null +++ b/src/Prey/game_barrel.h @@ -0,0 +1,30 @@ +#ifndef __HH_BARREL_H +#define __HH_BARREL_H + +class hhBarrel : public hhMoveable { + +public: + CLASS_PROTOTYPE( hhBarrel ); + hhBarrel(); + + void Spawn( void ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void BarrelThink( void ); + virtual void Think( void ); + virtual bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); + virtual void ClientPredictionThink( void ); + +private: + float radius; // radius of barrel + int barrelAxis; // one of the coordinate axes the barrel cylinder is parallel to + idVec3 lastOrigin; // origin of the barrel the last think frame + idMat3 lastAxis; // axis of the barrel the last think frame + float additionalRotation; // additional rotation of the barrel about it's axis + idMat3 additionalAxis; // additional rotation axis +}; + + +#endif \ No newline at end of file diff --git a/src/Prey/game_bindController.cpp b/src/Prey/game_bindController.cpp new file mode 100644 index 0000000..5541d1b --- /dev/null +++ b/src/Prey/game_bindController.cpp @@ -0,0 +1,290 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//========================================================================== +// +// hhBindController +// +// These can be bound to anything. They can be used to attach a player, monster, almost any entity +// and take control of their movement through space. Players are optionally allowed to look around +// while bound. This class was necessary to abstract all the physics differences away from the +// objects doing the binding. +//========================================================================== + +const idEventDef EV_BindAttach("bindattach", "ed"); +const idEventDef EV_BindDetach("binddrop", NULL); +const idEventDef EV_BindAttachBody("bindattachbody", "edd"); + +CLASS_DECLARATION(idEntity, hhBindController) + EVENT( EV_BindAttach, hhBindController::Event_Attach ) + EVENT( EV_BindDetach, hhBindController::Event_Detach ) + EVENT( EV_BindAttachBody, hhBindController::Event_AttachBody ) +END_CLASS + + +void hhBindController::Spawn() { + boundRider = NULL; + hand = NULL; + yawLimit = 180.0f; // no limit + float tension = spawnArgs.GetFloat("tension", "0.01"); + const char *anim = spawnArgs.GetString("boundanim"); + const char *hand = spawnArgs.GetString("def_hand"); + float yaw = spawnArgs.GetFloat("yawlimit", "180"); + float pitch = spawnArgs.GetFloat("pitchlimit", "75"); + bOrientPlayer = spawnArgs.GetBool("orientplayer"); + + bLooseBind = false; + + SetRiderParameters(anim, hand, yaw, pitch); + SetTension(tension); + hangID = 0; +} + +hhBindController::~hhBindController() { + Detach(); +} + +void hhBindController::Save(idSaveGame *savefile) const { + boundRider.Save(savefile); + savefile->WriteObject( hand ); + savefile->WriteBool( bLooseBind ); + savefile->WriteBool( bOrientPlayer ); + savefile->WriteStaticObject( force ); + savefile->WriteString( animationName ); + savefile->WriteString( handName ); + savefile->WriteFloat( yawLimit ); + savefile->WriteFloat(pitchLimit); + savefile->WriteInt( hangID ); +} + +void hhBindController::Restore( idRestoreGame *savefile ) { + boundRider.Restore(savefile); + savefile->ReadObject( reinterpret_cast(hand) ); + savefile->ReadBool( bLooseBind ); + savefile->ReadBool( bOrientPlayer ); + savefile->ReadStaticObject( force ); + savefile->ReadString( animationName ); + savefile->ReadString( handName ); + savefile->ReadFloat( yawLimit ); + savefile->ReadFloat(pitchLimit); + savefile->ReadInt( hangID ); +} + +idEntity *hhBindController::GetRider() const { + // Since the entity could be deleted at any time, we get the rider from the force (if used) + if (bLooseBind) { + //TODO: Make this force use a safe entityptr too + return force.GetEntity(); + } + return boundRider.GetEntity(); +} + +void hhBindController::SetTension(float tension) { + force.SetRestoreTime((1.0f - tension)*5.0f); + force.SetRestoreFactor(tension * g_springConstant.GetFloat()); +} + +void hhBindController::SetSlack(float slack) { + force.SetRestoreSlack(slack); +} + +void hhBindController::Think() { + idEntity::Think(); //TODO: This needed? + + if (bLooseBind && GetRider()) { + idVec3 loc = GetOrigin(); + force.SetTarget(loc); + + //TODO: Cull hangID out of this class if not needed. + + + // FIXME: Why is this needed? (Took out --pdm, test crane/rail before removing) +/* + idEntity *rider = GetRider(); + if (rider->IsType(idAFEntity_Base::Type)) { // Update the offset depending on if actor is ragdoll + idAFEntity_Base *af = static_cast(rider); + if (af->IsActiveAF()) { + force.SetEntity(rider, hangID, vec3_origin); + } + else { // non-ragdolled rider + if (rider->IsType(idActor::Type)) { + force.SetEntity(rider, 0, af->EyeOffset()); + } + else { + force.SetEntity(rider, 0, rider->GetOrigin()); + } + } + } +*/ + force.Evaluate(gameLocal.time); + } + else if (GetRider() && GetRider()->IsType(hhPlayer::Type) && OrientPlayer()) { + // This to insure that oriented players don't get out of sync + hhPlayer *player = static_cast(GetRider()); + idAngles angles = player->GetUntransformedViewAngles(); + player->SetOrientation(GetOrigin(), GetAxis(), angles.ToMat3()[0], angles); + } +} + +// Create our invisible hand to get rid of the weapon +void hhBindController::CreateHand(hhPlayer *player) { + if (gameLocal.isClient) { //rww + return; + } + + if (handName.Length()) { + ReleaseHand(); + + hand = hhHand::AddHand( player, handName.c_str() ); + } +} + +void hhBindController::ReleaseHand() { + if (hand) { + hand->RemoveHand(); + hand = NULL; + } +} + +void hhBindController::SetRiderParameters(const char *animname, const char *handname, float yaw, float pitch) { + animationName = animname; + handName = handname; + yawLimit = yaw; + pitchLimit = pitch; +} + +bool hhBindController::ValidRider(idEntity *ent) const { + return (ent != NULL); +} + +void hhBindController::Attach(idEntity *ent, bool loose, int bodyID, idVec3 &point) { + hhPlayer *player = ent->IsType(hhPlayer::Type) ? static_cast(ent) : NULL; + + if (GetRider()==NULL && ValidRider(ent)) { + + bLooseBind = loose; + + if (bLooseBind) { + idVec3 loc = GetOrigin(); + force.SetEntity(ent, bodyID, point); + force.SetTarget(loc); + if (ent->spawnArgs.GetBool("stable_tractor", "0")) { + force.SetAxisEntity(this); + } else { + force.SetAxisEntity(NULL); + } + BecomeActive(TH_THINK); + } + else { + boundRider = ent; + boundRider->SetOrigin(GetOrigin()); + + if (player && bOrientPlayer) { + idAngles angles = player->GetUntransformedViewAngles(); + player->SetOrientation(GetOrigin(), GetAxis(), angles.ToMat3()[0], angles); + } + else { + boundRider->SetAxis(GetAxis()); + } + boundRider->Bind(this, false); + } + + // Apply player parameters + if (player && !gameLocal.isMultiplayer) { //rww - in mp, don't want to restrict players when bound + player->maxRelativeYaw = yawLimit; + player->maxRelativePitch = pitchLimit; + player->bClampYaw = yawLimit < 180.0f; + CreateHand(player); +// player->hhweapon->Hide(); + } + // Play animation on actors (but not players in mp -rww) + if (ent->IsType(idActor::Type) && !ent->fl.tooHeavyForTractor && (!gameLocal.isMultiplayer || !ent->IsType(hhPlayer::Type))) { + idActor *actor = static_cast(ent); + actor->AI_BOUND = true; + if ( actor->GetHealth() > 0 ) { + actor->ProcessEvent(&AI_SetAnimPrefix, animationName.c_str()); + actor->SetAnimState( ANIMCHANNEL_LEGS, "Legs_Bound", 4 ); + actor->SetAnimState( ANIMCHANNEL_TORSO, "Torso_Bound", 4 ); + } + actor->BecameBound(this); + } + + hangID = 0; + if (ent->IsType(idAFEntity_Base::Type)) { + hangID = static_cast(ent)->spawnArgs.GetInt("hangID"); + } + } +} + +void hhBindController::Detach() { + idEntity *rider = GetRider(); + hhPlayer *player = rider && rider->IsType(hhPlayer::Type) ? static_cast(rider) : NULL; + if (rider) { + if (player) { + player->bClampYaw = false; + + if (gameLocal.isMultiplayer) { //do not fiddle with the player's anims in MP when he is grabbed, let him act normally + // Release player animation + player->AI_BOUND = false; + player->SetAnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 4 ); + player->SetAnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 4 ); + } + + ReleaseHand(); + } + if (rider->IsType(idActor::Type)) { + idActor *actor = static_cast(rider); + actor->AI_BOUND = false; + actor->BecameUnbound(this); + } + + if (bLooseBind) { + force.SetEntity(NULL); + } + else { + rider->Unbind(); + rider->SetAxis(mat3_identity); + if (player && bOrientPlayer) { + // Set everything back to normal at current view angles + idAngles angles = player->GetUntransformedViewAngles(); + player->SetOrientation(GetOrigin(), mat3_identity, angles.ToMat3()[0], angles); + } + boundRider = NULL; + } + } +} + +idVec3 hhBindController::GetRiderBindPoint() const { + idEntity* rider = GetRider(); + + if( !rider ) { + return vec3_origin; + } + + if( rider->IsType(idActor::Type) ) { + idActor *actor = static_cast( rider ); + if( actor->IsActiveAF() ) { + return actor->GetOrigin( GetHangID() ); + } else { + return actor->GetEyePosition(); + } + } + + return rider->GetOrigin(); +} + +void hhBindController::Event_Attach(idEntity *rider, bool loose) { + Attach(rider, loose); +} + +void hhBindController::Event_AttachBody(idEntity *rider, bool loose, int bodyID) { + Attach(rider, loose, bodyID); +} + +void hhBindController::Event_Detach() { + Detach(); +} + diff --git a/src/Prey/game_bindController.h b/src/Prey/game_bindController.h new file mode 100644 index 0000000..aa6c137 --- /dev/null +++ b/src/Prey/game_bindController.h @@ -0,0 +1,68 @@ + +#ifndef __HH_GAME_BINDCONTROLLER_H__ +#define __HH_GAME_BINDCONTROLLER_H__ + + +/* +=================================================================================== + + hhBindController + + Entity used to control an entity's position in space. + Used for rail rides, tractor beams, crane, etc. + +=================================================================================== +*/ + +extern const idEventDef EV_BindAttach; +extern const idEventDef EV_BindDetach; + +class hhBindController : public idEntity { + +public: + CLASS_PROTOTYPE( hhBindController ); + + void Spawn(); + virtual ~hhBindController(); + virtual void Think(); + virtual void Attach(idEntity *ent, bool loose=false, int bodyID=0, idVec3 &point=vec3_origin); + virtual void Detach(); + void SetTension(float tension); + void SetSlack(float slack); + void SetRiderParameters(const char *animname, const char *handname, float yaw, float pitch); + idVec3 GetRiderBindPoint() const; + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + bool OrientPlayer() const { return bOrientPlayer; } + idEntity * GetRider() const; + ID_INLINE bool IsLoose() const { return bLooseBind; } + ID_INLINE int GetHangID() const { return hangID; } + ID_INLINE void SetShuttle(bool shuttle) { force.SetShuttle(shuttle); } + +protected: + bool ValidRider(idEntity *ent) const; + void CreateHand(hhPlayer *player); + void ReleaseHand(); + + void Event_Attach(idEntity *rider, bool loose); + void Event_AttachBody(idEntity *rider, bool loose, int bodyID); + void Event_Detach(); + +protected: + idEntityPtr boundRider; // Pointer to rider for true bind-based riders + // Loose bindings store rider in the force + hhHand * hand; + bool bLooseBind; // True if the bind is through a force + bool bOrientPlayer; // Orient the player to my orientation + hhForce_Converge force; // Force used for "loose" binding + + // Apply these to any entities bound + idStr animationName; // Animation to play on riders + idStr handName; // Hand to apply to player riders + float yawLimit; // Yaw restriction for player riders + float pitchLimit; //rww - used for slabs and other things to restrict pitch of rider + int hangID; // Body id to attach to for hanging +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_blackjack.cpp b/src/Prey/game_blackjack.cpp new file mode 100644 index 0000000..6574f74 --- /dev/null +++ b/src/Prey/game_blackjack.cpp @@ -0,0 +1,508 @@ +// game_blackjack +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +enum { + BJRESULT_BUST=0, + BJRESULT_PUSH=1, + BJRESULT_WIN=2, + BJRESULT_LOSE=3, + BJRESULT_BLACKJACK=4, + BJRESULT_5CARD=5 +}; + +const idEventDef EV_Deal("Deal", NULL); +const idEventDef EV_Hit("Hit", NULL); +const idEventDef EV_Stay("Stay", NULL); +const idEventDef EV_Double("Double", NULL); + +CLASS_DECLARATION(hhConsole, hhBlackJack) + EVENT( EV_Deal, hhBlackJack::Event_Deal) + EVENT( EV_Hit, hhBlackJack::Event_Hit) + EVENT( EV_Stay, hhBlackJack::Event_Stay) + EVENT( EV_Double, hhBlackJack::Event_Double) + EVENT( EV_UpdateView, hhBlackJack::Event_UpdateView) +END_CLASS + + +void hhBlackJack::Spawn() { + + Reset(); +} + +void hhBlackJack::Reset() { + bCanDeal = 1; + bCanIncBet = 1; + bCanDecBet = 1; + bCanHit = 0; + bCanStay = 0; + bCanDouble = 0; + bCanSplit = 0; + PlayerBet = Bet = 1; + DealerScore = PlayerScore = 0; + DealerAces = PlayerAces = 0; + PlayerCredits = spawnArgs.GetInt("credits"); + victoryAmount = spawnArgs.GetInt("victory"); + resultIndex = -1; + creditsWon = 0; + PlayerHand.Clear(); + DealerHand.Clear(); + + UpdateView(); +} + +void hhBlackJack::Save(idSaveGame *savefile) const { + int i; + + savefile->WriteInt( PlayerHand.Num() ); // Saving of idList + for( i = 0; i < PlayerHand.Num(); i++ ) { + savefile->Write(&PlayerHand[i], sizeof(card_t)); + } + savefile->WriteInt( DealerHand.Num() ); // Saving of idList + for( i = 0; i < DealerHand.Num(); i++ ) { + savefile->Write(&DealerHand[i], sizeof(card_t)); + } + + savefile->WriteInt(Bet); + savefile->WriteInt(PlayerBet); + savefile->WriteInt(DealerScore); + savefile->WriteInt(PlayerScore); + savefile->WriteInt(DealerAces); + savefile->WriteInt(PlayerAces); + savefile->WriteInt(PlayerCredits); + savefile->WriteInt(victoryAmount); + savefile->WriteInt(resultIndex); + savefile->WriteBool(bCanDeal); + savefile->WriteBool(bCanIncBet); + savefile->WriteBool(bCanDecBet); + savefile->WriteBool(bCanHit); + savefile->WriteBool(bCanStay); + savefile->WriteBool(bCanDouble); + savefile->WriteBool(bCanSplit); + savefile->WriteInt(creditsWon); +} + +void hhBlackJack::Restore( idRestoreGame *savefile ) { + int i, num; + + PlayerHand.Clear(); + savefile->ReadInt( num ); + PlayerHand.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->Read(&PlayerHand[i], sizeof(card_t)); + } + + DealerHand.Clear(); + savefile->ReadInt( num ); + DealerHand.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->Read(&DealerHand[i], sizeof(card_t)); + } + + savefile->ReadInt(Bet); + savefile->ReadInt(PlayerBet); + savefile->ReadInt(DealerScore); + savefile->ReadInt(PlayerScore); + savefile->ReadInt(DealerAces); + savefile->ReadInt(PlayerAces); + savefile->ReadInt(PlayerCredits); + savefile->ReadInt(victoryAmount); + savefile->ReadInt(resultIndex); + savefile->ReadBool(bCanDeal); + savefile->ReadBool(bCanIncBet); + savefile->ReadBool(bCanDecBet); + savefile->ReadBool(bCanHit); + savefile->ReadBool(bCanStay); + savefile->ReadBool(bCanDouble); + savefile->ReadBool(bCanSplit); + savefile->ReadInt(creditsWon); +} + +card_t hhBlackJack::GetCard(bool visible) +{ + card_t card; + char names[] = { '2','3','4','5','6','7','8','9','T','J','Q','K','A'}; + int values[] = { 2 , 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11}; + + int r = gameLocal.random.RandomInt(13); + card.Name = names[r]; + card.Value = values[r]; + card.Suit = gameLocal.random.RandomInt(4); + card.Visible = visible; + return card; +} + +void hhBlackJack::Deal() +{ + // Empty hands + PlayerHand.Clear(); + DealerHand.Clear(); + + Bet = PlayerBet; + creditsWon = 0; + + // Deal initial hands + PlayerHand.Append(GetCard(1)); + DealerHand.Append(GetCard(0)); + PlayerHand.Append(GetCard(1)); + DealerHand.Append(GetCard(1)); + + RetallyScores(); + + bCanIncBet = false; + bCanDecBet = false; + bCanDeal = false; + DeterminePlayCommands(); + resultIndex = -1; + + if (PlayerScore == 21) { + AssessScores(); + EndGame(); + } + + UpdateView(); +} + +void hhBlackJack::Hit() +{ + PlayerHand.Append(GetCard(1)); + RetallyScores(); + DeterminePlayCommands(); + + if (PlayerScore >= 21 || PlayerHand.Num() == 5) { + FollowDealerRules(); + AssessScores(); + EndGame(); + } + + UpdateView(); +} + +void hhBlackJack::Stay() +{ + FollowDealerRules(); + AssessScores(); + EndGame(); + UpdateView(); +} + +void hhBlackJack::Double() +{ + Bet *= 2; + PlayerHand.Append(GetCard(1)); + RetallyScores(); + DeterminePlayCommands(); + + // Force player to stay + FollowDealerRules(); + AssessScores(); + EndGame(); + + UpdateView(); +} + +void hhBlackJack::IncBet() { + + int amount = 1; + idUserInterface *gui = renderEntity.gui[0]; + if (gui) { + amount = gui->GetStateInt("increment"); + } + + int oldBet = PlayerBet; + PlayerBet = idMath::ClampInt(PlayerBet, PlayerCredits, PlayerBet+amount); + PlayerBet = idMath::ClampInt(0, 999999, PlayerBet); + Bet = PlayerBet; + if (PlayerBet != oldBet) { + StartSound( "snd_betchange", SND_CHANNEL_ANY ); + } + + UpdateView(); +} + +void hhBlackJack::DecBet() { + + int amount = 1; + idUserInterface *gui = renderEntity.gui[0]; + if (gui) { + amount = gui->GetStateInt("increment"); + } + + int oldBet = PlayerBet; + if (PlayerBet > amount) { + PlayerBet -= amount; + } + else if (PlayerBet > 1) { + PlayerBet = 1; + } + Bet = PlayerBet; + if (PlayerBet != oldBet) { + StartSound( "snd_betchange", SND_CHANNEL_ANY ); + } + + UpdateView(); +} + + +/* + Blackjack utility functions +*/ + +void hhBlackJack::RetallyScores() +{ + int ix; + PlayerScore = DealerScore = PlayerAces = DealerAces = 0; + + for (ix=0; ix 21 && PlayerAces) { + PlayerScore -= 10; + PlayerAces--; + } + for (ix=0; ix 21 && DealerAces) { + DealerScore -= 10; + DealerAces--; + } +} + +void hhBlackJack::AssessScores() +{ + if (PlayerScore > 21) { + creditsWon = -Bet; + resultIndex = BJRESULT_BUST; + } + else if (DealerScore > 21) { + creditsWon = Bet; + resultIndex = BJRESULT_WIN; + } + else if (PlayerScore == 21 && PlayerHand.Num() == 2) { + // BlackJack + creditsWon = Bet * 2; + resultIndex = BJRESULT_BLACKJACK; + } + else if (PlayerScore <= 21 && PlayerHand.Num() == 5) { + creditsWon = Bet * 5; + resultIndex = BJRESULT_5CARD; + } + else if (DealerScore > PlayerScore) { + creditsWon = -Bet; + resultIndex = BJRESULT_LOSE; + } + else if (PlayerScore > DealerScore) { + creditsWon = Bet; + resultIndex = BJRESULT_WIN; + } + else { + // Push + creditsWon = 0; + resultIndex = BJRESULT_PUSH; + } + + PlayerCredits += creditsWon; + PlayerCredits = idMath::ClampInt(0, 999999999, PlayerCredits); + Bet = PlayerBet; + + // Play victory/failure sound + if (victoryAmount && PlayerCredits >= victoryAmount) { + StartSound( "snd_victory", SND_CHANNEL_ANY, 0, true, NULL ); + ActivateTargets( gameLocal.GetLocalPlayer() ); + victoryAmount = 0; + } + else if (creditsWon > 0) { + StartSound( "snd_win", SND_CHANNEL_ANY, 0, true, NULL ); + } + else if (creditsWon < 0) { + StartSound( "snd_lose", SND_CHANNEL_ANY, 0, true, NULL ); + } +} + +void hhBlackJack::UpdateBetMechanics() { + bCanIncBet = PlayerCredits > PlayerBet; + bCanDecBet = PlayerBet > 0; + if (PlayerCredits < PlayerBet) { + PlayerBet = PlayerCredits; + } +} + +void hhBlackJack::EndGame() { + bCanDeal = 1; + UpdateBetMechanics(); + bCanHit = 0; + bCanStay = 0; + bCanDouble = 0; + bCanSplit = 0; +} + +void hhBlackJack::UpdateView() { + // UpdateView() is posted as an event because sometimes we're already nested down in the gui handling code when it is called + // and it in turn re-enters the gui handling code with HandleNamedEvent() + CancelEvents(&EV_UpdateView); + PostEventMS(&EV_UpdateView, 0); +} + +void hhBlackJack::Event_UpdateView() { + int ix; + bool bGameOver = false; + idUserInterface *gui = renderEntity.gui[0]; + + if (gui) { + if (PlayerCredits <= 0) { + bCanIncBet = bCanDecBet = bCanHit = bCanStay = bCanDouble = bCanSplit = bCanDeal = false; + bGameOver = true; + } + + gui->SetStateBool("bgameover", bGameOver); + gui->SetStateBool("bcanincbet", bCanIncBet); + gui->SetStateBool("bcandecbet", bCanDecBet); + gui->SetStateBool("bcanhit", bCanHit); + gui->SetStateBool("bcanstay", bCanStay); + gui->SetStateBool("bcandouble", bCanDouble); + gui->SetStateBool("bcansplit", bCanSplit); + gui->SetStateBool("bcandeal", bCanDeal); + gui->SetStateInt("credits", PlayerCredits); + gui->SetStateInt("currentbet", Bet); + gui->SetStateInt("result", resultIndex); + gui->SetStateInt("creditswon", creditsWon); + + // Clear + for (ix=0; ix<6; ix++) { + gui->SetStateInt(va("Dealer%d_Visible", ix+1), 0); + gui->SetStateInt(va("Player%d_Visible", ix+1), 0); + gui->SetStateBool(va("Dealer%d_Flipped", ix+1), 0); + gui->SetStateBool(va("Player%d_Flipped", ix+1), 0); + } + + // Show dealer hand + for (ix=0; ixSetStateInt(va("Dealer%d_Visible", ix+1), 1); + if (DealerHand[ix].Visible) { + gui->SetStateBool(va("Dealer%d_Flipped", ix+1), 1); + gui->SetStateInt(va("Dealer%d_Suit", ix+1), DealerHand[ix].Suit); + char cardChar = DealerHand[ix].Name; + if (cardChar == 'T') { + gui->SetStateString(va("Dealer%d_Card", ix+1), "10"); + } + else { + gui->SetStateString(va("Dealer%d_Card", ix+1), va("%c", cardChar)); + } + gui->SetStateInt(va("Dealer%d_Red", ix+1), DealerHand[ix].Suit==SUIT_HEARTS || DealerHand[ix].Suit==SUIT_DIAMONDS ? 1 : 0); + } + } + + // Show player hand + for (ix=0; ixSetStateInt(va("Player%d_Visible", ix+1), 1); + if (PlayerHand[ix].Visible) { + gui->SetStateBool(va("Player%d_Flipped", ix+1), 1); + gui->SetStateInt(va("Player%d_Suit", ix+1), PlayerHand[ix].Suit); + char cardChar = PlayerHand[ix].Name; + if (cardChar == 'T') { + gui->SetStateString(va("Player%d_Card", ix+1), "10"); + } + else { + gui->SetStateString(va("Player%d_Card", ix+1), va("%c", cardChar)); + } + gui->SetStateInt(va("Player%d_Red", ix+1), PlayerHand[ix].Suit==SUIT_HEARTS || PlayerHand[ix].Suit==SUIT_DIAMONDS ? 1 : 0); + } + } + + gui->StateChanged(gameLocal.time, true); + CallNamedEvent("Update"); + } +} + +void hhBlackJack::FollowDealerRules() { + // Flip up cards + for (int i=0; iReadToken(&token)) { + return false; + } + + if (token == ";") { + return false; + } + + if (token.Icmp("deal") == 0) { + Deal(); + } + else if (token.Icmp("hit") == 0) { + Hit(); + } + else if (token.Icmp("stay") == 0) { + Stay(); + } + else if (token.Icmp("double") == 0) { + Double(); + } + else if (token.Icmp("incbet") == 0) { + IncBet(); + } + else if (token.Icmp("decbet") == 0) { + DecBet(); + } + else if (token.Icmp("reset") == 0) { + Reset(); + } + else if (token.Icmp("restart") == 0) { + PlayerCredits = spawnArgs.GetInt("credits"); + Bet = PlayerBet = 1; + EndGame(); + UpdateView(); + } + else { + src->UnreadToken(&token); + return false; + } + + return true; +} + +void hhBlackJack::Event_Deal() { + Deal(); +} + +void hhBlackJack::Event_Hit() { + Hit(); +} + +void hhBlackJack::Event_Stay() { + Stay(); +} + +void hhBlackJack::Event_Double() { + Double(); +} diff --git a/src/Prey/game_blackjack.h b/src/Prey/game_blackjack.h new file mode 100644 index 0000000..7e36374 --- /dev/null +++ b/src/Prey/game_blackjack.h @@ -0,0 +1,70 @@ + +#ifndef __GAME_BLACKJACK_H__ +#define __GAME_BLACKJACK_H__ + +extern const idEventDef EV_Deal; + +typedef struct card_s { + int Suit; + int Value; + char Name; + unsigned char Visible; +}card_t; + +class hhBlackJack : public hhConsole { +public: + CLASS_PROTOTYPE( hhBlackJack ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void Deal(); + void Hit(); + void Stay(); + void Double(); + void IncBet(); + void DecBet(); + + void Reset(); + card_t GetCard(bool visible); + void RetallyScores(); + void AssessScores(); + void EndGame(); + void UpdateView(); + void FollowDealerRules(); + void DeterminePlayCommands(); + void UpdateBetMechanics(); + bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + +protected: + void Event_Deal(); + void Event_Hit(); + void Event_Stay(); + void Event_Double(); + void Event_UpdateView(); + +private: + idList PlayerHand; + idList DealerHand; + + int Bet; + int PlayerBet; + int DealerScore, PlayerScore; + int DealerAces, PlayerAces; + int PlayerCredits; + int victoryAmount; + int resultIndex; + int creditsWon; + + bool bCanDeal; + bool bCanIncBet; + bool bCanDecBet; + bool bCanHit; + bool bCanStay; + bool bCanDouble; + bool bCanSplit; +}; + + +#endif /* __GAME_BLACKJACK_H__ */ diff --git a/src/Prey/game_cards.cpp b/src/Prey/game_cards.cpp new file mode 100644 index 0000000..3537ec2 --- /dev/null +++ b/src/Prey/game_cards.cpp @@ -0,0 +1,130 @@ +// game_cards.cpp +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +static char cardnames[] = { '2','3','4','5','6','7','8','9','T','J','Q','K','A' }; +static char suitnames[] = { 'D','H','S','C' }; + + +//============================================================== +// hhCard utility class +//============================================================== +hhCard::hhCard() { + suit = SUIT_SPADES; + value = CARD_ACE; +} + +hhCard::hhCard(int value, int suit) { + this->suit = suit; + this->value = value; +} + +int hhCard::operator==(const hhCard &other) const { + return value==other.Value() && suit==other.Suit(); +} + +int hhCard::Value() const { + return value; +} + +int hhCard::Suit() const { + return suit; +} + +char hhCard::ValueName() { + return cardnames[value]; +} + +char hhCard::SuitName() { + return suitnames[suit]; +} + + +//============================================================== +// hhDeck utility class +//============================================================== + +CLASS_DECLARATION(idClass, hhDeck) +END_CLASS + +hhDeck::hhDeck() { + Generate(); + Shuffle(); +} + +void hhDeck::Save(idSaveGame *savefile) const { + int i; + savefile->WriteInt( cards.Num() ); // hhStack + for (i=0; iWrite(&cards[i], sizeof(hhCard)); + } +} + +void hhDeck::Restore( idRestoreGame *savefile ) { + int i, num; + hhCard card; + + cards.Clear(); // hhStack + savefile->ReadInt( num ); + cards.SetNum( num ); + for (i=0; iRead(&card, sizeof(hhCard)); + cards[i] = card; + } +} + +void hhDeck::Generate() { + int value, suit; + cards.Clear(); + for (value=0; value cards; + }; + +#endif /* __GAME_CARDS_H__ */ diff --git a/src/Prey/game_cilia.cpp b/src/Prey/game_cilia.cpp new file mode 100644 index 0000000..5137437 --- /dev/null +++ b/src/Prey/game_cilia.cpp @@ -0,0 +1,300 @@ +//************************************************************************** +//** +//** GAME_CILIA.CPP +//** +//** Specific type of SpherePart. When shot, they retract for a while, +//** and trigger nearby cilia to retract as well. +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS ------------------------------------------------------------------ + +// TYPES ------------------------------------------------------------------- + +// CLASS DECLARATIONS ------------------------------------------------------ + +const idEventDef EV_StickOut( "stickout", NULL ); +const idEventDef EV_TriggerNearby( "triggernearby", NULL ); +const idEventDef EV_Idle( "idle", NULL ); + +CLASS_DECLARATION( hhSpherePart, hhSphereCilia ) + EVENT( EV_Activate, hhSphereCilia::Event_Trigger ) + EVENT( EV_TriggerNearby, hhSphereCilia::Event_TriggerNearby ) + EVENT( EV_StickOut, hhSphereCilia::Event_StickOut ) + EVENT( EV_Idle, hhSphereCilia::Event_Idle ) // JRM + EVENT( EV_Touch, hhSphereCilia::Event_Touch ) +END_CLASS + +// STATE DECLARATIONS ------------------------------------------------------- + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// hhSphereCilia::Spawn +// +//========================================================================== + +void hhSphereCilia::Spawn(void) { + fl.takedamage = true; // Allow the spherepart to be damaged + fl.allowSpiritWalkTouch = true; + bRetracted = false; // Start sticking out + bAlreadyActivated = false; // Hasn't been touched yet + + //HUMANHEAD jsh PCF 4/26/06 allow creatures to trigger cilia + GetPhysics()->SetContents( CONTENTS_TRIGGER | CONTENTS_RENDERMODEL ); + + idleAnim = GetAnimator()->GetAnim("idle"); + pullInAnim = GetAnimator()->GetAnim("pullin"); + stickOutAnim = GetAnimator()->GetAnim("stickout"); + + spawnArgs.GetFloat( "nearbySize", "128", nearbySize ); + spawnArgs.GetFloat( "idleDelay", "-1", idleDelay ); + + if( idleDelay < 0 ) { // negative number denotes a random start delay + idleDelay = gameLocal.random.RandomFloat(); + } + + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleAnim, gameLocal.time + idleDelay * 1000, 100 ); +} + +void hhSphereCilia::Save(idSaveGame *savefile) const { + + savefile->WriteInt( pullInAnim ); + savefile->WriteInt( stickOutAnim ); + savefile->WriteFloat( nearbySize ); + savefile->WriteFloat( idleDelay ); + savefile->WriteBool( bRetracted ); + savefile->WriteBool( bAlreadyActivated ); +} + +void hhSphereCilia::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( pullInAnim ); + savefile->ReadInt( stickOutAnim ); + savefile->ReadFloat( nearbySize ); + savefile->ReadFloat( idleDelay ); + savefile->ReadBool( bRetracted ); + savefile->ReadBool( bAlreadyActivated ); +} + +//========================================================================== +// +// hhSphereCilia::Damage +// +// Currently similar to +//========================================================================== + +void hhSphereCilia::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, + const char *damageDefName, const float damageScale, const int location ) { + + hhFxInfo fxInfo; + + if( bRetracted ) { // Don't trigger if already retracted + return; + } + + bRetracted = true; + + // Remove collision on the model + GetPhysics()->SetContents(0); + + // Switch to the broken model + SetModel( spawnArgs.GetString( "model_broken", "" ) ); + + // Play an explode sound + StartSound( "snd_explode", SND_CHANNEL_ANY, 0, true, NULL ); + + fl.takedamage = false; + fl.applyDamageEffects = false; // Cilia don't accept damage wounds, since they swap out models + + PostEventSec( &EV_TriggerNearby, 0.2f + gameLocal.random.RandomFloat() * 0.1 ); + + ApplyEffect(); +} + +//========================================================================== +// +// hhSphereCilia::Event_Touch +// +//========================================================================== +void hhSphereCilia::Event_Touch( idEntity *other, trace_t *trace ) { + Trigger( other ); +} + +//========================================================================== +// +// hhSphereCilia::Trigger +// +//========================================================================== + +void hhSphereCilia::Trigger( idEntity *activator ) { + if( bRetracted || !activator ) { // Don't trigger if already retracted + return; + } + + bRetracted = true; + + // Remove collision on the model + GetPhysics()->SetContents(0); + + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, pullInAnim, gameLocal.time, 100); + + StartSound( "snd_in", SND_CHANNEL_ANY, 0, true, NULL ); + + PostEventSec( &EV_TriggerNearby, 0.2f + gameLocal.random.RandomFloat() * 0.1 ); + PostEventSec( &EV_StickOut, 5.0f + idleDelay ); + + // Trigger this cilia's targets the first time it is touched + if ( !bAlreadyActivated ) { + ActivateTargets( activator ); + bAlreadyActivated = true; + } + + ApplyEffect(); +} + +//========================================================================== +// +// hhSphereCilia::Event_Trigger +// +//========================================================================== + +void hhSphereCilia::Event_Trigger( idEntity *activator ) { + Trigger( activator ); +} + +//========================================================================== +// +// hhSphereCilia::Event_TriggerNearby +// +// Trigger nearby cilia (to create a cascading retract effect) +//========================================================================== + +void hhSphereCilia::Event_TriggerNearby( void ) { + int i; + int e; + hhSphereCilia *ent; + idEntity *entityList[ MAX_GENTITIES ]; + int numListedEntities; + idBounds bounds; + idVec3 org; + + org = GetPhysics()->GetOrigin(); + for ( i = 0 ; i < 3 ; i++ ) { + bounds[0][i] = org[i] - nearbySize; + bounds[1][i] = org[i] + nearbySize; + } + + // Find the first closest neighbor cilia to trigger + numListedEntities = gameLocal.clip.EntitiesTouchingBounds( bounds, -1, entityList, MAX_GENTITIES ); + + for ( e = 0 ; e < numListedEntities ; e++ ) { + if( !entityList[e]->IsType( hhSphereCilia::Type ) ) { + continue; + } + + ent = static_cast< hhSphereCilia * >( entityList[e] ); + + if( ent->bRetracted ) { + continue; + } + + ent->Trigger( this ); + } +} + +//========================================================================== +// +// hhSphereCilia::Event_StickOut +// +// Return the cilia to its original state +//========================================================================== + +#define MAX_NEARBY_CILIA 8 + +void hhSphereCilia::Event_StickOut( void ) { + int i; + int num; + idEntity *touch[MAX_NEARBY_CILIA]; + idEntity *hit; + bool canFit; + + // Check if the cilia can fit when sticking out + num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), -1, touch, MAX_NEARBY_CILIA ); + + canFit = true; + for( i = 0; i < num; i++ ) { + hit = touch[ i ]; + assert( hit ); + + if(( hit == this )) { + continue; + } + + // Hit an entity, so reset the cilia to attempt to emerge in a bit + PostEventSec( &EV_StickOut, 5.0f ); + return; + } + + //HUMANHEAD jsh PCF 4/26/06 allow creatures to trigger cilia + // Restore the proper collision + GetPhysics()->SetContents( CONTENTS_TRIGGER | CONTENTS_RENDERMODEL ); + + // Play the stick out anims and blend the idle back in + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, stickOutAnim, gameLocal.time, 100); + PostEventMS(&EV_Idle, 100); // JRM + + StartSound( "snd_out", SND_CHANNEL_ANY, 0, true, NULL); + + bRetracted = false; +} + +// +// Event_Idle +// +// JRM +void hhSphereCilia::Event_Idle(void) { + + assert(idleAnim); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleAnim, gameLocal.time, 200); +}; + +void hhSphereCilia::ApplyEffect( void ) { + hhFxInfo fxInfo; + fxInfo.SetNormal( GetAxis()[0] ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_explode", GetOrigin() + (GetAxis()[0] * 16.0f), GetAxis(), &fxInfo ); + + int num; + int i; + idEntity* ents[MAX_GENTITIES]; + + num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), MASK_SHOT_BOUNDINGBOX|CONTENTS_RENDERMODEL, ents, MAX_GENTITIES ); + + const char *damageType = spawnArgs.GetString("damageType", "damage_cilia"); + for( i = 0; i < num; i++ ) { + if( ents[ i ] != this && !ents[ i ]->IsType(hhSphereCilia::Type) && !ents[ i ]->IsType(idAFAttachment::Type) ) { + ents[ i ]->Damage( this, this, vec3_origin, damageType, 1.0f, INVALID_JOINT ); + } + } + +} + diff --git a/src/Prey/game_cilia.h b/src/Prey/game_cilia.h new file mode 100644 index 0000000..1afadf2 --- /dev/null +++ b/src/Prey/game_cilia.h @@ -0,0 +1,36 @@ + +#ifndef __GAME_CILIA_H +#define __GAME_CILIA_H + +extern const idEventDef EV_TriggerNearby; +extern const idEventDef EV_StickOut; + +class hhSphereCilia : public hhSpherePart { +public: + CLASS_PROTOTYPE( hhSphereCilia ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Trigger( idEntity *activator ); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + +protected: + void Event_Trigger( idEntity *activator ); + void Event_TriggerNearby( void ); + void Event_StickOut( void ); + void Event_Idle( void ); // JRM + void Event_Touch( idEntity *other, trace_t *trace ); + + void ApplyEffect( void ); + +protected: + int pullInAnim; + int stickOutAnim; + float nearbySize; + float idleDelay; + bool bRetracted; + bool bAlreadyActivated; +}; + +#endif /* __GAME_CILIA_H */ diff --git a/src/Prey/game_console.cpp b/src/Prey/game_console.cpp new file mode 100644 index 0000000..44e7cec --- /dev/null +++ b/src/Prey/game_console.cpp @@ -0,0 +1,822 @@ +// Game_console.cpp +// + +// Container class for all world objects that have GUIs on them. (except monsters & weapons) +// Provides some keys to these guis not provided by default GUI code like "rand" + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define TRANSLATION_TIME 1000 // Time over which translation takes place + +const idEventDef EV_CallGuiEvent("guiEvent", "s"); + +//========================================================================== +// +// hhConsole +// +//========================================================================== + +CLASS_DECLARATION(idStaticEntity, hhConsole) + EVENT( EV_Activate, hhConsole::Event_Activate ) + EVENT( EV_TalonAction, hhConsole::Event_TalonAction ) + EVENT( EV_CallGuiEvent, hhConsole::Event_CallGuiEvent ) + EVENT( EV_BecomeNonSolid, hhConsole::Event_BecomeNonSolid ) + EVENT( EV_PostSpawn, hhConsole::Event_PostSpawn ) +END_CLASS + +void hhConsole::Spawn() { +// BecomeActive(TH_MISC2); // For ai + + bTimeEventsAlways = spawnArgs.GetBool("timeEventsAlways"); + bUsesRand = spawnArgs.GetBool("usesRand"); + if (bUsesRand || bTimeEventsAlways) { + BecomeActive(TH_THINK); + } + + bool bDamageable = !spawnArgs.GetBool("noDamage"); + if (bDamageable) { + fl.takedamage = true; + } + + // Stuff color into gui_parms 20,21,22 + idVec3 color; + GetColor(color); + SetOnAllGuis("gui_parm20", color[0]); + SetOnAllGuis("gui_parm21", color[1]); + SetOnAllGuis("gui_parm22", color[2]); + + // Translation variables + translationAlpha.Init(gameLocal.time, 0, 0.0f, 0.0f); // Start at 0 (untranslated) + transState = TS_UNTRANSLATED; + + // Start idle sound + StartSound("snd_idle", SND_CHANNEL_ANY, 0, true, NULL); + + UpdateVisuals(); + + // Init AI data + aiCanUse = spawnArgs.GetBool("ai_can_use"); + aiUseCount = 0; + aiReuseWaitTime = (int)(spawnArgs.GetFloat("ai_reuse_wait")*1000.0f); + aiUseTime = (int)(spawnArgs.GetFloat("ai_use_time")*1000.0f); + aiTriggerWaitTime = (int)(spawnArgs.GetFloat("ai_use_trigger_wait")*1000.0f); + aiMaxUses = spawnArgs.GetInt("ai_max_uses", "-1"); + aiRetriggerWait = (int)(spawnArgs.GetFloat("ai_use_retrigger_wait")*1000.0f); + + // Clamp the trigger wait to be less than the total use time + if(aiTriggerWaitTime > aiUseTime) { + aiTriggerWaitTime = aiUseTime - 32; + if(aiTriggerWaitTime < 0) { + aiTriggerWaitTime = 0; + } + } + + aiCurrUsedStartTime = gameLocal.GetTime(); + aiCurrUsedBy = NULL; + aiLastUsedBy = NULL; + aiLastUsedTime = -aiReuseWaitTime; + aiWaitingToTrigger = true; + aiLastTriggerTime = gameLocal.GetTime() - 1000; + + perchSpot = NULL; + PostEventMS( &EV_PostSpawn, 0 ); +} + +void hhConsole::Event_PostSpawn() { + // Automatically cause all consoles to spawn Talon perch spots + if(!spawnArgs.GetBool("noTalonTarget")) { + idDict args; + idVec3 offset; + bool bGuiInteractive = false; + + // cjr - Iterate through guis and determine if any are interactive. If all are not, then Talon should not squawk + for ( int ix = 0; ix < MAX_RENDERENTITY_GUI; ix++ ) { + if ( renderEntity.gui[ix] && renderEntity.gui[ix]->IsInteractive() ) { + bGuiInteractive = true; + } + } + + const char *perchDef; + if ( spawnArgs.GetBool( "noTalonSquawk" ) || !bGuiInteractive) { // CJR: Check if talon should squawk at this spot + perchDef = spawnArgs.GetString("def_perch"); + } else { + perchDef = spawnArgs.GetString("def_perchSquawk"); // Talon should squawk at this spot + } + + if (!gameLocal.isClient) { + if (perchDef && perchDef[0]) { + // Set the offset for the perch location and priority + offset = GetPhysics()->GetAxis() * spawnArgs.GetVector("offset_perch", "0 0 24"); + float offsetYaw = spawnArgs.GetFloat("offset_perchyaw"); + args.SetVector("origin", GetPhysics()->GetOrigin() + offset); + + idAngles angles = GetAxis().ToAngles(); + angles.yaw += offsetYaw; + args.SetMatrix("rotation", angles.ToMat3()); + args.Set("target", this->name.c_str()); + perchSpot = (hhTalonTarget *)gameLocal.SpawnObject( perchDef, &args ); // Consoles are automatically high priority spots + if (perchSpot && ( spawnArgs.GetBool("bindPerchSpot") || IsBound() ) ) { + perchSpot->Bind(this, true); + } + } + else { + gameLocal.Warning("Need def_perch key on %s", name.c_str()); + perchSpot = NULL; + } + } + } +} + +void hhConsole::Save(idSaveGame *savefile) const { + savefile->WriteFloat( translationAlpha.GetStartTime() ); // idInterpolate + savefile->WriteFloat( translationAlpha.GetDuration() ); + savefile->WriteFloat( translationAlpha.GetStartValue() ); + savefile->WriteFloat( translationAlpha.GetEndValue() ); + + savefile->Write( &transState, sizeof(transState) ); + savefile->WriteBool( bTimeEventsAlways ); + savefile->WriteBool( bUsesRand ); + savefile->WriteObject( perchSpot ); + savefile->WriteBool( aiCanUse ); + savefile->WriteInt( aiUseCount ); + savefile->WriteInt( aiMaxUses ); + savefile->WriteInt( aiReuseWaitTime ); + savefile->WriteInt( aiUseTime ); + savefile->WriteInt( aiTriggerWaitTime ); + savefile->WriteBool( aiWaitingToTrigger ); + savefile->WriteInt( aiLastTriggerTime ); + savefile->WriteInt( aiRetriggerWait ); + aiCurrUsedBy.Save(savefile); + savefile->WriteInt( aiCurrUsedStartTime ); + aiLastUsedBy.Save(savefile); + savefile->WriteInt( aiLastUsedTime ); +} + +void hhConsole::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadFloat( set ); // idInterpolate + translationAlpha.SetStartTime( set ); + savefile->ReadFloat( set ); + translationAlpha.SetDuration( set ); + savefile->ReadFloat( set ); + translationAlpha.SetStartValue(set); + savefile->ReadFloat( set ); + translationAlpha.SetEndValue( set ); + + savefile->Read( &transState, sizeof(transState) ); + savefile->ReadBool( bTimeEventsAlways ); + savefile->ReadBool( bUsesRand ); + savefile->ReadObject( reinterpret_cast(perchSpot) ); + savefile->ReadBool( aiCanUse ); + savefile->ReadInt( aiUseCount ); + savefile->ReadInt( aiMaxUses ); + savefile->ReadInt( aiReuseWaitTime ); + savefile->ReadInt( aiUseTime ); + savefile->ReadInt( aiTriggerWaitTime ); + savefile->ReadBool( aiWaitingToTrigger ); + savefile->ReadInt( aiLastTriggerTime ); + savefile->ReadInt( aiRetriggerWait ); + + aiCurrUsedBy.Restore(savefile); + savefile->ReadInt( aiCurrUsedStartTime ); + aiLastUsedBy.Restore(savefile); + savefile->ReadInt( aiLastUsedTime ); +} + +void hhConsole::Event_CallGuiEvent(const char *eventName) { + CallNamedEvent(eventName); +} + +void hhConsole::Event_BecomeNonSolid( void ) { + GetPhysics()->GetClipModel()->SetContents( 0 ); + GetPhysics()->SetClipMask( 0 ); +} + +void hhConsole::SetOnAllGuis(const char *key, float value) { + for (int ix=0; ixSetStateFloat(key, value); + renderEntity.gui[ix]->StateChanged(gameLocal.time); + } + } +} + +void hhConsole::SetOnAllGuis(const char *key, int value) { + for (int ix=0; ixSetStateInt(key, value); + renderEntity.gui[ix]->StateChanged(gameLocal.time); + } + } +} + +void hhConsole::SetOnAllGuis(const char *key, bool value) { + for (int ix=0; ixSetStateBool(key, value); + renderEntity.gui[ix]->StateChanged(gameLocal.time); + } + } +} + +void hhConsole::SetOnAllGuis(const char *key, const char *value) { + for (int ix=0; ixSetStateString(key, value); + renderEntity.gui[ix]->StateChanged(gameLocal.time); + } + } +} + +void hhConsole::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) { + + fl.takedamage = false; + + // JRM - disable msgs on kill? + if(spawnArgs.GetBool("ai_damage_disable_msgs","0")) { + fl.refreshReactions = false; + } + + // Inform the gui that we are damaged (not used, since gui materials can't see entity parms) +// SetShaderParm(SHADERPARM_DIVERSITY, gameLocal.random.RandomFloat()); + SetOnAllGuis("damaged", true); + + // Tell the gui that we died, so the gui can be in sync + CallNamedEvent("Death"); +} + +bool hhConsole::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + idToken token; + if (!src->ReadToken(&token) || token == ";") { + return false; + } + if (token.Icmp("setfont") == 0) { + if (src->ReadToken(&token)) { + idStr fontname = "fonts\\"; + fontname += token; + if (renderEntity.gui[0]) { + renderEntity.gui[0]->Translate(fontname.c_str()); + } + } + return true; + } + + src->UnreadToken( &token ); + return false; +} + +// This is overridden so we can update camera targets reguardless of PVS +void hhConsole::Present( void ) { + PROFILE_SCOPE("Present", PROFMASK_NORMAL); + + if ( !gameLocal.isNewFrame ) { + return; + } + + // don't present to the renderer if the entity hasn't changed + if ( !( thinkFlags & TH_UPDATEVISUALS ) ) { + return; + } + BecomeInactive( TH_UPDATEVISUALS ); + + // camera target for remote render views + if ( cameraTarget ) { // && gameLocal.InPlayerPVS( this ) ) { // HUMANHEAD pdm: removed PVS check + renderEntity.remoteRenderView = cameraTarget->GetRenderView(); + } + + // if set to invisible, skip + if ( !renderEntity.hModel || IsHidden() ) { + return; + } + + // add to refresh list + if ( modelDefHandle == -1 ) { + modelDefHandle = gameRenderWorld->AddEntityDef( &renderEntity ); + } else { + gameRenderWorld->UpdateEntityDef( modelDefHandle, &renderEntity ); + } +} + +void hhConsole::Think() { + + if (thinkFlags & TH_THINK) { + if (bUsesRand) { + float rand = gameLocal.random.RandomFloat(); + SetOnAllGuis("rand", rand); + } + + if (bTimeEventsAlways) { + // Call event handler so time events get run + const char *cmd; + sysEvent_t ev; + memset( &ev, 0, sizeof( ev ) ); + ev.evType = SE_NONE; + + // This should work after the merge is done, since in the new code, HandleEvent() calls RunTimeEvents() + for (int ix=0; ixHandleEvent(&ev, gameLocal.time); + HandleGuiCommands(this, cmd); + } + } + } + } + + if (thinkFlags & TH_MISC1) { + // Handle any translation + if (transState == TS_TRANSLATING || transState == TS_UNTRANSLATING) { + float alpha = translationAlpha.GetCurrentValue(gameLocal.time); + if (renderEntity.gui[0] || renderEntity.gui[1] || renderEntity.gui[2]) { + + SetOnAllGuis("translationAlpha", alpha); + + // Determine if the transition is done + if (translationAlpha.IsDone(gameLocal.time)) { + if (transState == TS_TRANSLATING) { + transState = TS_TRANSLATED; + BecomeInactive(TH_MISC1); + } + else if (transState == TS_UNTRANSLATING) { + transState = TS_UNTRANSLATED; + BecomeInactive(TH_MISC1); + } + } + } + } + } + + // JRM - ai using update + if (thinkFlags & TH_MISC2) { + UpdateUse(); + } + + idStaticEntity::Think(); +} + +// Toggles the translation state of the gui +void hhConsole::Translate(bool bLanded) { + + if (!spawnArgs.GetBool("translate")) { + return; + } + + float curalpha = translationAlpha.GetCurrentValue(gameLocal.time); + switch(transState) { + case TS_UNTRANSLATED: + case TS_UNTRANSLATING: + translationAlpha.Init(gameLocal.time, TRANSLATION_TIME, curalpha, 1.0f); + transState = TS_TRANSLATING; + BecomeActive(TH_MISC1); + break; + case TS_TRANSLATED: + case TS_TRANSLATING: + translationAlpha.Init(gameLocal.time, TRANSLATION_TIME, curalpha, 0.0f); + transState = TS_UNTRANSLATING; + BecomeActive(TH_MISC1); + break; + } + + if (bLanded) { + StartSound( "snd_translate", SND_CHANNEL_ANY, 0, true, NULL); + } +} + +void hhConsole::ConsoleActivated() { + // Called when someone "uses" the gui (gui calls activate cmd) + // JRM - turn off msgs after the player uses this + if(!aiCurrUsedBy.IsValid() && spawnArgs.GetBool("ai_player_use_disable_msgs", "0")) { + fl.refreshReactions = false; + } +} + +void hhConsole::ClearTalonTargetType() { // CJR - Called when any command is sent to a gui. This disables talon's squawking + if ( perchSpot ) { + perchSpot->SetSquawk( false ); // Set the console to stop Talon from squawking + } +} + +void hhConsole::Event_Activate( idEntity *activator ) { + // Don't toggle on/off like in idStaticEntity + // any GUIs still get the onTrigger() event +} + +void hhConsole::Event_TalonAction(idEntity *talon, bool landed) { + Translate(landed); +} + +void hhConsole::Use(idAI *ai) { + HH_ASSERT(ai); + + // Just started using console? + if(!aiCurrUsedBy.IsValid()) { + aiCurrUsedBy = ai; + aiCurrUsedStartTime = gameLocal.GetTime(); + BecomeActive(TH_MISC2); + } +} + +void hhConsole::UpdateUse(void) { + + // Time to deactivate? + if(aiCurrUsedBy.IsValid() && gameLocal.GetTime() > aiCurrUsedStartTime + aiUseTime) { + aiLastUsedBy = aiCurrUsedBy; + aiLastUsedTime = gameLocal.GetTime(); + aiCurrUsedBy = NULL; + aiWaitingToTrigger = true; + aiUseCount++; + if(aiMaxUses > 0 && aiUseCount >= aiMaxUses) { + fl.refreshReactions = false; + } + BecomeInactive(TH_MISC2); + } + + // Time to fire triggers? + if (aiCurrUsedBy.IsValid()) { + + // Waiting to trigger... + if(aiWaitingToTrigger && gameLocal.GetTime() > aiCurrUsedStartTime + aiTriggerWaitTime) { + aiWaitingToTrigger = false; + + OnTriggeredByAI(aiCurrUsedBy.GetEntity()); + } + // Time to re-trigger? + else if (!aiWaitingToTrigger && aiRetriggerWait > 0 && gameLocal.GetTime() - aiLastTriggerTime > aiRetriggerWait) { + OnTriggeredByAI(aiCurrUsedBy.GetEntity()); + } + } +} + +void hhConsole::OnTriggeredByAI(idAI *ai) { + + aiLastTriggerTime = gameLocal.GetTime(); + + // Tell the gui that we were used, so the gui can be in sync + CallNamedEvent("AIUse"); +} + +bool hhConsole::CanUse(idAI *ai) { + HH_ASSERT(ai); + + if (!aiCanUse) { + return false; + } + + if (aiMaxUses > 0 && aiUseCount >= aiMaxUses) { + return false; + } + + // Can't use it if someone else is using it + if (aiCurrUsedBy.IsValid() && aiCurrUsedBy.GetEntity() != ai) { + return false; + } + + // Have to wait before we can use it again? + if (gameLocal.GetTime() < aiLastUsedTime + aiReuseWaitTime) { + return false; + } + + return ai->spawnArgs.GetBool("CanUseConsole", "0"); +} + +//========================================================================== +// +// hhConsoleCountdown +// +//========================================================================== + +const idEventDef EV_CountdownStart("startcountdown", NULL); +const idEventDef EV_CountdownStop("stopcountdown", NULL); +const idEventDef EV_CountdownSet("setcountdown", "d"); + +CLASS_DECLARATION(hhConsole, hhConsoleCountdown) + EVENT( EV_Activate, hhConsoleCountdown::Event_Activate ) + EVENT( EV_CountdownStart, hhConsoleCountdown::Event_StartCountdown ) + EVENT( EV_CountdownStop, hhConsoleCountdown::Event_StopCountdown ) + EVENT( EV_CountdownSet, hhConsoleCountdown::Event_SetCountdown ) +END_CLASS + +void hhConsoleCountdown::Spawn() { + countingDown = false; + countStart = spawnArgs.GetFloat("countStart"); + countEnd = spawnArgs.GetFloat("countEnd"); + countdown.Init(gameLocal.time, 0, countStart, countStart); + SetGuiOctal((int)countStart); + + if (spawnArgs.GetBool("enabled")) { + StartCountdown(); + } +} + +void hhConsoleCountdown::Save(idSaveGame *savefile) const { + savefile->WriteBool(countingDown); + savefile->WriteFloat(countStart); + savefile->WriteFloat(countEnd); + + savefile->WriteFloat( countdown.GetStartTime() ); // idInterpolate + savefile->WriteFloat( countdown.GetDuration() ); + savefile->WriteFloat( countdown.GetStartValue() ); + savefile->WriteFloat( countdown.GetEndValue() ); +} + +void hhConsoleCountdown::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadBool(countingDown); + savefile->ReadFloat(countStart); + savefile->ReadFloat(countEnd); + + savefile->ReadFloat( set ); // idInterpolate + countdown.SetStartTime( set ); + savefile->ReadFloat( set ); + countdown.SetDuration( set ); + savefile->ReadFloat( set ); + countdown.SetStartValue(set); + savefile->ReadFloat( set ); + countdown.SetEndValue( set ); +} + +void hhConsoleCountdown::SetGuiOctal(int value) { + // Convert to base-8 numbers and set on gui + idStr octalValue; + sprintf(octalValue, "%o", value); + SetOnAllGuis("countdown", octalValue.c_str()); +} + +void hhConsoleCountdown::UpdateGUI(float curValue) { + SetGuiOctal((int)curValue); + SetOnAllGuis("fraction", curValue / countStart); + SetOnAllGuis("counting", countingDown); +} + +void hhConsoleCountdown::Think() { + hhConsole::Think(); + + if (thinkFlags & TH_MISC3) { + if (countingDown) { + UpdateGUI(countdown.GetCurrentValue(gameLocal.time)); + if (countdown.IsDone(gameLocal.time)) { + BecomeInactive(TH_MISC3); + ActivateTargets(this); + StopCountdown(); + } + } + } +} + +void hhConsoleCountdown::SetCountdown(int count) { + if (countingDown) { + gameLocal.Warning("You must stop countdown before set"); + return; + } + countStart = count; + countdown.Init(gameLocal.time, 0, countStart, countStart); + UpdateGUI(count); +} + +void hhConsoleCountdown::StartCountdown() { + if (!countingDown) { + // Start counting down from current value + float curValue = countdown.GetCurrentValue(gameLocal.time); + // resume from previously stopped count + countdown.Init(gameLocal.time, SEC2MS(countStart), curValue, countEnd); + BecomeActive(TH_MISC3); + countingDown = true; + } +} + +void hhConsoleCountdown::StopCountdown() { + if (countingDown) { + // Stop countdown at current value + float curValue = countdown.GetCurrentValue(gameLocal.time); + countdown.Init(gameLocal.time, 0, curValue, curValue); + countingDown = false; + UpdateGUI(countdown.GetCurrentValue(gameLocal.time)); + } +} + +void hhConsoleCountdown::Reset() { + // Reset to initial start time (or set start time) + countdown.Init(gameLocal.time, 0, countStart, countStart); + UpdateGUI(countdown.GetCurrentValue(gameLocal.time)); +} + +void hhConsoleCountdown::Event_Activate(idEntity *activator) { + StartCountdown(); +} +void hhConsoleCountdown::Event_SetCountdown(int count) { + SetCountdown(count); +} +void hhConsoleCountdown::Event_StartCountdown() { + StartCountdown(); +} +void hhConsoleCountdown::Event_StopCountdown() { + StopCountdown(); +} + + +//========================================================================== +// +// hhConsoleKeypad +// +// Generic console for entering text on a keypad and displaying it +//========================================================================== + +CLASS_DECLARATION(hhConsole, hhConsoleKeypad) +END_CLASS + +void hhConsoleKeypad::Spawn() { + maxLength = spawnArgs.GetInt("maxlength"); + + SetOnAllGuis("atmax", false); +} + +void hhConsoleKeypad::Save(idSaveGame *savefile) const { + savefile->WriteString(keyBuffer); + savefile->WriteInt(maxLength); +} + +void hhConsoleKeypad::Restore( idRestoreGame *savefile ) { + savefile->ReadString(keyBuffer); + savefile->ReadInt(maxLength); +} + +void hhConsoleKeypad::UpdateGui() { + int length = keyBuffer.Length(); + + // Enforce maxlength + if (length > maxLength) { + keyBuffer = keyBuffer.Left(maxLength); + } + + SetOnAllGuis("atmax", (length >= maxLength)); + SetOnAllGuis("keypad", keyBuffer.c_str()); +} + +bool hhConsoleKeypad::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + idToken token; + if (!src->ReadToken(&token) || token == ";") { + return false; + } + if (token.Icmp("backspace") == 0) { + if (keyBuffer.Length() > 0) { + keyBuffer = keyBuffer.Left(keyBuffer.Length()-1); + } + } + else if (token.Icmp("clear") == 0) { + keyBuffer.Empty(); + } + else if (token.Icmp("enter") == 0) { + keyBuffer += '\n'; + } + else if (token.IcmpPrefix("keypad_") == 0) { // KEYPAD_X + token.ToLower(); + token.Strip("keypad_"); + keyBuffer += token.c_str(); + } + else { + return false; + } + + UpdateGui(); + return true; +} + + +//========================================================================== +// +// hhConsoleAlarm +// +// alarm console +//========================================================================== + +const idEventDef EV_SpawnMonster(""); + +CLASS_DECLARATION(hhConsole, hhConsoleAlarm) + EVENT( EV_SpawnMonster, hhConsoleAlarm::Event_SpawnMonster ) +END_CLASS + +void hhConsoleAlarm::Spawn() { + bAlarmActive = false; + numMonsters = 0; + bSpawning = false; + maxMonsters = spawnArgs.GetInt( "max_monsters", "0" ); + BecomeActive(TH_TICKER); +} + +void hhConsoleAlarm::Save(idSaveGame *savefile) const { + savefile->WriteBool(bAlarmActive); + savefile->WriteInt( numMonsters ); + savefile->WriteInt( maxMonsters ); + currentMonster.Save( savefile ); + savefile->WriteBool( bSpawning ); +} + +void hhConsoleAlarm::Restore( idRestoreGame *savefile ) { + savefile->ReadBool(bAlarmActive); + savefile->ReadInt( numMonsters ); + savefile->ReadInt( maxMonsters ); + currentMonster.Restore( savefile ); + savefile->ReadBool( bSpawning ); +} + +void hhConsoleAlarm::ConsoleActivated() { + bAlarmActive = !bAlarmActive; + if ( !bAlarmActive ) { + CancelEvents( &EV_SpawnMonster ); + for ( int i=0;iIsType( hhMountedGun::Type ) ) { + targets[i]->PostEventMS( &EV_Deactivate, 0 ); + } + targets[i]->DeactivateTargetsType( hhMountedGun::Type ); + } + } + } +} + +void hhConsoleAlarm::Event_SpawnMonster() { + idDict args; + // Copy keys for monster + idStr tmpStr, realKeyName; + const idKeyValue *kv = spawnArgs.MatchPrefix("ent_", NULL); + while(kv) { + tmpStr = kv->GetKey(); + int usIndex = tmpStr.FindChar("ent_", '_'); + realKeyName = tmpStr.Mid(usIndex+1, strlen(kv->GetKey())-usIndex-1); + args.Set(realKeyName, kv->GetValue()); + kv = spawnArgs.MatchPrefix("ent_", kv); + } + idEntity *ent = gameLocal.FindEntity( spawnArgs.GetString( "spawn_location" ) ); + if ( ent ) { + args.SetVector( "origin", ent->GetOrigin() ); + } else { + gameLocal.Warning("No spawn_location specified for %s\n", GetName()); + return; + } + + // entity collision checks for seeing if we are going to collide with another entity on spawn + const idDict *entDef = gameLocal.FindEntityDefDict( spawnArgs.GetString( "def_monster" ), false ); + if ( entDef ) { + bool clipped = false; + idVec3 entSize; + idClipModel *cm; + idClipModel *clipModels[ MAX_GENTITIES ]; + entDef->GetVector( "size", "0", entSize ); + idBounds bounds = idBounds( ent->GetOrigin() ).Expand( max( entSize.x, entSize.y ) ); + int num = gameLocal.clip.ClipModelsTouchingBounds( bounds, MASK_MONSTERSOLID, clipModels, MAX_GENTITIES ); + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugBounds( colorRed, bounds, vec3_origin, 5000 ); + } + for ( int i=0;iIsRenderModel() ) { + continue; + } + idEntity *hit = cm->GetEntity(); + if ( ( hit == this ) || !hit->fl.takedamage || !hit->IsType( idActor::Type ) ) { + continue; + } + clipped = true; + break; + } + if ( !clipped ) { + currentMonster = gameLocal.SpawnObject(spawnArgs.GetString( "def_monster" ), &args); + if ( currentMonster.IsValid() ) { + currentMonster->PostEventMS( &EV_Activate, 0, this ); + bSpawning = false; + numMonsters++; + } else { + gameLocal.Warning( "%s could not spawn monster\n", GetName() ); + } + } + } +} + +void hhConsoleAlarm::Ticker() { + + if (bAlarmActive) { + if ( currentMonster.IsValid() && currentMonster->GetHealth() <= 0 ) { + currentMonster.Clear(); + } + + //check on our monster + if ( !bSpawning && !currentMonster.IsValid() && spawnArgs.GetString( "def_monster" )[0] ) { + if ( !maxMonsters || numMonsters < maxMonsters ) { + float min = spawnArgs.GetFloat( "mindelay", "2.0" ); + float max = spawnArgs.GetFloat( "maxdelay", "2.0" ); + if ( max < min ) { + max = min; + } + bSpawning = true; + PostEventSec( &EV_SpawnMonster, min + (max-min)*gameLocal.random.RandomFloat() ); + } + } + } + + hhConsole::Ticker(); +} \ No newline at end of file diff --git a/src/Prey/game_console.h b/src/Prey/game_console.h new file mode 100644 index 0000000..4ca9442 --- /dev/null +++ b/src/Prey/game_console.h @@ -0,0 +1,148 @@ + +#ifndef __GAME_CONSOLE_H__ +#define __GAME_CONSOLE_H__ + +extern const idEventDef EV_CallGuiEvent; + +enum ETranslationState { + TS_TRANSLATING, + TS_TRANSLATED, + TS_UNTRANSLATING, + TS_UNTRANSLATED +}; + +class hhTalonTarget; // CJR + +class hhConsole : public idStaticEntity { +public: + CLASS_PROTOTYPE( hhConsole ); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Think(); + virtual void ConsoleActivated(); + virtual void ClearTalonTargetType(); // CJR - Called when any command is sent to a gui. This disables talon's squawking + + virtual void Present( void ); + virtual bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + virtual void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location); + void Translate(bool bLanded); + void SetOnAllGuis(const char *key, float value); + void SetOnAllGuis(const char *key, int value); + void SetOnAllGuis(const char *key, bool value); + void SetOnAllGuis(const char *key, const char *value); + virtual void PlayerControls(usercmd_t *cmd) {} + + virtual void Use(idAI *ai); // JRM - An AI is starting to use this console + bool CanUse(idAI *ai); // JRM - Returns TRUE if the given AI is allowed to use this console right now + int GetLastUsedTime(void) {return aiLastUsedTime;} + idAI* GetLastUsedAI(void) {return aiLastUsedBy.GetEntity();} + virtual void OnTriggeredByAI(idAI *ai); // JRM - Called when the AI actually "presses" the console + void UpdateUse(void); // Called every tick + +protected: + void Event_Activate(idEntity *activator); + void Event_TalonAction(idEntity *talon, bool landed); + void Event_CallGuiEvent(const char *eventName); + void Event_BecomeNonSolid( void ); + void Event_PostSpawn(); + +protected: + idInterpolate translationAlpha; + ETranslationState transState; + bool bTimeEventsAlways; + bool bUsesRand; + + hhTalonTarget *perchSpot; // Associated Talon perch spot with this console + + // JRM - AI data + bool aiCanUse; // True if the AI is allowed to use this console + int aiUseCount; // Number of times AI has used this console + int aiMaxUses; // Max number of times the ai can use this console + int aiReuseWaitTime; // Time the AI has to wait before they can use this console again (ms) + int aiUseTime; // How long once AI starts using this console, do they stay here? (total ms) + int aiTriggerWaitTime; // How long once AI starts using this console until we fire this console's trigger? (ms) + bool aiWaitingToTrigger; // TRUE if the ai is waiting to fire the triggers for this console + int aiLastTriggerTime; // The time we last triggered this console + int aiRetriggerWait; // -1 = Never retrigger, otherwise time (ms) that we should retrigger while using + + idEntityPtr aiCurrUsedBy; // The AI is currently being using this console + int aiCurrUsedStartTime; // The time the current user STARTED using this console + + idEntityPtr aiLastUsedBy; // The AI that last used this console + int aiLastUsedTime; // The time that aiLastUsedBy used finished using this console + +}; + +class hhConsoleCountdown : public hhConsole { + CLASS_PROTOTYPE( hhConsoleCountdown ); + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think(); + + void Reset(); + void UpdateGUI(float curValue); + void SetGuiOctal(int value); + void SetCountdown(int count); + void StartCountdown(); + void StopCountdown(); + +protected: + void Event_Activate(idEntity *activator); + void Event_SetCountdown(int count); + void Event_StartCountdown(); + void Event_StopCountdown(); + +protected: + bool countingDown; // Whether we are running or not + float countStart; // Starting value of countdown + float countEnd; // Ending value of countdown + idInterpolate countdown; // Interpolator that always carries the current count +}; + + +class hhConsoleKeypad : public hhConsole { + CLASS_PROTOTYPE( hhConsoleKeypad ); + +public: + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + +protected: + void UpdateGui(); + +protected: + idStr keyBuffer; + int maxLength; +}; + + +class hhConsoleAlarm : public hhConsole { + CLASS_PROTOTYPE( hhConsoleAlarm ); + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void ConsoleActivated(); + virtual void Ticker(); +protected: + void Event_SpawnMonster(); + idEntityPtr currentMonster; + int maxMonsters; + int numMonsters; + bool bSpawning; +private: + bool bAlarmActive; +}; + + +#endif /* __GAME_CONSOLE_H__ */ diff --git a/src/Prey/game_damagetester.cpp b/src/Prey/game_damagetester.cpp new file mode 100644 index 0000000..38f2f9d --- /dev/null +++ b/src/Prey/game_damagetester.cpp @@ -0,0 +1,180 @@ +//************************************************************************** +//** +//** GAME_DAMAGETESTER.CPP +//** +//** +//** This object is used to calculate the damage done to it over time +//** When initially damaged, it calculates the amount of damage done for a +//** given amount of time +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" +#include "game_damagetester.h" + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +// MACROS ------------------------------------------------------------------ + +// TYPES ------------------------------------------------------------------- + +// CLASS DECLARATIONS ------------------------------------------------------ + +const idEventDef EV_ResetTarget( "", NULL ); +const idEventDef EV_CheckRemove( "", NULL ); + +CLASS_DECLARATION( hhAnimatedEntity, hhDamageTester ) + EVENT( EV_ResetTarget, hhDamageTester::Event_ResetTarget ) + EVENT( EV_CheckRemove, hhDamageTester::Event_CheckRemove ) +END_CLASS + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// hhDamageTester::Spawn +// +//========================================================================== + +void hhDamageTester::Spawn(void) { + fl.takedamage = true; + gameLocal.Printf("\"Name\", \"Damage (Melee)\", \"Damage (Close)\", \"Damage (Medium)\", \"Damage (Far)\", \ + \"Hits (Melee)\", \"Hits (Close)\", \"Hits (Medium)\", \"Hits (Far)\"\n"); + + testTime = spawnArgs.GetFloat( "testTime", "5" ); + if( testTime < 1 ) { + testTime = 1; + } + + // Get the distances and reset values + for( int i = 0; i < DD_MAX; i++ ) { + distance[i] = spawnArgs.GetVector( va("distance%d", i), "0 0 0" ); + totalDamage[i] = 0; + hitCount[i] = 0; + } + + // Get the name of the weapon (useful for outputting to an Excel-friendly format) + weaponName = spawnArgs.GetString( "weaponName", "None" ); + + // Store original location and axis + originalLocation = GetPhysics()->GetOrigin(); + originalAxis = GetPhysics()->GetAxis(); + + // Initialize the target + targetIndex = -1; + PostEventMS( &EV_ResetTarget, 0 ); + + GetPhysics()->SetContents( CONTENTS_SOLID ); +} + +//========================================================================== +// +// hhDamageTester::~hhDamageTester +// +//========================================================================== + +hhDamageTester::~hhDamageTester() { + int i; + + gameLocal.Printf( "\nDAMAGE TEST RESULTS:\n" ); + + // Weapon Name + gameLocal.Printf( "\"%s\"", weaponName); + + // Damage/Sec for each distance + for( i = 0; i < DD_MAX; i++ ) { + gameLocal.Printf(", %.1f", totalDamage[i] / testTime ); + } + + // Hits/Sec for each distance + for( i = 0; i < DD_MAX; i++ ) { + gameLocal.Printf(", %.1f", hitCount[i] / testTime ); + } + + gameLocal.Printf("\n\n"); +} + +//========================================================================== +// +// hhDamageTester::Damage +// +//========================================================================== + +void hhDamageTester::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if( targetIndex < 0 || targetIndex >= DD_MAX ) { + return; + } + + if( bUndamaged ) { // First time this object has been hit + bUndamaged = false; + PostEventSec( &EV_ResetTarget, testTime ); + } + + // Obtain damage + const idDeclEntityDef *damageDef = gameLocal.FindEntityDef( damageDefName, false ); + if ( !damageDef ) { + gameLocal.Warning( "hhDamageTester::Damage: Unknown damageDef '%s'", damageDefName ); + return; + } + + int damage = damageDef->dict.GetInt( "damage", "0" ); + + totalDamage[targetIndex] += damage * damageScale; + hitCount[targetIndex]++; + + return; +} + +//========================================================================== +// +// hhDamageTester::Event_ResetTarget +// +//========================================================================== + +void hhDamageTester::Event_ResetTarget( void ) { + // Reset the target to undamaged + bUndamaged = true; + health = 999999; // Ensure that it will never die + + targetIndex++; + if( targetIndex >= DD_MAX ) { + PostEventMS( &EV_Remove, 0 ); + return; + } + + // Set the location of the target + GetPhysics()->SetOrigin( originalLocation + distance[targetIndex] ); + UpdateVisuals(); + + PostEventSec( &EV_CheckRemove, testTime ); // Checks to autoremove the entity after a certain time +} + +//========================================================================== +// +// hhDamageTester::Event_CheckRemove +// +// Resets the target if a certain period of inactivity has occured (only if +// the weapon was unable to hit the target, for instance the wrench) +// Only happens if the target has not been hit +//========================================================================== + +void hhDamageTester::Event_CheckRemove( void ) { + if( bUndamaged && targetIndex > 0 ) { + PostEventMS( &EV_ResetTarget, 0 ); + } +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/game_damagetester.h b/src/Prey/game_damagetester.h new file mode 100644 index 0000000..16df836 --- /dev/null +++ b/src/Prey/game_damagetester.h @@ -0,0 +1,39 @@ +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#ifndef __GAME_DAMAGETESTER_H__ +#define __GAME_DAMAGETESTER_H__ + +typedef enum hhDamageDist_s { + DD_MELEE, + DD_CLOSE, + DD_MEDIUM, + DD_FAR, + DD_MAX +} hhDamageDist_t; + +class hhDamageTester : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhDamageTester ); + + ~hhDamageTester(); + void Spawn(void); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + void Event_ResetTarget( void ); + void Event_CheckRemove( void ); + +protected: + + bool bUndamaged; + int targetIndex; + float testTime; + + idVec3 originalLocation; + idMat3 originalAxis; + + int totalDamage[DD_MAX]; + int hitCount[DD_MAX]; + idVec3 distance[DD_MAX]; // Relatives distances for each target location (relative to axis) + const char *weaponName; +}; + +#endif /* __GAME_DAMAGETESTER_H__ */ +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/game_dda.cpp b/src/Prey/game_dda.cpp new file mode 100644 index 0000000..feaabcb --- /dev/null +++ b/src/Prey/game_dda.cpp @@ -0,0 +1,681 @@ +/* +=============================================================================== +game_dda.cpp + + This contains the generic functionality for the dynamic difficulty adjustment system, + as well as a statistic tracking system. +=============================================================================== +*/ + +// HEADER FILES --------------------------------------------------------------- + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//============================================================================= +// +// hhDDAManager::hhDDAManager +// +//============================================================================= + +hhDDAManager::hhDDAManager() { + bForcedDifficulty = false; + difficulty = 0.5f; + + ClearTracking(); +} + +//============================================================================= +// +// hhDDAManager::~hhDDAManager +// +//============================================================================= + +hhDDAManager::~hhDDAManager() { + ClearTracking(); +} + +//============================================================================= +// +// hhDDAManager::Save +// +//============================================================================= + +void hhDDAManager::Save(idSaveGame *savefile) const { + int i; + int num; + + savefile->WriteBool( bForcedDifficulty ); + savefile->WriteFloat( difficulty ); + + for ( i = 0; i < DDA_INDEX_MAX; i++ ) { + ddaProbability[i].Save( savefile ); + } + + savefile->WriteStringList( locationNameData ); + savefile->WriteStringList( locationData ); + + num = healthData.Num(); + savefile->WriteInt( num ); + for( i = 0; i < num; i++ ) { + savefile->WriteInt( healthData[i] ); + } + + savefile->WriteStringList( healthSpiritData ); + savefile->WriteStringList( ammoData ); + savefile->WriteStringList( miscData ); + + num = deathData.Num(); + savefile->WriteInt( num ); + for( i = 0; i < num; i++ ) { + savefile->WriteString( deathData[i].location ); + savefile->WriteInt( deathData[i].time ); + } +} + +//============================================================================= +// +// hhDDAManager::Restore +// +//============================================================================= + +void hhDDAManager::Restore( idRestoreGame *savefile ) { + int i; + int num; + + savefile->ReadBool( bForcedDifficulty ); + savefile->ReadFloat( difficulty ); + + for ( i = 0; i < DDA_INDEX_MAX; i++ ) { + ddaProbability[i].Restore( savefile ); + } + + savefile->ReadStringList( locationNameData ); + savefile->ReadStringList( locationData ); + + savefile->ReadInt( num ); + healthData.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->ReadInt( healthData[i] ); + } + + savefile->ReadStringList( healthSpiritData ); + savefile->ReadStringList( ammoData ); + savefile->ReadStringList( miscData ); + + savefile->ReadInt( num ); + deathData.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->ReadString( deathData[i].location ); + savefile->ReadInt( deathData[i].time ); + } +} + +//============================================================================= +// +// hhDDAManager::ClearTracking +// +//============================================================================= + +void hhDDAManager::ClearTracking() { + locationNameData.Clear(); + locationData.Clear(); + healthData.Clear(); + healthSpiritData.Clear(); + ammoData.Clear(); + miscData.Clear(); +} + +//============================================================================= +// +// hhDDAManager::GetDifficulty +// +//============================================================================= + +float hhDDAManager::GetDifficulty() { + return difficulty; +} + +//============================================================================= +// +// hhDDAManager::RecalculateDifficulty +// +// Recalculate overall difficulty from the individual creature difficulties +// +// Must be done after any damaged are added or deaths are recorded +//============================================================================= + +void hhDDAManager::RecalculateDifficulty( int updateFlags ) { + int count = 0; + float accum = 0.0f; + + float oldDiff = difficulty; // TEMP + + if ( g_wicked.GetBool() ) { // Wicked mode, force the DDA to 1.0 + difficulty = 1.0f; + return; + } + + if ( !g_useDDA.GetBool() ) { // Skip the DDA calculations. After g_wicked so that is still a valid mode + difficulty = 0.5f; + return; + } + + if ( bForcedDifficulty ) { // Don't recalculate if the difficulty is forced + return; + } + + for( int i = 0; i < DDA_INDEX_MAX; i++ ) { + if ( !ddaProbability[i].IsUsed() ) { // Ignore any difficulties that shouldn't count yet + continue; + } + + if ( (1 << i ) & updateFlags ) { // Only update the difficulty based upon the creatures currently attacking + accum += ddaProbability[i].GetIndividualDifficulty(); + count++; + } + } + + if ( count == 0 ) { // No individual difficulties, so return the default + difficulty = 0.5f; + } else { + difficulty = accum / count; + } + + if ( count && g_printDDA.GetBool() ) { + common->Printf( "difficulty: [%.2f]\n", difficulty ); + } +} + +//============================================================================= +// +// hhDDAManager::ForceDifficulty +// +// Force the difficulty to a given value -- used for debugging and Wicked mode +//============================================================================= + +void hhDDAManager::ForceDifficulty( float newDifficulty ) { + if ( newDifficulty > 1.0f ) { // Clamp the max + newDifficulty = 1.0f; + } else if ( newDifficulty < 0.0f ) { // negative difficulty values resets the difficulty + common->Printf( "Difficulty defaulted back to 0.5\n" ); + difficulty = 0.5f; + bForcedDifficulty = false; + return; + } + + bForcedDifficulty = true; + difficulty = newDifficulty; +} + +//============================================================================= +// +// hhDDAManager::DDA_Heartbeat +// +//============================================================================= + +void hhDDAManager::DDA_Heartbeat( hhPlayer* player ) { + idStr locText; + assert( player ); + + if ( !g_trackDDA.GetBool() || gameLocal.isMultiplayer ) { + return; + } + + ammo_t ammoType; + ammoType = idWeapon::GetAmmoNumForName( "ammo_rifle" ); + int ammo_rifle_count = player->inventory.ammo[ ammoType ]; + float ammo_rifle = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + ammoType = idWeapon::GetAmmoNumForName( "ammo_sniper" ); + int ammo_sniper_count = player->inventory.ammo[ ammoType ]; + float ammo_sniper = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + ammoType = idWeapon::GetAmmoNumForName( "ammo_crawler" ); + int ammo_crawler_count = player->inventory.ammo[ ammoType ]; + float ammo_crawler = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + ammoType = idWeapon::GetAmmoNumForName( "ammo_autocannon" ); + int ammo_autocannon_count = player->inventory.ammo[ ammoType ]; + float ammo_autocannon = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + ammoType = idWeapon::GetAmmoNumForName( "ammo_autocannon_grenade" ); + int ammo_autocannon_grenade_count = player->inventory.ammo[ ammoType ]; + float ammo_autocannon_grenade = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + ammoType = idWeapon::GetAmmoNumForName( "ammo_acid" ); + int ammo_acid_count = player->inventory.ammo[ ammoType ]; + float ammo_acid = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + ammoType = idWeapon::GetAmmoNumForName( "ammo_crawler_red" ); + int ammo_crawler_red_count = player->inventory.ammo[ ammoType ]; + float ammo_crawler_red = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + ammoType = idWeapon::GetAmmoNumForName( "ammo_energy" ); + int ammo_energy_count = player->inventory.ammo[ ammoType ]; + float ammo_energy = 100 * player->inventory.AmmoPercentage( player, ammoType ); + + + idStr location; + player->GetLocationText( location ); + + idVec3 playerOrigin = player->GetOrigin(); + + bool bTalonAttack = false; + if ( player->talon.IsValid() && player->talon->IsAttacking() ) { + bTalonAttack = true; + } + + locationNameData.Append( location ); + + locationData.Append( idStr( va( "%.2f %.2f %.2f", playerOrigin.x, playerOrigin.y, playerOrigin.z ) ) ); + + healthData.Append( player->GetHealth() ); + + healthSpiritData.Append( idStr( va( "%d, %d, %.2f, %.2f", player->GetHealth(), player->GetSpiritPower(), player->ddaProbabilityAccum, gameLocal.GetDDA()->GetDifficulty() ) ) ); + + ammoData.Append( idStr( va( "%d, %d, %d, %d, %d, %d, %d, %d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f", + ammo_rifle_count, ammo_sniper_count, ammo_crawler_count, ammo_autocannon_count, ammo_autocannon_grenade_count, ammo_acid_count, ammo_crawler_red_count, ammo_energy_count, // Ammo totals + ammo_rifle, ammo_sniper, ammo_crawler, ammo_autocannon, ammo_autocannon_grenade, ammo_acid, ammo_crawler_red, ammo_energy ) ) ); // Ammo percentages + + miscData.Append( idStr( va( "%d, %d, %d, %d, %d", player->GetCurrentWeapon(), player->IsLighterOn(), bTalonAttack, player->IsSpiritOrDeathwalking(), player->ddaNumEnemies ) ) ); // currentWeapon, lighter, talon attacking, IsSpiritOrDeathwalking, numEnemies +} + +//============================================================================= +// +// hhDDAManager::DDA_AddDamage +// +//============================================================================= + +void hhDDAManager::DDA_AddDamage( int ddaIndex, int damage ) { + if ( ddaIndex < 0 || ddaIndex >= DDA_INDEX_MAX ) { + common->Warning( "hhDDAManager::DDA_AddDamage: ddaIndex out of range %d [%d]\n", ddaIndex, DDA_INDEX_MAX ); + ddaIndex = 0; + } + + ddaProbability[ddaIndex].AddDamage( damage * 2 ); +} + +//============================================================================= +// +// hhDDAManager::DDA_AddSurvivalHealth +// +//============================================================================= + +void hhDDAManager::DDA_AddSurvivalHealth( int ddaIndex, int health ) { + if ( ddaIndex < 0 || ddaIndex >= DDA_INDEX_MAX ) { + common->Warning( "hhDDAManager::DDA_AddSurvivalHealth: ddaIndex out of range %d [%d]\n", ddaIndex, DDA_INDEX_MAX ); + ddaIndex = 0; + } + + ddaProbability[ddaIndex].AddSurvivalValue( health ); + + // Adjust DDA test + float prob = DDA_GetProbability( ddaIndex, health ); + float probAdjust = ( prob - 0.5f ) * 0.05f; // Difficulty adjustment raises and lowers based upon how far the probability is from the dda level + + ddaProbability[ddaIndex].AdjustDifficulty( probAdjust ); +} + +//============================================================================= +// +// hhDDAManager::DDA_AddDeath +// +//============================================================================= + +void hhDDAManager::DDA_AddDeath( hhPlayer *player, idEntity *attacker ) { + hhMonsterAI *monster; + int ddaIndex; + + if ( g_trackDDA.GetBool() && !gameLocal.isMultiplayer ) { + ddaDeath_t death; + death.time = gameLocal.GetTime(); + player->GetLocationText( death.location ); + deathData.Append( death ); + } + + if ( !attacker->IsType( hhMonsterAI::Type ) ) { + return; + } + + monster = static_cast(attacker); + + if ( monster ) { + ddaIndex = monster->spawnArgs.GetInt( "ddaIndex", "0" ); + + if ( ddaIndex < 0 ) { // Player could be killed by monsters that shouldn't be included in the DDA + return; + } + + if ( ddaIndex >= DDA_INDEX_MAX ) { + ddaIndex = 0; + } + } + + ddaProbability[ddaIndex].AdjustDifficulty( -0.075f ); +} + +//============================================================================= +// +// hhDDAManager::DDA_GetProbability +// +// Return the probability that this index of creature will do enough damage to reduce the player's health to zero +//============================================================================= + +float hhDDAManager::DDA_GetProbability( int ddaIndex, int value ) { + if ( ddaIndex < 0 || ddaIndex >= DDA_INDEX_MAX ) { + common->Warning( "hhDDAManager::DDA_GetProbability: ddaIndex out of range %d [%d]\n", ddaIndex, DDA_INDEX_MAX ); + ddaIndex = 0; + } + + return ddaProbability[ddaIndex].GetProbability( value ); +} + +//============================================================================= +// +// hhDDAManager::Export +// +//============================================================================= + +void hhDDAManager::Export( const char* filename ) { + idStr ExportFile, ExportBase; + + if( !filename ) { + ExportBase = gameLocal.GetMapName(); + ExportBase.StripPath(); + ExportBase.StripFileExtension(); + ExportBase = va("%s_%s", cvarSystem->GetCVarString("win_username"), ExportBase.c_str() ); + ExportBase.DefaultPath( "statfiles/" ); + } + else { + ExportBase = va( "%s_", filename ); + ExportBase.StripFileExtension(); + ExportBase.DefaultPath( "statfiles/" ); + } + + gameLocal.Printf( "Exporting stats to csv files\n" ); + + // Export to .cvs files and to a .lin file + ExportDDAData( ExportBase, "Location Name, Health, Spirit Power, Survival Chance, DDA", "_HEALTH", healthSpiritData ); + + ExportDDAData( ExportBase, + "Location Name, Ammo Rifle, Ammo Sniper, Ammo Crawler, Ammo Autocannon, Ammo Grenade, Ammo Acid, Ammo Launcher, Ammo Energy, Ammo Rifle %, Ammo Sniper %, Ammo Crawler %, Ammo Autocannon %, Ammo Grenade %, Ammo Acid %, Ammo Launcher %, Ammo Energy %", + "_AMMO", ammoData ); + + ExportDDAData( ExportBase, "Location Name, Current Weapon, Lighter On, Talon Attacking, IsSpiritOrDeathwalking, Num Enemies", "_MISC", miscData ); + + ExportLINData( ExportBase ); +} + +//============================================================================= +// +// hhDDAManager::ExportDDAData +// +//============================================================================= + +void hhDDAManager::ExportDDAData( idStr fileName, const char* header, const char *fileAddition, idList data ) { + idFile* f = NULL; + + //Build the filename + fileName.Append( fileAddition ); + + fileName.DefaultFileExtension("csv"); + f = fileSystem->OpenFileWrite( fileName.c_str() ); + if( !f ) { + common->Warning( "Failed to open stat tracking file '%s'", fileName.c_str() ); + return; + } + + //Export the header for this file... + f->Printf( "%s\n", header ); + + for( int i = 0; i < data.Num(); i++ ) { + f->Printf( "%s, %s\n", locationNameData[i].c_str(), data[i].c_str() ); + } + + //Close the output file + fileSystem->CloseFile( f ); +} + +//============================================================================= +// +// hhDDAManager::ExportLINData +// +//============================================================================= + +void hhDDAManager::ExportLINData( idStr fileName ) { + idFile* f = NULL; + + //Build the filename + fileName.DefaultFileExtension("lin"); + f = fileSystem->OpenFileWrite( fileName.c_str() ); + if( !f ) { + common->Warning( "Failed to open stat tracking file '%s'", fileName.c_str() ); + return; + } + + for( int i = 0; i < locationData.Num(); i++ ) { + if ( healthData[i] > 0 ) { // Only store living health values in the .lin file + f->Printf( "%s\n", locationData[i].c_str() ); + } + } + + //Close the output file + fileSystem->CloseFile( f ); +} + +//============================================================================= +// +// hhDDAManager::PrintDDA +// +//============================================================================= + +void hhDDAManager::PrintDDA( void ) { + int i; + hhDDAProbability *prob; + + for( i = 0; i < DDA_INDEX_MAX; i++ ) { + prob = &ddaProbability[i]; + if ( !prob->IsUsed() ) { + continue; + } + + common->Printf( "DDA Index: %d\n", i ); + common->Printf( "Ind. Difficulty: %.2f\n", prob->GetIndividualDifficulty() ); +/* + common->Printf( "Last Damages: [%.1f] [%.1f] [%.1f] [%.1f] [%.1f] [%.1f] [%.1f] [%.1f]\n", + prob->damages[0], prob->damages[1], prob->damages[2], prob->damages[3], + prob->damages[4], prob->damages[5], prob->damages[6], prob->damages[7] ); +*/ + } +} + +//============================================================================= +// NEW DDA +//============================================================================= + +//============================================================================= +// +// hhDDAProbability::hhDDAProbability +// +//============================================================================= + +hhDDAProbability::hhDDAProbability() { + damageRover = 0; + survivalRover = 0; + + mean = 25.0f; + stdDeviation = 1.0f; + bUsed = false; + individualDifficulty = 0.5f; + + for( int i = 0; i < NUM_DAMAGES; i++ ) { + damages[i] = mean; // Initialize to a low, but reasonable value + survivalValues[i] = 0.5f; // Initialize to a middle value + } +} + +//============================================================================= +// +// hhDDAProbability::Save +// +//============================================================================= + +void hhDDAProbability::Save( idSaveGame *savefile ) const { + int i; + + savefile->WriteBool( bUsed ); + + for ( i = 0; i < NUM_DAMAGES; i++ ) { + savefile->WriteInt( damages[i] ); + } + + savefile->WriteInt( damageRover ); + + for ( i = 0; i < NUM_DAMAGES; i++ ) { + savefile->WriteFloat( survivalValues[i] ); + } + + savefile->WriteInt( survivalRover ); + + savefile->WriteFloat( mean ); + savefile->WriteFloat( stdDeviation ); + savefile->WriteFloat( individualDifficulty ); +} + +//============================================================================= +// +// hhDDAProbability::Restore +// +//============================================================================= + +void hhDDAProbability::Restore( idRestoreGame *savefile ) { + int i; + + savefile->ReadBool( bUsed ); + + for ( i = 0; i < NUM_DAMAGES; i++ ) { + savefile->ReadInt( damages[i] ); + } + + savefile->ReadInt( damageRover ); + + for ( i = 0; i < NUM_DAMAGES; i++ ) { + savefile->ReadFloat( survivalValues[i] ); + } + + savefile->ReadInt( survivalRover ); + + savefile->ReadFloat( mean ); + savefile->ReadFloat( stdDeviation ); + savefile->ReadFloat( individualDifficulty ); +} + +//============================================================================= +// +// hhDDAProbability::AddDamage +// +//============================================================================= + +void hhDDAProbability::AddDamage( int damage ) { + // Add the damage to the list + damages[ damageRover ] = damage; + damageRover = ( damageRover + 1 ) % NUM_DAMAGES; + + bUsed = true; + + CalculateProbabilityCurve(); +} + +//============================================================================= +// +// hhDDAProbability::AddSurvivalValue +// +//============================================================================= + +void hhDDAProbability::AddSurvivalValue( int playerHealth ) { + // Get the survival probability against this creature and store it + survivalValues[ survivalRover ] = GetProbability( playerHealth ); + survivalRover = ( survivalRover + 1 ) % NUM_DAMAGES; +} + +//============================================================================= +// +// hhDDAProbability::GetProbability +// +//============================================================================= + +float hhDDAProbability::GetProbability( int value ) { + float z; + float x; + float probability = 0; + + if ( stdDeviation <= 0.0f ) { + stdDeviation = 1.0f; + } + + z = idMath::Fabs( value - mean ) / stdDeviation; + x = 1 + z * ( 0.049867347 + z * ( 0.0211410061 + z * ( 0.0032776263 ))); + probability = ( idMath::Exp( idMath::Log( x ) * -16 ) / 2 ); + if ( value >= mean ) { + probability = 1.0f - probability; + } + + return probability; +} + +//============================================================================= +// +// hhDDAProbability::CalculateProbabilityCurve +// +//============================================================================= + +void hhDDAProbability::CalculateProbabilityCurve() { + int i; + float diff; + float sumDiffSqr = 0; + + // Recompute mean and std deviation when new damage information is obtained + mean = 0; + for ( i = 0; i < NUM_DAMAGES; i++ ) { + mean += damages[i]; + } + mean /= NUM_DAMAGES; + + // calculate standard deviation + for ( i = 0; i < NUM_DAMAGES; i++ ) { + diff = mean - damages[i]; + sumDiffSqr += diff * diff; + } + + stdDeviation = idMath::Sqrt( sumDiffSqr / NUM_DAMAGES ); +} + +//============================================================================= +// +// hhDDAProbability::GetSurvivalMean +// +//============================================================================= + +float hhDDAProbability::GetSurvivalMean( void ) { + float sum = 0; + + for ( int i = 0; i < NUM_DAMAGES; i++ ) { + sum += survivalValues[i]; + } + + return sum / NUM_DAMAGES; +} + +//============================================================================= +// +// hhDDAProbability::AdjustDifficulty +// +//============================================================================= + +void hhDDAProbability::AdjustDifficulty( float diff ) { + individualDifficulty = idMath::ClampFloat( 0.0f, 1.0f, individualDifficulty + diff ); +} diff --git a/src/Prey/game_dda.h b/src/Prey/game_dda.h new file mode 100644 index 0000000..5c7abb7 --- /dev/null +++ b/src/Prey/game_dda.h @@ -0,0 +1,128 @@ +/* +=============================================================================== +game_dda.h + This contains the functionality for a dynamic difficulty adjustment system, + as well as a statistic tracking system. +=============================================================================== +*/ +#ifndef __GAME_DDA_H__ +#define __GAME_DDA_H__ + +// DDA_Export Methods. +enum hhDDAExport { + DDA_ExportTEXT, + DDA_ExportCSV, +}; + +typedef struct ddaDeath_s { + int time; + idStr location; +} ddaDeath_t; + +const int DDA_INDEX_MAX = 16; + +//====================================================================================== +//====================================================================================== +// +// NEW DDA SYSTEM +// +//====================================================================================== +//====================================================================================== + +#define NUM_DAMAGES 8 + +class hhDDAProbability { +public: + hhDDAProbability(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void AddDamage( int damage ); + void AddSurvivalValue( int playerHealth ); + + float GetProbability( int value ); + float GetSurvivalMean( void ); + + void AdjustDifficulty( float diff ); + float GetIndividualDifficulty( void ) { return individualDifficulty; } + + bool IsUsed( void ) { return bUsed; } + +protected: + + void CalculateProbabilityCurve(); + + bool bUsed; // True if this probability instance is used (so the damage probability of later creatures doesn't influence earlier levels) + + int damages[NUM_DAMAGES]; + int damageRover; + + float survivalValues[NUM_DAMAGES]; + int survivalRover; + + float mean; + float stdDeviation; + + float individualDifficulty; +}; + +/*********************************************************************** + hhDDAManager. + Class that controls the DDA tracking and difficulty of the game. +***********************************************************************/ +class hhDDAManager { +public: + hhDDAManager(); + ~hhDDAManager(); +public: + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //Public difficulty functions/accessors.. + float GetDifficulty(); //Called to get current difficulty rating (0.0 - 1.0) + void DifficultySummary(); + void ForceDifficulty( float newDifficulty ); // Force the difficulty to this value + + void RecalculateDifficulty( int updateFlags ); + + //Public DDA Notifications. (these are called to update the DDA system about activities happening in the game) + void DDA_Heartbeat( hhPlayer* player ); + + void DDA_AddDamage( int ddaIndex, int damage ); + void DDA_AddSurvivalHealth( int ddaIndex, int health ); + void DDA_AddDeath( hhPlayer *player, idEntity *attacker ); + + float DDA_GetProbability( int ddaIndex, int value ); + + //Public DDA Exporting functions. These are called to export DDA stats to either the console or a file. + void Export( const char* filename ); + + void PrintDDA( void ); + + //Public DDA Controls. These are used to clear/reset the DDA system (on map-switching). + void ClearTracking(); + +protected: + //Protected Internal functions for exporting purposes. + void ExportDDAData( idStr fileName, const char* header, const char *fileAddition, idList data ); + void ExportLINData( idStr FileName ); + + //Protected Interal Difficulty 'Ticker'. Difficulty system will re-evaluate whether to change diff here. + //o void DifficultyUpdate(); + + //Protected Internal Difficulty data. + bool bForcedDifficulty; // Used for testing, and Wicked mode + float difficulty; // Our current difficulty rating. + hhDDAProbability ddaProbability[DDA_INDEX_MAX]; // CJR: array of probabilites used in the DDA system + + idList locationNameData; + idList locationData; + idList healthData; + idList healthSpiritData; + idList ammoData; + idList miscData; + idList deathData; +}; + +#endif // __GAME_DDA_H__ \ No newline at end of file diff --git a/src/Prey/game_deathwraith.cpp b/src/Prey/game_deathwraith.cpp new file mode 100644 index 0000000..de82b70 --- /dev/null +++ b/src/Prey/game_deathwraith.cpp @@ -0,0 +1,559 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +#define DEBUGGING_WRAITHS 0 + +//============================================================================= +// hhDeathWraith +//============================================================================= + +CLASS_DECLARATION( hhWraith, hhDeathWraith ) + EVENT( AI_FindEnemy, hhWraith::Event_FindEnemy ) +END_CLASS + + +//============================================================================= +// +// hhDeatWraith::Spawn +// +//============================================================================= + +void hhDeathWraith::Spawn() { + // Each wraith is assigned a random distance, rotation speed, and height around the enemy + idVec3 minCircleInfo = spawnArgs.GetVector( "min_circleInfo", "100 10 100" ); // dist, speed, height + idVec3 maxCircleInfo = spawnArgs.GetVector( "max_circleInfo", "100 10 100" ); // dist, speed, height + + circleDist = minCircleInfo[0] + gameLocal.random.RandomFloat() * ( maxCircleInfo[0] - minCircleInfo[0] ); + circleSpeed = minCircleInfo[1] + gameLocal.random.RandomFloat() * ( maxCircleInfo[1] - minCircleInfo[1] ); + circleHeight = minCircleInfo[2] + gameLocal.random.RandomFloat() * ( maxCircleInfo[2] - minCircleInfo[2] ); + + circleClockwise = (gameLocal.random.RandomFloat() > 0.5f ? true : false); + + // Compute the random attack time (in frames) + attackTimeMin = spawnArgs.GetFloat( "min_attackTime", "4" ) * 60; + attackTimeDelta = ( spawnArgs.GetFloat( "max_attackTime", "8" ) * 60 ) - attackTimeMin; + + if ( state == WS_FLY ) { + // The wraith is now circling the enemy - set the attack time + countDownTimer = attackTimeMin + gameLocal.random.RandomInt( attackTimeDelta ); + } + + // Determine the wraith type + healthWraith = (gameLocal.random.RandomFloat() < spawnArgs.GetFloat( "healthTypeChance", "0.35" ) ? true : false); +} + +//============================================================================= +// +// hhDeathWraith::FlyUp +// +//============================================================================= +void hhDeathWraith::FlyUp() { + hhWraith::FlyUp(); + + if ( state == WS_FLY ) { + // The wraith is now circling the enemy - set the attack time + countDownTimer = attackTimeMin + gameLocal.random.RandomInt( attackTimeDelta ); + } +} + +//============================================================================= +// +// hhDeathWraith::FlyToEnemy +// +// Circle an enemy +//============================================================================= +void hhDeathWraith::FlyToEnemy() { + float distAdjust; + idVec3 origin; + float delta; + bool dir; + +#if DEBUGGING_WRAITHS +gameRenderWorld->DebugLine(colorWhite, GetOrigin(), GetOrigin()+idVec3(0,0,5), 5000); +#endif + + // Look for an enemy for the Wraith + // FIXME: This needs to be done often, because the player could spiritwalk after the Wraith has him as the enemy + // and we want the wraiths to try to target the player's proxy. + // We could either have this check via a heartbeat, or have some type of broadcast message when the player spiritwalks (?) + Event_FindEnemy( false ); + + // Ensure that the wraith is on the correct path, and if not, then set the wraith's velocity and angle to return to the path + origin = GetOrigin(); + idVec3 toEnemy = enemy->GetOrigin() - origin; + toEnemy.z = 0; + float distance = toEnemy.Normalize(); + + idVec3 tangent( -toEnemy.y, toEnemy.x, 0 ); // Quick tangent, swap x and y + + // If counter-clockwise, then reverse the tangent vector + if ( !circleClockwise ) { + tangent *= -1; + } + + // Determine if the radius is too large and gradually pull the Wraith in closer or if the wraith is too close, push it out + if ( distance > circleDist + circleSpeed ) { // Move closer + distAdjust = circleSpeed * 0.1f; + } else if ( distance < circleDist - circleSpeed ) { // Move farther away + distAdjust = -circleSpeed * 0.1f; + } else { // No adjustment + distAdjust = 0; + } + + // Decide if the wraith should change z-height + idVec3 zVelocity( 0, 0, 0 ); + + if ( origin.z < enemy->GetOrigin().z + circleHeight ) { + zVelocity.z = 2.0f; + } else if ( origin.z > enemy->GetOrigin().z + circleHeight + 30.0f ) { + zVelocity.z = -2.0f; + } + + // Move the wraith through the world + idVec3 velocity = tangent * circleSpeed + toEnemy * distAdjust + zVelocity; + + // Rotate towards velocity direction + dir = GetFacePosAngle( GetOrigin() + velocity, delta ); + + if ( delta > this->turn_threshold ) { // Wraith should turn + if ( dir ) { // Turn to the left + deltaViewAngles.yaw = this->turn_radius_max * (60.0f * USERCMD_ONE_OVER_HZ); + } else { + deltaViewAngles.yaw = -this->turn_radius_max * (60.0f * USERCMD_ONE_OVER_HZ); + } + } + + SetOrigin( GetOrigin() + (velocity * (60.0f * USERCMD_ONE_OVER_HZ)) ); + + // Decide if the wraith has waited long enough and then attack + if ( --countDownTimer <= 0 ) { + if ( health > 0 && enemy.IsValid() ) { // Only attack if the wraith has an enemy (which it should at all times) and the wraith hasn't been killed + EnterAttackState(); + } else { // Reset the attack time + countDownTimer = attackTimeMin + gameLocal.random.RandomInt( attackTimeDelta ); + } + } +} + +//============================================================================= +// +// hhDeathWraith::Damage +// +//============================================================================= +void hhDeathWraith::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + // Skip wraiths brighten when damaged logic + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} + +//============================================================================= +// +// hhDeathWraith::Killed +// +// When a deathwraith is killed, it immediately is removed +// The queue is reset and a new wraith will be spawned shortly after +//============================================================================= +void hhDeathWraith::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + hhFxInfo fxInfo; + + // Stop flyloop and chatter sounds, if any + StopSound( SND_CHANNEL_BODY ); + StopSound( SND_CHANNEL_VOICE ); + + SetShaderParm( SHADERPARM_TIMEOFFSET, MS2SEC( gameLocal.time ) ); + SetShaderParm( SHADERPARM_DIVERSITY, 1.0f ); + + const char *deathEffect = spawnArgs.GetString("fx_deatheffect1"); + if (attacker && attacker->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast( attacker ); + if ( healthWraith ) { + deathEffect = spawnArgs.GetString("fx_deatheffect2"); + } + } + + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoAlongBone( deathEffect, spawnArgs.GetString( "joint_collision" ), &fxInfo ); + + physicsObj.SetContents(0); + fl.takedamage = false; + UnlinkCombat(); + + StartSound( "snd_death", SND_CHANNEL_VOICE ); + + NotifyDeath( inflictor, attacker ); + + PostEventSec( &EV_Remove, spawnArgs.GetFloat( "burnOutTime" ) ); +} + +//============================================================================= +// +// hhDeathWraith::NotifyDeath +// +// Handles notification of player on death +//============================================================================= +void hhDeathWraith::NotifyDeath( idEntity *inflictor, idEntity *attacker ) { + + if ( attacker && attacker->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast( attacker ); + + SpawnEnergy(player); + + if ( player->IsDeathWalking() ) { + player->KilledDeathWraith(); + } + } +} + +//============================================================================= +// +// hhDeathWraith::SpawnEnergy +// +// Energy travels from wraith to deathWalkProxy +//============================================================================= +void hhDeathWraith::SpawnEnergy(hhPlayer *player) { + idEntity *destEntity = NULL; + const char *deathEnergyName = NULL; + idVec3 origin; + idMat3 axis; + idDict args; + + if ( healthWraith ) { + // Spawn health energy + deathEnergyName = spawnArgs.GetString("def_deathEnergyHealth", NULL); + } + else { + // Spawn spirit power energy + deathEnergyName = spawnArgs.GetString("def_deathEnergySpirit", NULL); + } + destEntity = player->GetDeathwalkEnergyDestination(); + + // Spawn the energy system that will deliver the spiritpower to the player + if (deathEnergyName && destEntity) { + idVec3 center = GetPhysics()->GetAbsBounds().GetCenter(); + args.Clear(); + args.Set("origin", center.ToString()); + idEntity *ent = gameLocal.SpawnObject(deathEnergyName, &args); + if (ent && ent->IsType(hhDeathWraithEnergy::Type)) { + hhDeathWraithEnergy *energy = static_cast(ent); + energy->SetPlayer(player); + energy->SetDestination( destEntity->GetOrigin() ); + } + } +} + +//============================================================================= +// +// hhDeathWraith::Think +// +//============================================================================= +void hhDeathWraith::Think( void ) { + + if ( enemy.IsValid() ) { + assert( enemy->IsType( hhPlayer::Type ) ); + hhPlayer *player = reinterpret_cast ( enemy.GetEntity() ); + + if( !player->IsDeathWalking() ) { + BecomeInactive( TH_THINK ); + PostEventMS( &EV_Remove, 0 ); + return; + } + else { + if ( healthWraith ) { + SetShaderParm(SHADERPARM_MODE, 1.0f); + } + else { + SetShaderParm(SHADERPARM_MODE, 0.0f); + } + } + } + + // Don't call hhWraith::Think(), since we don't want our health to drop over time + hhMonsterAI::Think(); + + // Since idAI is only capable of yawing, not piching, we post apply our pitch + if (state == WS_DEATH_CHARGE && enemy.IsValid()) { + idVec3 toEnemy = enemy->GetOrigin() - GetOrigin(); + toEnemy.Normalize(); + viewAxis = toEnemy.ToMat3(); + } + +#if DEBUGGING_WRAITHS + gameRenderWorld->DebugLine(colorRed, GetOrigin(), GetOrigin()+viewAxis[0]*20, 1000); + gameRenderWorld->DebugLine(colorGreen, GetOrigin(), GetOrigin()+viewAxis[1]*20, 1000); + gameRenderWorld->DebugLine(colorBlue, GetOrigin(), GetOrigin()+viewAxis[2]*20, 1000); +#endif +} + +//============================================================================= +// +// hhDeathWraith::FlyMove +// +//============================================================================= +void hhDeathWraith::FlyMove( void ) { + idMat3 axis; + idVec3 delta; + idVec3 toEnemy; + + // The state of the creature determines how it will move + switch ( state ) { + case WS_SPAWN: + FlyUp(); // Flying up right after spawn + break; + case WS_FLY: + FlyToEnemy(); + break; + case WS_FLEE: // Flying away + FlyAway(); + break; + case WS_DEATH_CHARGE: // Charging an enemy + +#if 1 + // Tranform animation movement by actual 'pitched' axis + if (enemy.IsValid()) { + toEnemy = enemy->GetOrigin() - GetOrigin(); + toEnemy.Normalize(); + axis = toEnemy.ToMat3(); + } else { // HUMANHEAD mdl: Enemy might not be valid if player is just leaving deathwalk + axis = viewAxis; + } +#else + axis = viewAxis; +#endif + + if (ChargeEnemy()) { + // Using animation delta to generate movement + physicsObj.UseFlyMove( false ); + physicsObj.UseVelocityMove( false ); + GetMoveDelta( axis, axis, delta ); + physicsObj.SetDelta( delta ); + physicsObj.ForceDeltaMove( true ); + RunPhysics(); + } + return; + case WS_STILL: // Wraith is not moving at all (playing a specific anim, etc) + return; + } + + // run the physics for this frame + physicsObj.UseFlyMove( true ); + physicsObj.UseVelocityMove( false ); + physicsObj.SetDelta( vec3_zero ); + physicsObj.ForceDeltaMove( disableGravity ); + RunPhysics(); +} + + +//============================================================================= +// +// hhDeathWraith::EnterAttackState +// +//============================================================================= +void hhDeathWraith::EnterAttackState() { + state = WS_DEATH_CHARGE; + + // Set the wraith's rotation target towards the enemy + TurnTowardEnemy(); + + SetShaderParm( SHADERPARM_MISC, 1 ); // Charging + StartSound( "snd_charge", SND_CHANNEL_VOICE2 ); + + countDownTimer = PlayAnim( "alert", 2 ) / USERCMD_MSEC; +} + +//============================================================================= +// +// hhDeathWraith::ChargeEnemy +// +//============================================================================= +bool hhDeathWraith::ChargeEnemy( void ) { + + if ( !enemy.IsValid() ) { + ExitAttackState(); + return false; + } + + assert(enemy.IsValid() && enemy->IsType(hhPlayer::Type)); + + // Set the wraith's rotation target towards the enemy + TurnTowardEnemy(); + + // Attempt to attack the enemy + // If the wraith did not damage the enemy, then continue the attack flight + if ( !CheckEnemy() ) { + // Once the animation is finished (and the wraith wasn't killed or didn't hit the player), + // then transition to circle mode which will handle getting the wraith back to the path + if ( --countDownTimer <= 0 ) { + ExitAttackState(); + return false; + } + } + return true; +} + +//============================================================================= +// +// hhDeathWraith::ExitAttackState +// +//============================================================================= +void hhDeathWraith::ExitAttackState() { + state = WS_FLY; + PlayCycle( "fly", 2 ); + countDownTimer = attackTimeMin + gameLocal.random.RandomInt( attackTimeDelta ); + SetShaderParm( SHADERPARM_MISC, 0 ); // No longer charging + + GetPhysics()->SetLinearVelocity(vec3_origin); +} + +//============================================================================= +// +// hhDeathWraith::CheckEnemy +// +//============================================================================= +bool hhDeathWraith::CheckEnemy( void ) { + + // Don't try to damage if the wraith doesn't have a target, or if the wraith is dead or harvested + if ( health <= 0 || !enemy.IsValid() || fl.takedamage == false ) { + return false; + } + + idVec3 center = GetPhysics()->GetAbsBounds().GetCenter(); + idVec3 dir = enemy->GetPhysics()->GetAbsBounds().GetCenter() - center; + + if ( dir.LengthSqr() < spawnArgs.GetFloat( "minDamageDistSqr", "2500" ) ) { + HitEnemy(); + return true; + } + + return false; +} + +//============================================================================= +// +// hhDeathWraith::HitEnemy +// +//============================================================================= +void hhDeathWraith::HitEnemy( void ) { + hhFxInfo fxInfo; + + if (IsHidden()) { + return; + } + + // Activate the death trigger + idEntityPtr deathTrigger = gameLocal.FindEntity( spawnArgs.GetString( "triggerOnHit" ) ); + if ( deathTrigger.IsValid() ) { + if ( deathTrigger->RespondsTo( EV_Activate ) || deathTrigger->HasSignal( SIG_TRIGGER ) ) { + deathTrigger->Signal( SIG_TRIGGER ); + deathTrigger->ProcessEvent( &EV_Activate, this ); + } + } + + Hide(); + fl.takedamage = false; + + // Hit enemy effects -- for now, using the death effects + idVec3 center = GetPhysics()->GetAbsBounds().GetCenter(); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_hitenemy", center, mat3_identity, &fxInfo ); + StartSound( "snd_attack", SND_CHANNEL_VOICE ); + + ApplyImpulseToEnemy(); + + if (enemy->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast(enemy.GetEntity()); + + if ( healthWraith ) { + int playerHealth = player->GetHealth(); + int minHealth = player->spawnArgs.GetInt( "minResurrectHealth", "50" ); + + playerHealth -= spawnArgs.GetInt( "playerHealthDamage", "10" ); + if ( playerHealth < minHealth ) { + playerHealth = minHealth; + } + player->SetHealth( playerHealth ); + } else { + player->DeathWalkDamagedByWraith( this, spawnArgs.GetString( "def_damagetype" ) ); + } + } + + PostEventMS( &EV_Remove, 2000 ); +} + +//============================================================================= +// +// hhDeathWraith::ApplyImpulseToEnemy +// +//============================================================================= +void hhDeathWraith::ApplyImpulseToEnemy() { + if( enemy.IsValid() ) { + idMat3 axis = GetAxis(); + idVec3 impulseDir = axis[0] - axis[2]; + impulseDir.Normalize(); + enemy->ApplyImpulse( this, 0, enemy->GetOrigin(), impulseDir * spawnArgs.GetFloat("impulseMagnitude") * enemy->GetPhysics()->GetMass() ); + } +} + +//============================================================================= +// +// hhDeathWraith::EnemyDead +// +//============================================================================= +void hhDeathWraith::EnemyDead() { + return; // Death wraiths expect their enemies to be dead. +} + +//============================================================================= +// +// hhDeathWraith::PlayCycle +// +//============================================================================= +void hhDeathWraith::PlayCycle( const char *name, int blendFrame ) { + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), FRAME2MS(blendFrame) ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, GetAnimator()->GetAnim(name), gameLocal.GetTime(), FRAME2MS(blendFrame) ); +} + +//============================================================================= +// +// hhDeathWraith::PlayAnim +// +//============================================================================= +int hhDeathWraith::PlayAnim( const char *name, int blendFrame ) { + int fadeTime = FRAME2MS( blendFrame ); + int animLength = 0; + + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), FRAME2MS(blendFrame) ); + + int anim = GetAnimator()->GetAnim( name ); + if ( anim ) { + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, anim, gameLocal.GetTime(), fadeTime ); + animLength = GetAnimator()->CurrentAnim( ANIMCHANNEL_ALL )->Length(); + animLength = (animLength > fadeTime) ? animLength - fadeTime : animLength; + } + + return animLength; +} + +//============================================================================= +// +// hhDeathWraith::Event_FindEnemy +// +//============================================================================= +void hhDeathWraith::Event_FindEnemy( int useFOV ) { + // These are meant for single player deathwalk only + idPlayer *player = gameLocal.GetLocalPlayer(); + SetEnemy( player ); + idThread::ReturnEntity( player ); +} + +//============================================================================= +// +// hhDeathWraith::TeleportIn +// +//============================================================================= + +void hhDeathWraith::TeleportIn( idEntity *activator ) { + Show(); + PostEventMS( &EV_Activate, 0, activator ); +} diff --git a/src/Prey/game_deathwraith.h b/src/Prey/game_deathwraith.h new file mode 100644 index 0000000..51763c9 --- /dev/null +++ b/src/Prey/game_deathwraith.h @@ -0,0 +1,48 @@ +#ifndef __GAME_DEATHWRAITH_H__ +#define __GAME_DEATHWRAITH_H__ + +class hhDeathWraith : public hhWraith { + public: + CLASS_PROTOTYPE( hhDeathWraith ); + + void Spawn(); + virtual void FlyToEnemy(); + virtual void SetEnemy( idActor *newEnemy ) { enemy = newEnemy; } + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Think( void ); + + protected: + virtual int PlayAnim( const char *name, int blendFrame ); + virtual void PlayCycle( const char *name, int blendFrame ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void NotifyDeath( idEntity *inflictor, idEntity *attacker ); + void SpawnEnergy(hhPlayer *player); + void EnterAttackState(); + void ExitAttackState(); + virtual bool ChargeEnemy(); + bool CheckEnemy(); + void HitEnemy(); + void ApplyImpulseToEnemy(); + virtual void FlyUp(); + virtual void FlyMove( void ); + virtual void EnemyDead(); + void Event_FindEnemy( int useFOV ); + void TeleportIn( idEntity *activator ); + + protected: + // Variables for controlling the movement of the wraith around the anchor point + // These variables are unique for each wraith for variety + float circleDist; // Distance to stay from the anchor point + float circleSpeed; // Orbit speed + float circleHeight; // Height above the point + bool circleClockwise; // if true, orbit clockwise + + int attackTimeMin; // Range for the attack time + int attackTimeDelta; // Range for the attack time + + idMat3 chargeAxis; + + bool healthWraith; // true if a health type +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_debrisspawner.cpp b/src/Prey/game_debrisspawner.cpp new file mode 100644 index 0000000..eb0d9ba --- /dev/null +++ b/src/Prey/game_debrisspawner.cpp @@ -0,0 +1,439 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_RemoveAll( "removeAll", NULL ); + +CLASS_DECLARATION( idEntity, hhDebrisSpawner ) + EVENT( EV_Activate, hhDebrisSpawner::Activate ) +END_CLASS + +/* +============ +hhDebrisSpawner::hhDebrisSpawner() +============ +*/ +hhDebrisSpawner::hhDebrisSpawner() { +} + + +/* +============ +hhDebrisSpawner::~hhDebrisSpawner() +============ +*/ +hhDebrisSpawner::~hhDebrisSpawner() { +} + + +/* +============ +hhDebrisSpawner::Spawn() + Spawns the entity, and all other entities associated with the mass +============ +*/ +void hhDebrisSpawner::Spawn() { + + // Set the defaults + activated = false; + sourceEntity = NULL; + hasBounds = false; + + // Get the passed in + spawnArgs.GetVector( "origin", "0 0 0", origin ); + spawnArgs.GetVector( "orientation", "1 0 0", orientation ); + spawnArgs.GetVector( "velocity", "0 0 0", velocity ); + spawnArgs.GetVector( "power", "50 50 50", power ); + spawnArgs.GetFloat( "duration", "0", duration ); + spawnArgs.GetBool( "multi_trigger", "0", multiActivate ); + spawnArgs.GetBool( "spawnUsingEntity", "0", useEntity ); + spawnArgs.GetBool( "fillBounds", "1", fillBounds ); + spawnArgs.GetBool( "testBounds", "1", testBounds ); + spawnArgs.GetBool( "nonsolid", "0", nonSolid ); + spawnArgs.GetBool( "useAFBounds", "0", useAFBounds ); //rww + + // Hide the model + GetPhysics()->SetContents( 0 ); + Hide(); + bounds = GetPhysics()->GetClipModel()->GetBounds(); + GetPhysics()->DisableClip(); + + // Do stuff + power *= 20; + + // spawnWhenActivated means that + if ( !useEntity && !spawnArgs.GetInt( "trigger", "0" ) ) { + Activate( NULL ); + } +} + +void hhDebrisSpawner::Save(idSaveGame *savefile) const { + savefile->WriteVec3(origin); + savefile->WriteVec3(orientation); + savefile->WriteVec3(velocity); + savefile->WriteVec3(power); + savefile->WriteBool(activated); + savefile->WriteBool(multiActivate); + savefile->WriteBool(hasBounds); + savefile->WriteBounds(bounds); + savefile->WriteFloat(duration); + savefile->WriteBool(fillBounds); + savefile->WriteBool(testBounds); + savefile->WriteObject(sourceEntity); +} + +void hhDebrisSpawner::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3(origin); + savefile->ReadVec3(orientation); + savefile->ReadVec3(velocity); + savefile->ReadVec3(power); + savefile->ReadBool(activated); + savefile->ReadBool(multiActivate); + savefile->ReadBool(hasBounds); + savefile->ReadBounds(bounds); + savefile->ReadFloat(duration); + savefile->ReadBool(fillBounds); + savefile->ReadBool(testBounds); + savefile->ReadObject( reinterpret_cast(sourceEntity) ); + + spawnArgs.GetBool( "spawnUsingEntity", "0", useEntity ); + spawnArgs.GetBool( "nonsolid", "0", nonSolid ); + spawnArgs.GetBool( "useAFBounds", "0", useAFBounds ); +} + + +/* +================ +hhDebrisSpawner::Activate + Do the actual spawning of debris, FX, etc.. +================ +*/ +void hhDebrisSpawner::Activate( idEntity *aSourceEntity ) { + + if ( useEntity ) { + sourceEntity = aSourceEntity; + } + else { // If we don't have an entity, use our bounds instead + hasBounds = true; + } + + if ( !activated || multiActivate ) { + SpawnDebris(); + + SpawnFX(); + SpawnDecals(); + + // If we have a fixed duration, remove ourselves after they are removed + if ( ( duration > 0.0f ) && !multiActivate ) { + PostEventSec( &EV_Remove, duration + 1.0f ); + } + + activated = true; + } +} + + +/* +============ +hhDebrisSpawner::SpawnDebris +============ +*/ +void hhDebrisSpawner::SpawnDebris() { + const idKeyValue * kv = NULL; + idEntity * debris; + idDict args; + idStr debrisEntity; + idList debrisEntities; + int numDebris; + // For entity sources + idBounds sourceBounds; + idVec3 sourceBoundCenter; + idVec3 debrisOrigin; + idBounds searchBounds; + idVec3 debrisVelocity; + bool fit; + idBounds afBounds; //rww + bool gotAFBounds = false; //rww + idVec3 defOrigin = vec3_zero; + idAngles defAngles = ang_zero; + + // Optimization: if player is far away, don't spawn the chunks + if (!gameLocal.isMultiplayer) { + float maxDistance = spawnArgs.GetFloat("max_debris_distance"); + if (maxDistance > 0.0f) { + float distSquared = (gameLocal.GetLocalPlayer()->GetOrigin() - origin).LengthSqr(); + if (distSquared > maxDistance*maxDistance) { + return; + } + } + } + args.SetInt( "nodrop", 1 ); + args.SetVector( "origin", origin ); + args.SetFloat( "duration", duration ); + + // Pass along variabes requested + hhUtils::PassArgs( spawnArgs, args, "pass_" ); + + if (useAFBounds && sourceEntity && sourceEntity->IsType(idActor::Type)) { //rww - try for AF bounds + idActor *actor = static_cast(sourceEntity); + if (actor->IsActiveAF()) { //they are ragdolling, so we have a chance. + idPhysics_AF *afPhys = actor->GetAFPhysics(); + + if (afPhys) { //got the af physics object, now loop through the bodies and collect an appropriate bounds. + afBounds.Zero(); + + for (int bodyCount = 0; bodyCount < afPhys->GetNumBodies(); bodyCount++) { + idAFBody *b = afPhys->GetBody(bodyCount); + if (b) { + idClipModel *bodyClip = b->GetClipModel(); + if (bodyClip) { //now add the bounds of the clipModel into afBounds + idBounds bodyBounds = bodyClip->GetBounds(); + idVec3 point = (b->GetWorldOrigin()-origin); + + bodyBounds.AddPoint(point); + afBounds.AddBounds(bodyBounds); + gotAFBounds = true; + } + } + } + } + } + } + + // Find all Debris + while ( GetNextDebrisData( debrisEntities, numDebris, kv, defOrigin, defAngles ) ) { + int numEntities = debrisEntities.Num(); + for ( int count = 0; count < numDebris; ++count ) { + // Select a debris to use from the list + debrisEntity = debrisEntities[ gameLocal.random.RandomInt( numEntities ) ]; + + // If we have a bounding box to fill, use that + if ( fillBounds && ( sourceEntity || hasBounds ) ) { + if (gotAFBounds) { //rww + sourceBounds = afBounds; + //gameRenderWorld->DebugBounds(colorGreen, afBounds, origin, 1000); + } + else if ( sourceEntity ) { + sourceBounds = sourceEntity->GetPhysics()->GetBounds(); + } + else { + sourceBounds = bounds; + } + + fit = false; + for ( int i = 0; i < 4; i++ ) { + if ( i == 0 && defOrigin != vec3_zero ) { + //first try bone origin without random offset + debrisOrigin = defOrigin; + } else { + debrisOrigin = origin + hhUtils::RandomPointInBounds( sourceBounds ); + } + + if ( !testBounds || hhUtils::EntityDefWillFit( debrisEntity, debrisOrigin, defAngles.ToMat3(), CONTENTS_SOLID, NULL ) ) { + fit = true; + break; // Found a spot for the gib + } + } + + if ( !fit ) { // Gib didn't fit after 4 attempts, so don't spawn the gib + //common->Warning( "SpawnDebris: gib didn't fit when spawned (%s)\n", debrisEntity.c_str() ); + defAngles = ang_zero; + continue; + } + + args.SetVector( "origin", debrisOrigin ); + if ( sourceEntity ) { + idMat3 sourceAxis = idAngles( 0, sourceEntity->GetAxis().ToAngles().yaw, 0 ).ToMat3(); + args.SetMatrix( "rotation", defAngles.ToMat3() * sourceAxis ); + } else { + args.SetMatrix( "rotation", defAngles.ToMat3() ); + } + } + + + if ( duration ) { + args.SetFloat( "removeTime", duration + duration * gameLocal.random.CRandomFloat() ); + } + + // Spawn the object + debris = gameLocal.SpawnObject( debrisEntity, &args ); + HH_ASSERT(debris != NULL); // JRM - Nick, I added this because of a crash I got. Hopefully this will catch it sooner + + // mdl: Added this check to make sure no AFs go through the debris spawner + if ( debris->IsType( idAFEntity_Base::Type ) ) { + gameLocal.Warning( "hhDebrisSpawner spawned an articulated entity: '%s'.", debrisEntity.c_str() ); + } + + if ( nonSolid ) { + + idVec3 origin; + if ( debris->GetFloorPos( 64.0f, origin ) ) { + debris->SetOrigin( origin ); // Start on floor, since we're not going to be moving at all + debris->RunPhysics(); // Make sure any associated effects get updated before we turn collision off + } + + // Turn off collision + debris->GetPhysics()->SetContents( 0 ); + debris->GetPhysics()->SetClipMask( 0 ); + debris->GetPhysics()->UnlinkClip(); + debris->GetPhysics()->PutToRest(); + + } else { + // Add in random velocity + idVec3 randVel( gameLocal.random.RandomInt( power.x ) - power.x / 2, + gameLocal.random.RandomInt( power.y ) - power.y / 2, + gameLocal.random.RandomInt( power.z ) - power.z / 2 ); + + // Set linear velocity + debrisVelocity.x = velocity.x * power.x * 0.25; + debrisVelocity.y = velocity.y * power.y * 0.25; + debrisVelocity.z = 0.0f; + debris->GetPhysics()->SetLinearVelocity( debrisVelocity + randVel ); + + // Add random angular velocity + idVec3 aVel; + aVel.x = spawnArgs.GetFloat( "ang_vel", "90.0" ) * gameLocal.random.CRandomFloat(); + aVel.y = spawnArgs.GetFloat( "ang_vel", "90.0" ) * gameLocal.random.CRandomFloat(); + aVel.z = spawnArgs.GetFloat( "ang_vel", "90.0" ) * gameLocal.random.CRandomFloat(); + + if ( defAngles == ang_zero ) { + debris->GetPhysics()->SetAxis( idVec3( 1, 0, 0 ).ToMat3() ); + } + debris->GetPhysics()->SetAngularVelocity( aVel ); + } + } + defAngles = ang_zero; + + // Zero out the list + debrisEntities.Clear(); + } + + return; +} + + +/* +============ +hhDebrisSpawner::SpawnFX +============ +*/ +void hhDebrisSpawner::SpawnFX() { + hhFxInfo fxInfo; + + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx", origin, orientation.ToMat3(), &fxInfo ); +} + + +//============================================================================= +// +// hhDebrisSpawner::SpawnDecals +// +//============================================================================= + +void hhDebrisSpawner::SpawnDecals( void ) { + const int DIR_COUNT = 5; + idVec3 dir[DIR_COUNT] = { ( 1, 0, 0 ), ( -1, 0, 0 ), ( 0, 0, -1 ), ( 0, 1, 0 ), ( 0, -1, 0) }; + + // do blood splats + float size = spawnArgs.GetFloat( "decal_size", "96" ); + + const idKeyValue* kv = spawnArgs.MatchPrefix( "mtr_decal" ); + while( kv ) { + if( kv->GetValue().Length() ) { + gameLocal.ProjectDecal( origin, dir[ gameLocal.random.RandomInt( DIR_COUNT ) ], 64.0f, true, size, kv->GetValue().c_str() ); + } + kv = spawnArgs.MatchPrefix( "mtr_decal", kv ); + } +} + +/* +============ +hhDebrisSpawner::GetNextDebrisData +============ +*/ +bool hhDebrisSpawner::GetNextDebrisData( idList &entityDefs, int &count, const idKeyValue * &kv, idVec3 &origin, idAngles &angle ) { + static char debrisDef[] = "def_debris"; + static char debrisKey[] = "debris"; + idStr indexStr; + idStr entityDefBase; + int numDebris, minDebris, maxDebris; + origin = vec3_zero; + + // Loop through looking for the next valid debris key + for ( kv = spawnArgs.MatchPrefix( debrisDef, kv ); + kv && kv->GetValue().Length(); + kv = spawnArgs.MatchPrefix( debrisDef, kv ) ) { + indexStr = kv->GetKey(); + indexStr.Strip( debrisDef ); + + // Is a valid debris base And it isn't a variation. (ie, contains a .X postfix) + if ( idStr::IsNumeric( indexStr ) && indexStr.Find( '.' ) < 0 ) { + + // Get Number of Debris + numDebris = -1; + + if ( sourceEntity && sourceEntity->IsType( idAI::Type ) ) { + idVec3 bonePos; + idMat3 boneAxis; + idStr bone = spawnArgs.GetString( va( "%s%s%s", debrisKey, "_bone", ( const char * ) indexStr ) ); + if( !bone.IsEmpty() ) { + static_cast(sourceEntity)->GetJointWorldTransform( bone.c_str(), bonePos, boneAxis ); + origin = bonePos; + angle = spawnArgs.GetAngles( va( "%s%s%s", debrisKey, "_angle", ( const char * ) indexStr ) ); + } + } + if ( !spawnArgs.GetInt( va( "%s%s%s", debrisKey, "_num", + ( const char * ) indexStr ), + "-1", numDebris ) || numDebris < 0 ) { + // No num found, look for min and max + if ( spawnArgs.GetInt( va( "%s%s%s", debrisKey, "_min", + ( const char * ) indexStr ), + "-1", minDebris ) && minDebris >= 0 && + spawnArgs.GetInt( va( "%s%s%s", debrisKey, "_max", + ( const char * ) indexStr ), + "-1", maxDebris ) && maxDebris >= 0 ) { + numDebris = + gameLocal.random.RandomInt( ( maxDebris - minDebris ) ) + minDebris; + } //. No min/max found + + } //. No num found + + // No valid num found + if ( numDebris < 0 ) { + gameLocal.Warning( "ERROR: No debris num could be be found for %s%s", + ( const char * ) debrisDef, + ( const char * ) indexStr ); + } + else { // Valid num found + const char * entityDefPrefix; + const idKeyValue * otherDefKey = NULL; + + entityDefBase = kv->GetValue(); + count = numDebris; + + entityDefs.Append( entityDefBase ); + + // Find the other defs that may be there. using .X scheme + entityDefPrefix = va( "%s.", (const char *) kv->GetKey() ); + + // gameLocal.Printf( "Looking for %s\n", entityDefPrefix ); + + for ( otherDefKey = spawnArgs.MatchPrefix( entityDefPrefix, otherDefKey ); + otherDefKey && otherDefKey->GetValue().Length(); + otherDefKey = spawnArgs.MatchPrefix( entityDefPrefix, otherDefKey ) ) { + // gameLocal.Printf( "Would have added %s\n", (const char *) otherDefKey->GetValue() ); + entityDefs.Append( otherDefKey->GetValue() ); + } + + return( true ); + } + + } //. Valid debris base + } //. debris loop + + + return( false ); +} + diff --git a/src/Prey/game_debrisspawner.h b/src/Prey/game_debrisspawner.h new file mode 100644 index 0000000..0aaec89 --- /dev/null +++ b/src/Prey/game_debrisspawner.h @@ -0,0 +1,47 @@ +#ifndef __PREY_GAME_DEBRISMASS_H__ +#define __PREY_GAME_DEBRISMASS_H__ + +class hhDebrisSpawner : public idEntity { + CLASS_PROTOTYPE( hhDebrisSpawner ); + + public: + hhDebrisSpawner(); + virtual ~hhDebrisSpawner(); + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Activate( idEntity *sourceEntity ); + + ID_INLINE float GetDuration() const { return duration; } + + protected: + void Event_RemoveAll(); + + void SpawnDebris(); + void SpawnFX(); + void SpawnDecals( void ); + + bool GetNextDebrisData( idList &entityDefs, int &count, + const idKeyValue * &kv, idVec3 &origin, idAngles &angle ); +protected: + idVec3 origin; + idVec3 orientation; + idVec3 velocity; + idVec3 power; + bool activated; + bool multiActivate; + bool useEntity; // Use an entity to spawn in + bool useAFBounds; //rww - use collective AF bodies to produce an appropriate bounds for spawning. + bool hasBounds; + idBounds bounds; + // Vars for removing everything after a fixed period + float duration; + + bool fillBounds; // Try to fill in the bounds with the gibs. + bool testBounds; // test to see if debris fits in bounds + bool nonSolid; + + idEntity * sourceEntity; +}; + +#endif __PREY_GAME_DEBRISMASS_H__ diff --git a/src/Prey/game_dock.cpp b/src/Prey/game_dock.cpp new file mode 100644 index 0000000..4a959a9 --- /dev/null +++ b/src/Prey/game_dock.cpp @@ -0,0 +1,70 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//----------------------------------------------------------------------- +// +// hhDock +// +//----------------------------------------------------------------------- +const idEventDef EV_DockLock("lockVehicleInDock", NULL); +const idEventDef EV_DockUnlock("unlockDock", NULL); + +ABSTRACT_DECLARATION(idEntity, hhDock) + EVENT( EV_DockLock, hhDock::Event_Lock) + EVENT( EV_DockUnlock, hhDock::Event_Unlock) + EVENT( EV_Activate, hhDock::Event_Activate) + EVENT( EV_PostSpawn, hhDock::Event_PostSpawn ) +END_CLASS + +void hhDock::Spawn() { + if ( !gameLocal.isClient ) { + PostEventMS(&EV_PostSpawn, 0); + } +} + +void hhDock::Event_PostSpawn() { + dockingZone = SpawnDockingZone(); +} + +void hhDock::Save(idSaveGame *savefile) const { + dockingZone.Save(savefile); +} + +void hhDock::Restore( idRestoreGame *savefile ) { + dockingZone.Restore(savefile); +} + +hhDockingZone *hhDock::SpawnDockingZone() { + hhDockingZone *zone; + idBounds localBounds; + + // Define our bounds + localBounds[0] = spawnArgs.GetVector("dockingzonemins"); + localBounds[1] = spawnArgs.GetVector("dockingzonemaxs"); + + // create a docking zone with this size + idDict args; + args.SetVector( "origin", GetOrigin() ); + args.SetVector( "mins", localBounds[0] ); + args.SetVector( "maxs", localBounds[1] ); + args.SetMatrix( "rotation", GetAxis() ); + zone = (hhDockingZone *)gameLocal.SpawnEntityType(hhDockingZone::Type, &args); + assert(zone); + zone->Bind(this, true); + zone->RegisterDock(this); + return zone; +} + +void hhDock::Event_Lock() { + Lock(); +} + +void hhDock::Event_Unlock() { + Unlock(); +} + +void hhDock::Event_Activate( idEntity *activator ) { + Unlock(); +} \ No newline at end of file diff --git a/src/Prey/game_dock.h b/src/Prey/game_dock.h new file mode 100644 index 0000000..197e739 --- /dev/null +++ b/src/Prey/game_dock.h @@ -0,0 +1,49 @@ +#ifndef __GAME_DOCK_H__ +#define __GAME_DOCK_H__ + +extern const idEventDef EV_DockLock; +extern const idEventDef EV_DockUnlock; + +class hhShuttle; +class hhDockingZone; + +class hhDock : public idEntity { +public: + ABSTRACT_PROTOTYPE( hhDock ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool ValidEntity(idEntity *ent) = 0; + virtual void EntityEntered( idEntity *ent ) = 0; + virtual void EntityEncroaching( idEntity *ent ) = 0; + virtual void EntityLeaving( idEntity *ent ) = 0; + virtual void ShuttleExit( hhShuttle *shuttle ) = 0; + virtual bool IsLocked() = 0; + virtual bool CanExitLocked() = 0; + virtual bool AllowsBoost() = 0; + virtual bool AllowsFiring() = 0; + virtual bool AllowsExit() = 0; + virtual bool IsTeleportDest() = 0; + virtual void UpdateAxis( const idMat3 &newAxis ) = 0; + + virtual bool Recharges() const { return false; } + +protected: + //rww - needs a delay for mp + virtual void Event_PostSpawn(); + + hhDockingZone* SpawnDockingZone(); + virtual void Lock()=0; + virtual void Unlock()=0; + + void Event_Lock(); + void Event_Unlock(); + void Event_Activate(idEntity *activator); + +protected: + idEntityPtr dockingZone; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_dockedgun.cpp b/src/Prey/game_dockedgun.cpp new file mode 100644 index 0000000..925d7cc --- /dev/null +++ b/src/Prey/game_dockedgun.cpp @@ -0,0 +1,16 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// +// hhDockedGun +// +//========================================================================== + +CLASS_DECLARATION(hhShuttle, hhDockedGun) +END_CLASS + +void hhDockedGun::Spawn() { +} diff --git a/src/Prey/game_dockedgun.h b/src/Prey/game_dockedgun.h new file mode 100644 index 0000000..7679826 --- /dev/null +++ b/src/Prey/game_dockedgun.h @@ -0,0 +1,19 @@ +#ifndef __GAME_DOCKEDGUN_H__ +#define __GAME_DOCKEDGUN_H__ + +//========================================================================== +// +// hhDockedGun +// +//========================================================================== + +class hhDockedGun : public hhShuttle { + CLASS_PROTOTYPE( hhDockedGun ); + +public: + void Spawn(); + +protected: +}; + +#endif diff --git a/src/Prey/game_door.cpp b/src/Prey/game_door.cpp new file mode 100644 index 0000000..a255211 --- /dev/null +++ b/src/Prey/game_door.cpp @@ -0,0 +1,586 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idDoor, hhDoor ) + EVENT( EV_Mover_ReturnToPos1, hhDoor::Event_ReturnToPos1 ) + EVENT( EV_ReachedPos, hhDoor::Event_Reached_BinaryMover ) + EVENT( EV_SetBuddiesShaderParm, hhDoor::Event_SetBuddiesShaderParm ) + EVENT( EV_Touch, hhDoor::Event_Touch ) +END_CLASS + +//-------------------------------- +// hhDoor::Spawn +//-------------------------------- +void hhDoor::Spawn() { + airlockMaster = NULL; + + airlockTeam.SetOwner( this ); + airlockTeamName = spawnArgs.GetString( "airlockTeam" ); + spawnArgs.GetFloat( "airlockwait", "3", airLockSndWait ); + airLockSndWait *= 1000.0f; // Convert from seconds to ms + + idEntity* master = DetermineTeamMaster( GetAirLockTeamName() ); + if( master ) { + airlockMaster = static_cast( master ); + if( airlockMaster != this ) { + JoinAirLockTeam( airlockMaster ); + } + } + + if( spawnArgs.GetBool("start_open") ) { + VerifyAirlockTeamStatus(); + } + + if( moveMaster != this ) { + CopyTeamInfoToMoveMaster( static_cast(moveMaster) ); + } + + // We only open when dead if we have health to start with + openWhenDead = health > 0; + forcedOpen = false; + nextAirLockSnd = 0; + + bShuttleDoors = spawnArgs.GetBool( "shuttle_doors" ); +} + +void hhDoor::Save(idSaveGame *savefile) const { + savefile->WriteString(airlockTeamName); + savefile->WriteObject(airlockMaster); + savefile->WriteBool(openWhenDead); + savefile->WriteBool(forcedOpen); + savefile->WriteFloat(airLockSndWait); +} + +void hhDoor::Restore( idRestoreGame *savefile ) { + savefile->ReadString(airlockTeamName); + savefile->ReadObject( reinterpret_cast(airlockMaster) ); + savefile->ReadBool(openWhenDead); + savefile->ReadBool(forcedOpen); + savefile->ReadFloat(airLockSndWait); + + airlockTeam.SetOwner( this ); + if( airlockMaster && airlockMaster != this ) { + JoinAirLockTeam( airlockMaster ); + } + + nextAirLockSnd = 0; + + bShuttleDoors = spawnArgs.GetBool( "shuttle_doors" ); +} + +//-------------------------------- +// hhDoor::~hhDoor +//-------------------------------- +hhDoor::~hhDoor() { + airlockTeam.Remove(); +} + +//-------------------------------- +// hhDoor::Killed +//-------------------------------- +void hhDoor::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + + // Only can really be killed if we can be damaged + if ( openWhenDead ) { + Open(); + + fl.takedamage = false; + forcedOpen = true; + } +} // Killed( idEntity *, idEntity *, int, const idVec3 &, int ) + +//-------------------------------- +// hhDoor::Use_BinaryMover +//-------------------------------- +void hhDoor::Use_BinaryMover( idEntity *activator ) { + if ( airlockMaster && activator && !activator->IsType( idPlayer::Type ) ) { + return; // Don't allow anyone but the player to affect airlock doors + } + + // only the master should be used + if ( moveMaster != this ) { + moveMaster->Use_BinaryMover( activator ); + return; + } + + if ( !enabled ) { + return; + } + + if ( moverState == MOVER_POS1 || moverState == MOVER_2TO1 ) { + GotoPosition2(); + } + else if ( moverState == MOVER_POS2 || moverState == MOVER_1TO2 ) { + GotoPosition1(); + } +} + +//-------------------------------- +// hhDoor::GotoPosition1 +// +// CloseDoor +//-------------------------------- +void hhDoor::GotoPosition1() { + GotoPosition1( spawnArgs.GetBool("toggle") ? 0.0f : wait ); +} + +//-------------------------------- +// hhDoor::GotoPosition1 +// +// CloseDoor +//-------------------------------- +void hhDoor::GotoPosition1( float wait ) { + idMover_Binary* slave = NULL; + int partial = 0; + + //HUMANHEAD: aob - airlock stuff + if( !CanClose() ) { + return; + } + //HUMNAHEAD END + + if ( moverState == MOVER_POS2 ) { + SetGuiStates( guiBinaryMoverStates[MOVER_2TO1] ); + + CancelReturnToPos1(); + + if( wait > 0 ) { + //HUMANHEAD: aob + PostEventSec( &EV_Mover_ReturnToPos1, wait ); + //HUMANHEAD END + } else { + ProcessEvent( &EV_Mover_ReturnToPos1 ); + } + } + + else + + // only partway up before reversing + if ( moverState == MOVER_1TO2 ) { + // use the physics times because this might be executed during the physics simulation + partial = physicsObj.GetLinearEndTime() - physicsObj.GetTime(); + MatchActivateTeam( MOVER_2TO1, physicsObj.GetTime() - partial ); + } +} + +//-------------------------------- +// hhDoor::GotoPosition2 +// +// OpenDoor +//-------------------------------- +void hhDoor::GotoPosition2() { + GotoPosition2( 0.0f ); +} + +//-------------------------------- +// hhDoor::GotoPosition2 +// +// OpenDoor +//-------------------------------- +void hhDoor::GotoPosition2( float wait ) { + int partial = 0; + hhDoor *airlockMaster = GetAirLockMaster(); + + //HUMANHEAD: aob - airlock stuff + if( !CanOpen() && airlockMaster ) { + ForceAirLockTeamClosed(); + if( gameLocal.time > nextAirLockSnd ) { + int length; + StartSound( "snd_airlock", SND_CHANNEL_BODY, 0, false, &length ); + nextAirLockSnd = gameLocal.time + length + airLockSndWait; + } + return; + } + //HUMNAHEAD END + + if ( moverState == MOVER_POS1 ) { + ActivatePrefixed("triggerStartOpen", GetActivator()); + + SetGuiStates( guiBinaryMoverStates[MOVER_1TO2] ); + + MatchActivateTeam( MOVER_1TO2, gameLocal.time ); + + // open areaportal + ProcessEvent( &EV_Mover_OpenPortal ); + } + else if ( moverState == MOVER_2TO1 ) { // only partway up before reversing + // use the physics times because this might be executed during the physics simulation + partial = physicsObj.GetLinearEndTime() - physicsObj.GetTime(); + MatchActivateTeam( MOVER_1TO2, physicsObj.GetTime() - partial ); + } + + if( airlockMaster ) { + // Mark the other team members as locked + for( hhDoor* node = airlockMaster->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + + // Skip this door and it's buddies + if( node == this || buddyNames.Find( node->name ) ) { + continue; + } + + // Don't change doors that are really locked + if( node->IsLocked() ) { + continue; + } + + // Mark as locked + node->SetBuddiesShaderParm( SHADERPARM_MISC, 1.0f ); + } + + // This prevents the our shader from being set as locked from the player moving too quickly between airlock doors + SetBuddiesShaderParm( SHADERPARM_MISC, 0.0f ); + } +} + +//-------------------------------- +// hhDoor::Open +//-------------------------------- +void hhDoor::Open( void ) { + GotoPosition2( wait ); +} + +//-------------------------------- +// hhDoor::Close +//-------------------------------- +void hhDoor::Close( void ) { + // HUMANHEAD nla + if ( ForcedOpen() ) { return; } + // HUMANHEAD + + GotoPosition1( 0.0f ); +} + +//-------------------------------- +// hhDoor::CanOpen +//-------------------------------- +bool hhDoor::CanOpen() const { + if( !GetAirLockMaster() ) { + return true; + } + + for( hhDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node == this ) { + continue; + } + + if( OnMyMoveTeam(node) ) { + continue; + } + + if( node->IsOpen() ) { + return false; + } + } + + return true; +} + +//-------------------------------- +// hhDoor::CanClose +//-------------------------------- +bool hhDoor::CanClose() const { + return true; +} + +//-------------------------------- +// hhDoor::OnMyMoveTeam +//-------------------------------- +bool hhDoor::OnMyMoveTeam( hhDoor* doorPiece ) const { + for( const idMover_Binary* localDoorPiece = this; localDoorPiece != NULL; localDoorPiece = localDoorPiece->GetActivateChain() ) { + if( localDoorPiece == doorPiece ) { + return true; + } + } + + return false; +} + +//-------------------------------- +// hhDoor::CopyTeamInfoToMoveMaster +//-------------------------------- +void hhDoor::CopyTeamInfoToMoveMaster( hhDoor* master ) { + if( !master ) { + return; + } + + for( int ix = buddies.Num() - 1; ix >= 0; --ix ) { + master->buddies.AddUnique( buddies[ix] ); + } + + for( int ix = buddyNames.Num() - 1; ix >= 0; --ix ) { + master->buddyNames.AddUnique( buddyNames[ix] ); + } +} + +//-------------------------------- +// hhDoor::DetermineTeamMaster +//-------------------------------- +idEntity* hhDoor::DetermineTeamMaster( const char* teamName ) { + idEntity* ent = NULL; + + if ( teamName && teamName[0] ) { + // find the first entity spawned on this team (which could be us) + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if (ent->IsType(hhModelDoor::Type) && !idStr::Icmp( static_cast(ent)->GetAirLockTeamName(), teamName )) { + return ent; + } + if (ent->IsType(hhDoor::Type) && !idStr::Icmp( static_cast(ent)->GetAirLockTeamName(), teamName )) { + return ent; + } + } + } + + return NULL; +} + +//-------------------------------- +// hhDoor::JoinAirLockTeam +//-------------------------------- +void hhDoor::JoinAirLockTeam( hhDoor *master ) { + assert( master ); + + airlockTeam.AddToEnd( master->airlockTeam ); +} + +//-------------------------------- +// hhDoor::VerifyAirlockTeamStatus +//-------------------------------- +void hhDoor::VerifyAirlockTeamStatus() { + if( !GetAirLockMaster() ) { + return; + } + + for( hhDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node == this ) { + continue; + } + + if( OnMyMoveTeam(node) ) { + continue; + } + + if( !node->IsClosed() ) { + gameLocal.Warning( "Airlock team '%s' has more than one member starting open", GetAirLockTeamName() ); + } + } +} + +//-------------------------------- +// hhDoor::IsClosed +//-------------------------------- +bool hhDoor::IsClosed() const { + return ( moverState == MOVER_POS1 ); +} + +//-------------------------------- +// hhDoor::ForceAirLockTeamClosed +//-------------------------------- +void hhDoor::ForceAirLockTeamClosed() { + hhDoor* localMoveMaster = NULL; + + if( !GetAirLockMaster() ) { + return; + } + + for( hhDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node == this ) { + continue; + } + + if( !node->moveMaster->IsType(hhDoor::Type) ) { + continue; + } + + localMoveMaster = static_cast( node->moveMaster ); + //if( node != localMoveMaster ) { + // continue; + //} + + localMoveMaster->CancelReturnToPos1(); + if( !localMoveMaster->IsClosed() ) { + localMoveMaster->Close(); + } + } +} + +//-------------------------------- +// hhDoor::ForceAirLockTeamOpen +//-------------------------------- +void hhDoor::ForceAirLockTeamOpen() { + hhDoor* localMoveMaster = NULL; + + if( !GetAirLockMaster() ) { + return; + } + + for( hhDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node == this ) { + continue; + } + + if( !node->moveMaster->IsType(hhDoor::Type) ) { + continue; + } + + localMoveMaster = static_cast( node->moveMaster ); + //if( node != localMoveMaster ) { + // continue; + //} + + localMoveMaster->CancelReturnToPos1(); + if( !localMoveMaster->IsOpen() ) { + localMoveMaster->Open(); + } + } +} + +//-------------------------------- +// hhDoor::CancelReturnToPos1 +//-------------------------------- +void hhDoor::CancelReturnToPos1() { + for( idMover_Binary* slave = this; slave != NULL; slave = slave->GetActivateChain() ) { + slave->CancelEvents( &EV_Mover_ReturnToPos1 ); + } +} + +//-------------------------------- +// hhDoor::SetBuddiesShaderParm +//-------------------------------- +void hhDoor::SetBuddiesShaderParm( int parm, float value ) { + idEntity* buddy = NULL; + + for( int ix = buddyNames.Num() - 1; ix >= 0; --ix ) { + if( !buddyNames[ix].Length() ) { + continue; + } + + buddy = gameLocal.FindEntity( buddyNames[ix].c_str() ); + if( !buddy ) { + continue; + } + + buddy->SetShaderParm( parm, value ); + } +} + +//-------------------------------- +// hhDoor::ToggleBuddiesShaderParm +//-------------------------------- +void hhDoor::ToggleBuddiesShaderParm( int parm, float firstValue, float secondValue, float toggleDelay) { + SetBuddiesShaderParm( parm, firstValue ); + PostEventMS( &EV_SetBuddiesShaderParm, toggleDelay, parm, secondValue ); +} + +//-------------------------------- +// hhDoor::Event_SetBuddiesShaderParm +//-------------------------------- +void hhDoor::Event_SetBuddiesShaderParm( int parm, float value ) { + SetBuddiesShaderParm( parm, value ); +} + +//-------------------------------- +// hhDoor::Event_ReturnToPos1 +//-------------------------------- +void hhDoor::Event_ReturnToPos1( void ) { + + if ( !ForcedOpen() ) { + idDoor::Event_ReturnToPos1(); + } + +} // Event_ReturnToPos1() + +//-------------------------------- +// hhDoor::Event_Reached_BinaryMover +//-------------------------------- +void hhDoor::Event_Reached_BinaryMover( void ) { + if ( moverState == MOVER_2TO1 ) { + SetBlocked(false); + ActivatePrefixed("triggerClosed", this); + } else if (moverState == MOVER_1TO2) { + ActivatePrefixed("triggerOpened", this); + } + + if ( moverState == MOVER_1TO2 ) { + // reached pos2 + idThread::ObjectMoveDone( move_thread, this ); + move_thread = 0; + + // HUMANHEAD aob - removed + //if ( moveMaster == this ) { + //StartSound( "snd_opened", SND_CHANNEL_ANY ); + //} + //HUMANHEAD END + + SetMoverState( MOVER_POS2, gameLocal.time ); + + SetGuiStates( guiBinaryMoverStates[MOVER_POS2] ); + + UpdateBuddies(1); + + if( enabled && wait >= 0 && !spawnArgs.GetBool("toggle") ) { + // return to pos1 after a delay + //HUMANHEAD: aob + CancelReturnToPos1(); + //HUMANHEAD END + PostEventSec( &EV_Mover_ReturnToPos1, wait ); + } + + // fire targets + ActivateTargets( moveMaster->GetActivator() ); + } else if ( moverState == MOVER_2TO1 ) { + // reached pos1 + idThread::ObjectMoveDone( move_thread, this ); + move_thread = 0; + + SetMoverState( MOVER_POS1, gameLocal.time ); + + SetGuiStates( guiBinaryMoverStates[MOVER_POS1] ); + + UpdateBuddies(0); + + // close areaportals + if ( moveMaster == this ) { + ProcessEvent( &EV_Mover_ClosePortal ); + } + + if( GetAirLockMaster() ) { + // Mark the other team members as unlocked + for( hhDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + + // Skip this door and it's buddies + if( node == this || buddyNames.Find( node->name ) ) { + continue; + } + + // Don't change doors that are really locked + if( node->IsLocked() ) { + continue; + } + + // Mark as unlocked when door closes + node->SetBuddiesShaderParm( SHADERPARM_MISC, 0.0f ); + } + } + } else { + gameLocal.Error( "Event_Reached_BinaryMover: bad moverState" ); + } +} + +/* +================ +hhDoor::Event_Touch +================ +*/ +void hhDoor::Event_Touch( idEntity *other, trace_t *trace ) { + if ( bShuttleDoors && ( ! other->IsType( idActor::Type ) || ! reinterpret_cast ( other )->InVehicle() ) ) { + if ( gameLocal.time > nextSndTriggerTime ) { + StartSound( "snd_locked", SND_CHANNEL_ANY, 0, false, NULL ); + nextSndTriggerTime = gameLocal.time + 10000; + } + return; + } + + idDoor::Event_Touch( other, trace ); +} diff --git a/src/Prey/game_door.h b/src/Prey/game_door.h new file mode 100644 index 0000000..80ce90d --- /dev/null +++ b/src/Prey/game_door.h @@ -0,0 +1,69 @@ +#ifndef __PREY_DOOR_H__ +#define __PREY_DOOR_H__ + +class hhDoor : public idDoor { + CLASS_PROTOTYPE( hhDoor ); + +public: + void Spawn(); + virtual ~hhDoor(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Use_BinaryMover( idEntity *activator ); + virtual void GotoPosition1(); + virtual void GotoPosition2(); + + //HUMANHEAD: aob + virtual void GotoPosition1( float wait ); + virtual void GotoPosition2( float wait ); + //HUMANHEAD END + + ID_INLINE const char* GetAirLockTeamName() const { return airlockTeamName.c_str(); } + ID_INLINE hhDoor* GetAirLockMaster() const { return airlockMaster; } + void Open(); + void Close(); + bool CanOpen() const; + bool CanClose() const; + bool IsClosed() const; + + bool OnMyMoveTeam( hhDoor* doorPiece ) const; + void CopyTeamInfoToMoveMaster( hhDoor* master ); + + void CancelReturnToPos1(); + + void SetBuddiesShaderParm( int parm, float value ); + void ToggleBuddiesShaderParm( int parm, float firstValue, float secondValue, float toggleDelay ); + + bool ForcedOpen() const { return( forcedOpen ); }; + void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + +protected: + idEntity* DetermineTeamMaster( const char* teamName ); + void JoinAirLockTeam( hhDoor *master ); + void VerifyAirlockTeamStatus(); + + void ForceAirLockTeamClosed(); + void ForceAirLockTeamOpen(); + +protected: + void Event_ReturnToPos1( void ); // overridden to stop it moving when closed + void Event_SetBuddiesShaderParm( int parm, float value ); + void Event_Reached_BinaryMover( void ); + virtual void Event_Touch( idEntity *other, trace_t *trace ); + +public: + idLinkList airlockTeam; + +protected://More airLock stuff + idStr airlockTeamName; + hhDoor* airlockMaster; + + bool openWhenDead; // Do we open when dead? + bool forcedOpen; // Is the door forced open + float airLockSndWait; + float nextAirLockSnd; + bool bShuttleDoors; +}; + +#endif diff --git a/src/Prey/game_eggspawner.cpp b/src/Prey/game_eggspawner.cpp new file mode 100644 index 0000000..065f5d2 --- /dev/null +++ b/src/Prey/game_eggspawner.cpp @@ -0,0 +1,281 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// +// hhEggSpawner +// +// When activated, shoots out an egg along it's X axis +//========================================================================== + +#define MAX_HATCH_TIME (SEC2MS(10)) + +CLASS_DECLARATION(hhAnimatedEntity, hhEggSpawner) + EVENT( EV_PlayIdle, hhEggSpawner::Event_PlayIdle) + EVENT( EV_Activate, hhEggSpawner::Event_Activate) +END_CLASS + +void hhEggSpawner::Spawn(void) { + GetPhysics()->SetContents( CONTENTS_SOLID ); + fl.takedamage = true; + + idleAnim = GetAnimator()->GetAnim("idle"); + hatchAnim = GetAnimator()->GetAnim("launch"); + painAnim = GetAnimator()->GetAnim("pain"); + + PostEventMS(&EV_PlayIdle, 0); +} + +void hhEggSpawner::Save(idSaveGame *savefile) const { + savefile->WriteInt( idleAnim ); + savefile->WriteInt( hatchAnim ); + savefile->WriteInt( painAnim ); +} + +void hhEggSpawner::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( idleAnim ); + savefile->ReadInt( hatchAnim ); + savefile->ReadInt( painAnim ); +} + +void hhEggSpawner::SpawnEgg(idEntity *activator) { + idVec3 dir = GetPhysics()->GetAxis()[0]; + idVec3 offset = spawnArgs.GetVector("offset_spawn"); + float power = spawnArgs.GetFloat("power"); + + // Play anim + if (hatchAnim) { + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, hatchAnim, gameLocal.time, 0); + int ms = GetAnimator()->GetAnim( hatchAnim )->Length(); + PostEventMS(&EV_PlayIdle, ms); + } + + idDict args; + args.Clear(); + args.SetVector( "origin", GetPhysics()->GetOrigin() + offset * GetPhysics()->GetAxis() ); + hhEgg *egg = static_cast(gameLocal.SpawnObject(spawnArgs.GetString("def_egg"), &args)); + if (egg) { + egg->GetPhysics()->SetLinearVelocity(dir * power); + egg->SetActivator(activator); + + // Copy our targets + for ( int i = 0; i < targets.Num(); i++ ) { + egg->targets.AddUnique(targets[i]); + } + } + + StartSound( "snd_spawn", SND_CHANNEL_ANY, 0, true, NULL ); +} + +void hhEggSpawner::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + // Don't actually take damage, but give feedback + if (painAnim) { + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, painAnim, gameLocal.time, 0); + int ms = GetAnimator()->GetAnim( painAnim )->Length(); + PostEventMS(&EV_PlayIdle, ms); + } +} + +void hhEggSpawner::Event_PlayIdle() { + if (idleAnim) { + GetAnimator()->ClearAllAnims(gameLocal.time, 200); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleAnim, gameLocal.time, 200); + } +} + +void hhEggSpawner::Event_Activate(idEntity *activator) { + SpawnEgg(activator); +} + + +//========================================================================== +// +// hhEgg +// +// Moveable that spawns a creature on impact +//========================================================================== + +const idEventDef EV_Hatch(""); + +CLASS_DECLARATION(hhMoveable, hhEgg) + EVENT( EV_Activate, hhEgg::Event_Activate) + EVENT( EV_Hatch, hhEgg::Event_Hatch) +END_CLASS + +void hhEgg::Spawn(void) { + enemy = NULL; + bHatched = false; + bHatching = false; + const char *tableName = spawnArgs.GetString("table_hatch"); + table = static_cast(declManager->FindType( DECL_TABLE, tableName, true )); + hatchTime = -1.0f; + + PostEventSec( &EV_Activate, spawnArgs.GetFloat("secondsBeforeHatch"), this ); +} + +void hhEgg::Save(idSaveGame *savefile) const { + + savefile->WriteBool( bHatching ); + savefile->WriteBool( bHatched ); + + savefile->WriteFloat( deformAlpha.GetStartTime() ); // idInterpolate + savefile->WriteFloat( deformAlpha.GetDuration() ); + savefile->WriteFloat( deformAlpha.GetStartValue() ); + savefile->WriteFloat( deformAlpha.GetEndValue() ); + savefile->WriteFloat( hatchTime ); + + enemy.Save(savefile); +} + +void hhEgg::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadBool( bHatching ); + savefile->ReadBool( bHatched ); + + savefile->ReadFloat( set ); // idInterpolate + deformAlpha.SetStartTime( set ); + savefile->ReadFloat( set ); + deformAlpha.SetDuration( set ); + savefile->ReadFloat( set ); + deformAlpha.SetStartValue(set); + savefile->ReadFloat( set ); + deformAlpha.SetEndValue( set ); + + savefile->ReadFloat( hatchTime ); + + const char *tableName = spawnArgs.GetString("table_hatch"); + table = static_cast(declManager->FindType( DECL_TABLE, tableName, true )); + + enemy.Restore(savefile); +} + + +void hhEgg::SetActivator(idEntity *activator) { + if (activator && activator->IsType(idActor::Type)) { + enemy = static_cast(activator); + } +} + +void hhEgg::Ticker() { + if (bHatching) { + // over time, send different parms into deformation + if (deformAlpha.IsDone(gameLocal.time)) { + Hatch(); + } + else { + float alpha = deformAlpha.GetCurrentValue(gameLocal.time); + float value = table->TableLookup(alpha); + SetShaderParm(SHADERPARM_ANY_DEFORM, DEFORMTYPE_VIBRATE); + SetShaderParm(SHADERPARM_ANY_DEFORM_PARM1, value); + } + } +} + +bool hhEgg::Collide( const trace_t &collision, const idVec3 &velocity ) { + AttemptToPlayBounceSound( collision, velocity ); + + if (spawnArgs.GetBool("spawnOnImpact")) { //obs + Hatch(); + return true; + } + return false; +} + +void hhEgg::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) { + Hatch(); +} + +bool hhEgg::SpawnHatchling(const char *monsterName, const idVec3 &spawnLocation) { + idDict args; + args.Clear(); + args.SetVector( "origin", spawnLocation ); + args.SetFloat( "angle", GetAxis().ToAngles().yaw ); + args.Set( "trigger_anim", "birth" ); + hhMonsterAI *monster = static_cast(gameLocal.SpawnObject(monsterName, &args)); + + if (monster) { + // Set enemy + if (enemy.IsValid()) { + monster->SetEnemy(enemy.GetEntity()); + } + + // Copy our targets + for ( int i = 0; i < targets.Num(); i++ ) { + monster->targets.AddUnique(targets[i]); + } + + monster->PostEventSec( &EV_Activate, 0, this ); + } + + return monster != NULL; +} + +void hhEgg::Hatch() { + if (!bHatched) { + if (hatchTime == -1.0f) { + hatchTime = gameLocal.time; + } + // Spawn hatchling + const char *monsterName = spawnArgs.GetString("def_hatchling", NULL); + if (monsterName) { + idVec3 spawnLocation = GetPhysics()->GetOrigin(); + + const float hatchOffsetDistance = 50.0f; + bool spawned = false; + idVec3 offset; + idVec3 offsetLocation; + trace_t trace; + memset(&trace, 0, sizeof(trace_t)); + + if (hhUtils::EntityDefWillFit(monsterName, spawnLocation, mat3_identity, MASK_MONSTERSOLID, this)) { + //gameRenderWorld->DebugArrow(colorGreen, spawnLocation, offsetLocation, 10, 10000); + spawned = SpawnHatchling(monsterName, spawnLocation); + } + + if (!spawned) { // Creature wouldn't fit, try again later + CancelEvents(&EV_Hatch); + if (gameLocal.time - hatchTime > MAX_HATCH_TIME) { + // Give up after 15 seconds of struggling + ActivateTargets(this); + // Spawn gibs from monster + const idDict *dict = gameLocal.FindEntityDefDict(monsterName, false); + if (dict && dict->FindKey("def_gibdebrisspawner")) { + GetPhysics()->SetLinearVelocity( vec3_zero ); //HUMANHEAD jsh PCF zero out velocity to prevent debris huge translation + hhUtils::SpawnDebrisMass(dict->GetString("def_gibdebrisspawner"), this); + } + } else { + //idVec3 dir = hhUtils::RandomVector(); + //gameRenderWorld->DebugArrow(colorGreen, spawnLocation, spawnLocation + dir * 50.0f, 10, 250); + GetPhysics()->AddForce(0, GetPhysics()->GetOrigin(), hhUtils::RandomVector() * idMath::ClampFloat(0x1000000, 0x100000000, (0x1000000 * ((gameLocal.time - hatchTime) / 25.0f)))); + PostEventMS(&EV_Hatch, 250); + return; + } + } + } + + hatchTime = -1.0f; + bHatched = true; + GetPhysics()->SetContents(0); + PostEventMS(&EV_Remove, 0); + + StartSound( "snd_hatch", SND_CHANNEL_ANY, 0, true, NULL ); + GetPhysics()->SetLinearVelocity( vec3_zero ); //HUMANHEAD jsh PCF zero out velocity to prevent debris huge translation + hhUtils::SpawnDebrisMass(spawnArgs.GetString("def_afterbirth"), this); + } +} + +void hhEgg::Event_Activate(idEntity *activator) { + if (!bHatching) { + bHatching = true; + int msForTable = SEC2MS(spawnArgs.GetFloat("secondsToHatch")); + deformAlpha.Init(gameLocal.time, msForTable, 0.0f, 1.0f); + BecomeActive(TH_TICKER); + } +} + +void hhEgg::Event_Hatch(void) { + Hatch(); +} diff --git a/src/Prey/game_eggspawner.h b/src/Prey/game_eggspawner.h new file mode 100644 index 0000000..e2a350c --- /dev/null +++ b/src/Prey/game_eggspawner.h @@ -0,0 +1,53 @@ + +#ifndef __GAME_EGGSPAWNER_H__ +#define __GAME_EGGSPAWNER_H__ + +class hhEggSpawner : public hhAnimatedEntity { + CLASS_PROTOTYPE( hhEggSpawner ); +public: + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void SpawnEgg(idEntity *activator); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + +protected: + void Event_Activate(idEntity *activator); + void Event_PlayIdle(); + +protected: + int idleAnim; + int hatchAnim; + int painAnim; +}; + + +class hhEgg : public hhMoveable { + CLASS_PROTOTYPE( hhEgg ); +public: + virtual bool Collide( const trace_t &collision, const idVec3 &velocity ); + virtual void Ticker(); + virtual void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Hatch(); + void SetActivator(idEntity *activator); + +protected: + bool SpawnHatchling(const char *monsterName, const idVec3 &spawnLocation); + void Event_Activate(idEntity *activator); + void Event_Hatch(); + +protected: + bool bHatching; + bool bHatched; + idInterpolate deformAlpha; + const idDeclTable * table; + idEntityPtr enemy; + float hatchTime; +}; + +#endif diff --git a/src/Prey/game_energynode.cpp b/src/Prey/game_energynode.cpp new file mode 100644 index 0000000..89a2aa8 --- /dev/null +++ b/src/Prey/game_energynode.cpp @@ -0,0 +1,126 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_EnableNode( "enableNode", NULL ); +const idEventDef EV_DisableNode( "disableNode", NULL ); + +CLASS_DECLARATION( idStaticEntity, hhEnergyNode ) + EVENT( EV_EnableNode, hhEnergyNode::Event_Enable ) + EVENT( EV_DisableNode, hhEnergyNode::Event_Disable ) +END_CLASS + +hhEnergyNode::hhEnergyNode(void) +:disabled(false) +{} + +hhEnergyNode::~hhEnergyNode(void) { + Event_Disable(); +} + + +void hhEnergyNode::Save(idSaveGame *savefile) const { + savefile->WriteBool( disabled ); + savefile->WriteVec3( leechPoint ); + + energyFx.Save( savefile ); +} + +void hhEnergyNode::Restore( idRestoreGame *savefile ) { + savefile->ReadBool( disabled ); + savefile->ReadVec3( leechPoint ); + + energyFx.Restore( savefile ); +} + +void hhEnergyNode::Spawn( void ) { + fl.networkSync = true; + GetPhysics()->SetContents( CONTENTS_SOLID ); + + leechPoint = GetAxis()*spawnArgs.GetVector("leechPoint")+GetOrigin(); + + fl.clientEvents = true; + PostEventMS(&EV_EnableNode, 1); +} + +void hhEnergyNode::LeechTrigger(idEntity *activator, const char* type) { + idStr name = spawnArgs.GetString(type); + if ( name != "" ) { + idEntity* ent = gameLocal.FindEntity(name); + if ( ent ) { + if ( ent->RespondsTo( EV_Activate ) || ent->HasSignal( SIG_TRIGGER ) ) { + ent->Signal( SIG_TRIGGER ); + ent->ProcessEvent( &EV_Activate, activator ); + } + ent->TriggerGuis(); + } + } +} + +void hhEnergyNode::Finish() { + Event_Disable(); + + if ( spawnArgs.GetInt("infinite","0") || gameLocal.isMultiplayer ) { // delay before reenabling -cjr + PostEventSec( &EV_EnableNode, spawnArgs.GetFloat( "reenableDelay", "20" ) ); + } +} + +void hhEnergyNode::Event_Enable() { + disabled = false; + + const idDict *energyDef = NULL; + const char* str = spawnArgs.GetString( "def_energy" ); + if ( str && str[0] ) { + energyDef = gameLocal.FindEntityDefDict( str ); + } + + if ( energyDef ) { + hhFxInfo fxInfo; + if (IsBound() || spawnArgs.GetBool("force_bind")) { + fxInfo.SetEntity( this ); // Only bind if we will be moving + } + energyFx = SpawnFxLocal( energyDef->GetString("fx_node"), leechPoint, GetAxis(), &fxInfo, true ); + + idVec3 color = energyDef->GetVector("nodeColor"); + SetShaderParm( SHADERPARM_RED, color.x ); + SetShaderParm( SHADERPARM_GREEN, color.y ); + SetShaderParm( SHADERPARM_BLUE, color.z ); + + StartSound( "snd_activate", SND_CHANNEL_ANY ); + StartSound( "snd_idle", SND_CHANNEL_BODY ); + } +} + +void hhEnergyNode::Event_Disable() { + disabled = true; + if( energyFx.IsValid() ) { + SAFE_REMOVE(energyFx); + } + + SetShaderParm( SHADERPARM_RED, 0 ); + SetShaderParm( SHADERPARM_GREEN, 0 ); + SetShaderParm( SHADERPARM_BLUE, 0 ); + + StopSound( SND_CHANNEL_BODY ); +} + +void hhEnergyNode::WriteToSnapshot( idBitMsgDelta &msg ) const { + idStaticEntity::WriteToSnapshot(msg); + + msg.WriteBits(disabled, 1); +} + +void hhEnergyNode::ReadFromSnapshot( const idBitMsgDelta &msg ) { + idStaticEntity::ReadFromSnapshot(msg); + + bool snapDisabled = !!msg.ReadBits(1); + if (snapDisabled != disabled) { + if (snapDisabled) { + Event_Disable(); + } + else { + Event_Enable(); + } + } +} diff --git a/src/Prey/game_energynode.h b/src/Prey/game_energynode.h new file mode 100644 index 0000000..f8055e3 --- /dev/null +++ b/src/Prey/game_energynode.h @@ -0,0 +1,35 @@ +#ifndef __GAME_ENERGYNODE_H__ +#define __GAME_ENERGYNODE_H__ + +class hhEnergyNode : public idStaticEntity { +public: + CLASS_PROTOTYPE( hhEnergyNode ); + + hhEnergyNode(void); + ~hhEnergyNode(); + + void Spawn( void ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + inline bool CanLeech() { return !disabled; } + void LeechTrigger(idEntity *activator, const char* type); + void Finish(); + + virtual void Event_Enable(); + virtual void Event_Disable(); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + idVec3 leechPoint; + +protected: + bool disabled; + + idEntityPtr energyFx; +}; + +#endif // __GAME_ENERGYNODE_H__ diff --git a/src/Prey/game_entityfx.cpp b/src/Prey/game_entityfx.cpp new file mode 100644 index 0000000..54f5f36 --- /dev/null +++ b/src/Prey/game_entityfx.cpp @@ -0,0 +1,637 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idEntityFx, hhEntityFx ) + EVENT( EV_Activate, hhEntityFx::Event_Trigger ) + EVENT( EV_Fx_KillFx, hhEntityFx::Event_ClearFx ) +END_CLASS + +/* +================ +hhEntityFx::hhEntityFx +================ +*/ +hhEntityFx::hhEntityFx() { + // HUMANHEAD nla + setFxInfo = false; + // HUMANHEAD + +// HUMANHEAD bg + restartActive = false; +// HUMANHEAD END +} + +/* +================ +hhEntityFx::~hhEntityFx +================ +*/ +hhEntityFx::~hhEntityFx() { + //HUMANHEAD: aob - need to shut everything down + Event_ClearFx(); + //HUMANHEAD END +} + +void hhEntityFx::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject( fxInfo ); + savefile->WriteBool( setFxInfo ); + savefile->WriteBool( removeWhenDone ); +// HUMANHEAD bg + savefile->WriteBool( restartActive ); +// HUMANHEAD END +} + +void hhEntityFx::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject( fxInfo ); + savefile->ReadBool( setFxInfo ); + savefile->ReadBool( removeWhenDone ); +// HUMANHEAD bg + savefile->ReadBool( restartActive ); +// HUMANHEAD END +} + +void hhEntityFx::WriteToSnapshot( idBitMsgDelta &msg ) const { + idEntityFx::WriteToSnapshot(msg); + + fxInfo.WriteToSnapshot(msg); +} + +void hhEntityFx::ReadFromSnapshot( const idBitMsgDelta &msg ) { + idEntityFx::ReadFromSnapshot(msg); + + fxInfo.ReadFromSnapshot(msg); +} + +/* +================ +hhEntityFx::CleanUpSingleAction +================ +*/ +void hhEntityFx::CleanUpSingleAction( const idFXSingleAction& fxaction, idFXLocalAction& laction ) { + idEntityFx::CleanUpSingleAction( fxaction, laction ); + + // HUMANHEAD pdm: sound "duration" support + if ( fxaction.type == FX_SOUND && fxaction.sibling == -1 && laction.soundStarted ) { + const idSoundShader *def = declManager->FindSound( fxaction.data ); + refSound.referenceSound->StopSound( SND_CHANNEL_ANY ); + } + + if ( fxaction.type == FX_PARTICLE && fxaction.sibling == -1 ) { + laction.particleSystem = NULL; + laction.particleStartTime = -1; + } + // HUMANHEAD END +} + +/* +================ +hhEntityFx::Hide +================ +*/ +void hhEntityFx::Hide() { + CleanUp(); +} + +/* +================ +hhEntityFx::Run + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +================ +*/ +void hhEntityFx::Run( int time ) { + int ieff, j; + idEntity *ent = NULL; + const idDict *projectileDef = NULL; + idProjectile *projectile = NULL; + + if ( !fxEffect || IsHidden() ) { + return; + } + + for( ieff = 0; ieff < fxEffect->events.Num(); ieff++ ) { + const idFXSingleAction& fxaction = fxEffect->events[ieff]; + idFXLocalAction& laction = actions[ieff]; + + // + // if we're currently done with this one + // + if ( laction.start == -1 ) { + continue; + } + + // + // see if it's delayed + // + if ( laction.delay ) { + if ( laction.start + (time - laction.start) < laction.start + (laction.delay * 1000) ) { + continue; + } + } + + // + // each event can have it's own delay and restart + // + int actualStart = laction.delay ? laction.start + (int)( laction.delay * 1000 ) : laction.start; + float pct = (float)( time - actualStart ) / (1000 * fxaction.duration ); + if ( pct >= 1.0f ) { + laction.start = -1; + float totalDelay = 0.0f; + if ( fxaction.restart ) { + if ( fxaction.random1 || fxaction.random2 ) { + totalDelay = fxaction.random1 + gameLocal.random.RandomFloat() * (fxaction.random2 - fxaction.random1); + } else { + totalDelay = fxaction.delay; + } + laction.delay = totalDelay; + laction.start = time; + } + continue; + } + + if ( fxaction.fire.Length() ) { + for( j = 0; j < fxEffect->events.Num(); j++ ) { + if ( fxEffect->events[j].name.Icmp( fxaction.fire ) == 0 ) { + actions[j].delay = 0; + } + } + } + + idFXLocalAction *useAction = NULL; + if ( fxaction.sibling == -1 ) { + useAction = &laction; + } else { + useAction = &actions[fxaction.sibling]; + } + assert( useAction ); + + switch( fxaction.type ) { + case FX_ATTACHLIGHT: + case FX_LIGHT: { + if ( useAction->lightDefHandle == -1 ) { + if ( fxaction.type == FX_LIGHT ) { + memset( &useAction->renderLight, 0, sizeof( renderLight_t ) ); + //HUMANHEAD: bjk once again fixing aob code + useAction->renderLight.axis = DetermineAxis( fxaction ); + useAction->renderLight.origin = GetOrigin() + fxaction.offset * useAction->renderLight.axis; + useAction->renderLight.axis = hhUtils::SwapXZ( useAction->renderLight.axis ); + //HUMANHEAD END + useAction->renderLight.lightRadius[0] = fxaction.lightRadius; + useAction->renderLight.lightRadius[1] = fxaction.lightRadius; + useAction->renderLight.lightRadius[2] = fxaction.lightRadius; + useAction->renderLight.shader = declManager->FindMaterial( fxaction.data, false ); + useAction->renderLight.shaderParms[ SHADERPARM_RED ] = fxaction.lightColor.x; + useAction->renderLight.shaderParms[ SHADERPARM_GREEN ] = fxaction.lightColor.y; + useAction->renderLight.shaderParms[ SHADERPARM_BLUE ] = fxaction.lightColor.z; + useAction->renderLight.shaderParms[ SHADERPARM_TIMESCALE ] = 1.0f; + useAction->renderLight.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( time ); + useAction->renderLight.referenceSound = refSound.referenceSound; + useAction->renderLight.pointLight = true; + if ( fxaction.noshadows ) { + useAction->renderLight.noShadows = true; + } + useAction->lightDefHandle = gameRenderWorld->AddLightDef( &useAction->renderLight ); + } + if ( fxaction.noshadows ) { + for( j = 0; j < fxEffect->events.Num(); j++ ) { + idFXLocalAction& laction2 = actions[j]; + if ( laction2.modelDefHandle != -1 ) { + laction2.renderEntity.noShadow = true; + } + } + } + } else if ( fxaction.trackOrigin ) { + //HUMANHEAD: bjk + useAction->renderLight.axis = DetermineAxis( fxaction ); + useAction->renderLight.origin = GetOrigin() + fxaction.offset * useAction->renderLight.axis; + useAction->renderLight.axis = hhUtils::SwapXZ( useAction->renderLight.axis ); + gameRenderWorld->UpdateLightDef( useAction->lightDefHandle, &useAction->renderLight ); + //HUMANHEAD END + } + ApplyFade( fxaction, *useAction, time, actualStart ); + break; + } + case FX_SOUND: { + if ( !useAction->soundStarted ) { + useAction->soundStarted = true; + const idSoundShader *shader = declManager->FindSound(fxaction.data); + StartSoundShader( shader, SND_CHANNEL_ANY, 0, false, NULL ); + for( j = 0; j < fxEffect->events.Num(); j++ ) { + idFXLocalAction& laction2 = actions[j]; + if ( laction2.lightDefHandle != -1 ) { + laction2.renderLight.referenceSound = refSound.referenceSound; + gameRenderWorld->UpdateLightDef( laction2.lightDefHandle, &laction2.renderLight ); + } + } + } + break; + } + case FX_DECAL: { + if ( !useAction->decalDropped ) { + useAction->decalDropped = true; + // HUMANHEAD pdm: Increased depth to from 8 to 25 + //TODO: Expose depth and parrallel to the FX files + gameLocal.ProjectDecal( GetPhysics()->GetOrigin(), GetPhysics()->GetGravity(), 25.0f, true, fxaction.size, fxaction.data ); + } + break; + } + case FX_SHAKE: { + if ( !useAction->shakeStarted ) { + idDict args; + args.Clear(); + args.SetFloat( "kick_time", fxaction.shakeTime ); + args.SetFloat( "kick_amplitude", fxaction.shakeAmplitude ); + for ( j = 0; j < gameLocal.numClients; j++ ) { + idPlayer *player = gameLocal.GetClientByNum( j ); + if ( player && ( player->GetPhysics()->GetOrigin() - GetPhysics()->GetOrigin() ).LengthSqr() < Square( fxaction.shakeDistance ) ) { + if ( !gameLocal.isMultiplayer || !fxaction.shakeIgnoreMaster || GetBindMaster() != player ) { + player->playerView.DamageImpulse( fxaction.offset, &args ); + } + } + } + if ( fxaction.shakeImpulse != 0.0f && fxaction.shakeDistance != 0.0f ) { + idEntity *ignore_ent = NULL; + if ( gameLocal.isMultiplayer ) { + ignore_ent = this; + if ( fxaction.shakeIgnoreMaster ) { + ignore_ent = GetBindMaster(); + } + } + // lookup the ent we are bound to? + gameLocal.RadiusPush( GetPhysics()->GetOrigin(), fxaction.shakeDistance, fxaction.shakeImpulse, this, ignore_ent, 1.0f, true ); + } + useAction->shakeStarted = true; + } + break; + } + case FX_ATTACHENTITY: + case FX_MODEL: { + if ( useAction->modelDefHandle == -1 ) { + memset( &useAction->renderEntity, 0, sizeof( renderEntity_t ) ); + //HUMANHEAD: aob - rotated offset by axis + useAction->renderEntity.axis = DetermineAxis( fxaction ); + useAction->renderEntity.origin = GetOrigin() + fxaction.offset * useAction->renderEntity.axis; + useAction->renderEntity.axis = hhUtils::SwapXZ( useAction->renderEntity.axis ); + useAction->renderEntity.weaponDepthHack = renderEntity.weaponDepthHack; + useAction->renderEntity.onlyVisibleInSpirit = renderEntity.onlyVisibleInSpirit; + useAction->renderEntity.onlyInvisibleInSpirit = renderEntity.onlyInvisibleInSpirit; // tmj + useAction->renderEntity.allowSurfaceInViewID = renderEntity.allowSurfaceInViewID; // bjk + //HUMANHEAD END + useAction->renderEntity.hModel = renderModelManager->FindModel( fxaction.data ); + // HUMANHEAD pdm: allow color on fx, but they'll be the same for all particles + if ( renderEntity.shaderParms[SHADERPARM_RED] != 0.0f || + renderEntity.shaderParms[SHADERPARM_GREEN] != 0.0f || + renderEntity.shaderParms[SHADERPARM_BLUE] != 0.0f) { + useAction->renderEntity.shaderParms[ SHADERPARM_RED ] = renderEntity.shaderParms[SHADERPARM_RED]; + useAction->renderEntity.shaderParms[ SHADERPARM_GREEN ] = renderEntity.shaderParms[SHADERPARM_GREEN]; + useAction->renderEntity.shaderParms[ SHADERPARM_BLUE ] = renderEntity.shaderParms[SHADERPARM_BLUE]; + } + else { + useAction->renderEntity.shaderParms[ SHADERPARM_RED ] = 1.0f; + useAction->renderEntity.shaderParms[ SHADERPARM_GREEN ] = 1.0f; + useAction->renderEntity.shaderParms[ SHADERPARM_BLUE ] = 1.0f; + } + useAction->renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( time ); + useAction->renderEntity.shaderParms[3] = 1.0f; + useAction->renderEntity.shaderParms[ SHADERPARM_DIVERSITY ] = gameLocal.random.RandomFloat(); //HUMANHEAD bjk + if ( useAction->renderEntity.hModel ) { + useAction->renderEntity.bounds = useAction->renderEntity.hModel->Bounds( &useAction->renderEntity ); + } + useAction->modelDefHandle = gameRenderWorld->AddEntityDef( &useAction->renderEntity ); + } else if ( fxaction.trackOrigin ) { + //HUMANHEAD: aob - rotated offset by axis + useAction->renderEntity.axis = DetermineAxis( fxaction ); + if (fxaction.offset.x || fxaction.offset.y || fxaction.offset.z) { + useAction->renderEntity.origin = GetOrigin() + fxaction.offset * useAction->renderEntity.axis; + } + else { + useAction->renderEntity.origin = GetOrigin(); + } + useAction->renderEntity.axis = hhUtils::SwapXZ( useAction->renderEntity.axis ); + gameRenderWorld->UpdateEntityDef( useAction->modelDefHandle, &useAction->renderEntity ); + //HUMANHEAD END + } + ApplyFade( fxaction, *useAction, time, actualStart ); + break; + } + case FX_PARTICLE: { + //HUMANHEAD: aob + //rww - kind of a hack, but otherwise the timing is off, which causes issues in mp. + if (useAction->particleStartTime == -2) { + useAction->particleStartTime = gameLocal.time; + } + + if( !useAction->particleSystem && fxaction.data.Length() ) { + useAction->particleSystem = static_cast( declManager->FindType( DECL_PARTICLE, fxaction.data, false ) ); + } else if( useAction->particleStartTime >= 0 && useAction->particleSystem ) { + idMat3 axis = DetermineAxis( fxaction ); + if( !gameLocal.smokeParticles->EmitSmoke(useAction->particleSystem, useAction->particleStartTime, gameLocal.random.RandomFloat(), GetOrigin() + fxaction.offset * axis, hhUtils::SwapXZ(axis)) ) { + useAction->particleStartTime = -1; + } + } + // HUMANHEAD END + break; + } + case FX_LAUNCH: { + //HUMANHEAD rww - if this is a client ent then assert. + assert(!fl.clientEntity); + + if ( gameLocal.isClient ) { + // client never spawns entities outside of ClientReadSnapshot + useAction->launched = true; + break; + } + if ( !useAction->launched ) { + useAction->launched = true; + projectile = NULL; + // FIXME: may need to cache this if it is slow + projectileDef = gameLocal.FindEntityDefDict( fxaction.data, false ); + if ( !projectileDef ) { + gameLocal.Warning( "projectile \'%s\' not found", fxaction.data.c_str() ); + } else { + gameLocal.SpawnEntityDef( *projectileDef, &ent, false ); + if ( ent && ent->IsType( idProjectile::Type ) ) { + projectile = ( idProjectile * )ent; + projectile->Create( this, GetPhysics()->GetOrigin(), GetPhysics()->GetAxis()[0] ); + projectile->Launch( GetPhysics()->GetOrigin(), GetPhysics()->GetAxis()[0], vec3_origin ); + } + } + } + break; + } + } + } +} + +/* +================ +hhEntityFx::DetermineAxis + +HUMANHEAD: aob +================ +*/ +idMat3 hhEntityFx::DetermineAxis( const idFXSingleAction& fxaction ) { + idVec3 fxDir; + + if( fxaction.explicitAxis ) { + if (fxaction.useAxis == AXIS_CUSTOMLOCAL) { + // When customlocal is set, axis is the specifed axis, untransformed by the entities axis + return fxaction.dir.ToMat3(); + } + return (fxaction.dir[0] * GetAxis()[0] + fxaction.dir[1] * GetAxis()[1] + fxaction.dir[2] * GetAxis()[2]).ToMat3(); + } else if( fxInfo.GetAxisFor(fxaction.useAxis, fxDir) ) { + return fxDir.ToMat3(); + } + + return GetAxis(); +} + +/* +================ +hhEntityFx::Nozzle + +HUMANHEAD: aob - used to toggle particle systems +================ +*/ +void hhEntityFx::Nozzle( bool bOn ) { + if ( !fxEffect ) { + return; + } + + if( bOn && !IsActive( TH_THINK ) ) { + Event_Trigger( NULL ); + } + + if( !bOn ) { + //Canceling events incase we are toggled before our actvate event has fired. + CancelEvents( &EV_Activate ); + + if( IsActive( TH_THINK ) ) { + Event_ClearFx(); + } + } +} + +/* +================ +hhEntityFx::DormantBegin + +//HUMANHEAD: aob +================ +*/ +void hhEntityFx::DormantBegin() { + idEntityFx::DormantBegin(); + + //HUMANHEAD: aob - used CleanUp directly so 'started' doesn't get reset + //Stop(); + CleanUp(); + //HUMANHEAD END + + //want to make sure we can trigger these back on + nextTriggerTime = -1; +} + +/* +================ +hhEntityFx::DormantEnd + +//HUMANHEAD: aob +================ +*/ +void hhEntityFx::DormantEnd() { + idEntityFx::DormantEnd(); + + if ( started >= 0 ) { + CreateFx( this ); + } +} + +/* +================ +hhEntityFx::Event_Trigger +================ +*/ +void hhEntityFx::Event_Trigger( idEntity *activator ) { + if ( gameLocal.time < nextTriggerTime ) { + return; + } + +// HUMANHEAD bg: Special handling for "restart" fx enable/disable. + if ( restartActive && (activator != this) ) { + CancelEvents( &EV_Fx_Action ); + CancelEvents( &EV_Activate ); + CancelEvents( &EV_Fx_KillFx ); + Stop(); + CleanUp(); + BecomeInactive( TH_THINK ); + restartActive = false; + return; + } + if ( !restartActive && spawnArgs.GetFloat( "restart" ) ) { + restartActive = true; + } +// HUMANHEAD END + + //HUMANHEAD: aob + if( spawnArgs.GetBool("toggle") && IsActive(TH_THINK) ) { + ProcessEvent( &EV_Fx_KillFx ); + return; + } + //HUMANHEAD END + + //HUMANHEAD: aob - moved logic to helper function so I can call code specifically + CreateFx( activator ); + //HUMANHEAD END +} + +/* +================ +hhEntityFx::CreateFx + +HUMANHEAD: aob +================ +*/ +void hhEntityFx::CreateFx( idEntity *activator ) { + if ( g_skipFX.GetBool() ) { + return; + } + + float fxActionDelay; + const char *fx; + + if ( spawnArgs.GetString( "fx", "", &fx) ) { + Setup( fx ); + Start( gameLocal.time ); + //HUMANHEAD: aob - added so fx can be deleted at end of duration + if( RemoveWhenDone() ) { + PostEventMS( &EV_Remove, Duration() ); + } else { + PostEventMS( &EV_Fx_KillFx, Duration() ); + } + //HUMANHEAD END + BecomeActive( TH_THINK ); + } + + fxActionDelay = spawnArgs.GetFloat( "fxActionDelay" ); + if ( fxActionDelay != 0.0f ) { + nextTriggerTime = gameLocal.time + SEC2MS( fxActionDelay ); + } else { + // prevent multiple triggers on same frame + nextTriggerTime = gameLocal.time + 1; + } + PostEventSec( &EV_Fx_Action, fxActionDelay, activator ); +} + +/* +================ +hhEntityFx::StartFx + +creates an fx entity, ONLY CALL THIS ON THE SERVER. -rww +================ +*/ +hhEntityFx *hhEntityFx::StartFx( const char *fx, const idVec3 *useOrigin, const idMat3 *useAxis, idEntity *ent, bool bind ) +{ + + if ( g_skipFX.GetBool() || !fx || !*fx ) { + return NULL; + } + + idDict args; + args.SetBool( "start", true ); + args.Set( "fx", fx ); + hhEntityFx *nfx = static_cast( gameLocal.SpawnEntityType( hhEntityFx::Type, &args ) ); + if ( nfx->Joint() && *nfx->Joint() ) { + nfx->BindToJoint( ent, nfx->Joint(), true ); + nfx->SetOrigin( vec3_origin ); + } else { + nfx->SetOrigin( (useOrigin) ? *useOrigin : ent->GetPhysics()->GetOrigin() ); + nfx->SetAxis( (useAxis) ? *useAxis : ent->GetPhysics()->GetAxis() ); + } + + if ( bind ) { + // never bind to world spawn + if ( ent != gameLocal.world ) { + nfx->Bind( ent, true ); + } + } + nfx->Show(); + return nfx; +} + +/* +================ +hhEntityFx::SetParticleShaderParm + Allow setting shader parms directly to particle renderentities + Note that this functionality isn't set to the default SetShaderParm() + to avoid breaking any existing code +================ +*/ +void hhEntityFx::SetParticleShaderParm( int parmnum, float value ) { + + if ( ( parmnum < 0 ) || ( parmnum >= MAX_ENTITY_SHADER_PARMS ) ) { + gameLocal.Warning( "shader parm index (%d) out of range", parmnum ); + return; + } + + for( int ieff = 0; ieff < fxEffect->events.Num(); ieff++ ) { + const idFXSingleAction& fxaction = fxEffect->events[ieff]; + idFXLocalAction *useAction = NULL; + idFXLocalAction& laction = actions[ieff]; + if ( fxaction.sibling == -1 ) { + useAction = &laction; + } else { + useAction = &actions[fxaction.sibling]; + } + if ( !useAction ) { + continue; + } + switch( fxaction.type ) { + case FX_MODEL: { + useAction->renderEntity.shaderParms[parmnum] = value; + break; + } + } + } +} + +/* +================ +hhEntityFx::Event_ClearFx + + Clears any visual fx started when item(mob) was spawned +================ +*/ +void hhEntityFx::Event_ClearFx( void ) { + + if ( g_skipFX.GetBool() ) { + return; + } + + Stop(); + CleanUp(); + BecomeInactive( TH_THINK ); + + if ( spawnArgs.GetBool("test") ) { + PostEventMS( &EV_Activate, 0, this ); + } else { + if (!spawnArgs.GetBool("triggered")) { + float rest = spawnArgs.GetInt( "restart", "0" ); + if ( rest == 0.0f ) { + //HUMANHEAD: aob - added RemoveWhenDone and CancelEvents call + if( RemoveWhenDone() ) { + PostEventSec( &EV_Remove, 0.1f ); + } else { + CancelEvents( &EV_Fx_Action ); + } + //HUMANHEAD END + } else { + rest *= gameLocal.random.RandomFloat(); +// HUMANHEAD bg: Give ability for minimum restart time by adding offset. + rest += spawnArgs.GetFloat( "restartDelay", "0" ); +// HUMANHEAD END + PostEventSec( &EV_Activate, rest, this ); + } + } + } +} diff --git a/src/Prey/game_entityfx.h b/src/Prey/game_entityfx.h new file mode 100644 index 0000000..bae8d61 --- /dev/null +++ b/src/Prey/game_entityfx.h @@ -0,0 +1,62 @@ +#ifndef __HH_ENTITY_FX_H +#define __HH_ENTITY_FX_H + +class hhEntityFx : public idEntityFx { + CLASS_PROTOTYPE( hhEntityFx ); + +public: + hhEntityFx(); + virtual ~hhEntityFx(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //HUMANHEAD rww + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + //HUMANHEAD END + + virtual void Run( int time ); + + // HUMANHEAD nla + void SetUseAxis( fxAxis theAxis ) { }; + void SetFxInfo( const hhFxInfo &i ) { fxInfo = i; setFxInfo = true; removeWhenDone = fxInfo.RemoveWhenDone(); GetRenderEntity()->weaponDepthHack = fxInfo.UseWeaponDepthHack(); } + bool RemoveWhenDone() { return( removeWhenDone ); } + void RemoveWhenDone( bool remove ) { removeWhenDone = remove; } + void Toggle() { Nozzle( !IsActive(TH_THINK) ); } + void Nozzle( bool bOn ); + idMat3 DetermineAxis( const idFXSingleAction& fxaction ); + void CreateFx( idEntity *activator ); + + virtual void Hide(); + + static hhEntityFx *StartFx( const char *fx, const idVec3 *useOrigin, const idMat3 *useAxis, idEntity *ent, bool bind ); + void SetParticleShaderParm( int parmnum, float value ); + // HUMANHEAD END + +protected: + virtual void CleanUpSingleAction( const idFXSingleAction& fxaction, idFXLocalAction& laction ); + virtual void DormantBegin(); + virtual void DormantEnd(); + +protected: + void Event_Trigger( idEntity *activator ); + void Event_ClearFx( void ); + +protected: + // HUMANHEAD nla + enum fxAxis { AXIS_CURRENT, AXIS_NORMAL, AXIS_BOUNCE, AXIS_INCOMING, AXIS_CUSTOMLOCAL }; + // HUMANHEAD END + + // HUMANHEAD + hhFxInfo fxInfo; + bool setFxInfo; + bool removeWhenDone; + // HUMANHEAD END + + // HUMANHEAD bg + bool restartActive; + // HUMANHEAD END +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_entityspawner.cpp b/src/Prey/game_entityspawner.cpp new file mode 100644 index 0000000..cd91c9b --- /dev/null +++ b/src/Prey/game_entityspawner.cpp @@ -0,0 +1,117 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_SpawnEntity("spawnEntity", NULL, 'd'); + +CLASS_DECLARATION( idEntity, hhEntitySpawner ) + EVENT(EV_Activate, hhEntitySpawner::Event_Activate) + EVENT(EV_SpawnEntity, hhEntitySpawner::Event_SpawnEntity) +END_CLASS + +// +// Spawn() +// +void hhEntitySpawner::Spawn( void ) { + + maxSpawnCount = spawnArgs.GetInt("max_spawn", "-1"); // Default: Infinite + currSpawnCount = 0; + if(!spawnArgs.GetString("def_entity", "", entDefName)) { + gameLocal.Error("def_entity not specified for hhEntitySpawner"); + } + entSpawnArgs.Clear(); + idStr tmpStr; + idStr realKeyName; + + // Copy keys for monster + const idKeyValue *kv = spawnArgs.MatchPrefix("ent_", NULL); + while(kv) { + tmpStr = kv->GetKey(); + int usIndex = tmpStr.FindChar("ent_", '_'); + realKeyName = tmpStr.Mid(usIndex+1, strlen(kv->GetKey())-usIndex-1); + entSpawnArgs.Set(realKeyName, kv->GetValue()); + + kv = spawnArgs.MatchPrefix("ent_", kv); + } +} + +void hhEntitySpawner::Save(idSaveGame *savefile) const { + savefile->WriteString( entDefName ); + savefile->WriteDict( &entSpawnArgs ); + savefile->WriteInt( maxSpawnCount ); + savefile->WriteInt( currSpawnCount ); +} + +void hhEntitySpawner::Restore( idRestoreGame *savefile ) { + savefile->ReadString( entDefName ); + savefile->ReadDict( &entSpawnArgs ); + savefile->ReadInt( maxSpawnCount ); + savefile->ReadInt( currSpawnCount ); +} + +// +// Event_Activate() +// +void hhEntitySpawner::Event_Activate(idEntity *activator) { + Event_SpawnEntity(); +} + +// +// Event_Activate() +// +void hhEntitySpawner::Event_SpawnEntity( void ) { + idVec3 entSize; + + // Can't spawn anymore + if(maxSpawnCount >= 0 && currSpawnCount >= maxSpawnCount) { + return; + } + + idDict args = entSpawnArgs;; + + args.SetVector( "origin", GetPhysics()->GetOrigin()); + args.SetMatrix("rotation", GetAxis()); + + // entity collision checks for seeing if we are going to collide with another entity on spawn + const idDict *entDef = gameLocal.FindEntityDefDict( entDefName, false ); + if ( !entDef ) { + if (!entDefName) { //HUMANHEAD rww + gameLocal.Error("NULL entDefName in hhEntitySpawner::Event_SpawnEntity\n"); + } + else { + gameLocal.Warning( "Unknown Def '%s'", entDefName ); + } + return; + } + entDef->GetVector( "size", "0", entSize ); + idBounds bounds = idBounds( GetPhysics()->GetOrigin() ).Expand( max( entSize.x, entSize.y ) ); + idEntity* ents[MAX_GENTITIES]; + int numModels = gameLocal.clip.EntitiesTouchingBounds( bounds, -1, ents, MAX_GENTITIES ); + for ( int i = 0; i < numModels ; i++ ) { + if( ents[i] && ents[i]->IsType(idActor::Type) ) { + idThread::ReturnInt( false ); + return; + } + } + + idEntity *e = gameLocal.SpawnObject(entDefName, &args); + + if(!e) { + gameLocal.Error("hhEntitySpawner: Failed to spawn entity def named: %s", entDefName); + return; + } + + // Copy the targets that our case has to our newly spawned entity + for( int i = 0; i < targets.Num(); i++ ) { + e->targets.AddUnique(targets[i]); + } + + if(e->IsType(idAI::Type)) { + static_cast(e)->viewAxis = GetAxis(); + } + + currSpawnCount++; + idThread::ReturnInt( true ); +} diff --git a/src/Prey/game_entityspawner.h b/src/Prey/game_entityspawner.h new file mode 100644 index 0000000..701ab2a --- /dev/null +++ b/src/Prey/game_entityspawner.h @@ -0,0 +1,25 @@ +#ifndef GAME_ENTITYSPAWNER_H +#define GAME_ENTITYSPAWNER_H + +// +// hhEntitySpawner +// +class hhEntitySpawner : public idEntity { +public: + CLASS_PROTOTYPE( hhEntitySpawner ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void Event_Activate(idEntity *activator); + void Event_SpawnEntity( void ); + + idStr entDefName; + idDict entSpawnArgs; + int maxSpawnCount; + int currSpawnCount; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_events.cpp b/src/Prey/game_events.cpp new file mode 100644 index 0000000..79dd453 --- /dev/null +++ b/src/Prey/game_events.cpp @@ -0,0 +1,12 @@ +// game_events.cpp +// +// All of our events that have use to multiple classes are kept together so everyone can get at them + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + + + diff --git a/src/Prey/game_events.h b/src/Prey/game_events.h new file mode 100644 index 0000000..60cfa48 --- /dev/null +++ b/src/Prey/game_events.h @@ -0,0 +1,18 @@ +#ifndef __PREY_EVENTS_H__ +#define __PREY_EVENTS_H__ + + +/* +extern const idEventDef EV_Kill; + +extern const idEventDef EV_Trigger; +extern const idEventDef EV_Retrigger; +extern const idEventDef EV_UnTrigger; +extern const idEventDef EV_PollForUntouch; + +extern const idEventDef EV_Hit; +extern const idEventDef EV_Stay; +extern const idEventDef EV_Double; +*/ + +#endif // __PREY_EVENTS_H__ diff --git a/src/Prey/game_fixedpod.cpp b/src/Prey/game_fixedpod.cpp new file mode 100644 index 0000000..e7b27f8 --- /dev/null +++ b/src/Prey/game_fixedpod.cpp @@ -0,0 +1,209 @@ +// Game_FixedPod.cpp +// +// non-moving exploding pod + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_ExplodedBy("", "e"); +const idEventDef EV_SpawnShrapnel(""); + +CLASS_DECLARATION(idEntity, hhFixedPod) + EVENT( EV_Activate, hhFixedPod::Event_Trigger ) + EVENT( EV_ExplodeDamage, hhFixedPod::Event_ExplodeDamage ) + EVENT( EV_ExplodedBy, hhFixedPod::Event_ExplodedBy ) + EVENT( EV_SpawnShrapnel, hhFixedPod::Event_SpawnDebris ) + EVENT( EV_Broadcast_AssignFx, hhFixedPod::Event_AssignFx ) +END_CLASS + + +hhFixedPod::hhFixedPod() { + fx = NULL; +} + +//========================================================================== +// +// hhFixedPod::Spawn +// +//========================================================================== +void hhFixedPod::Spawn(void) { + + fl.takedamage = true; + + // setup the clipModel + GetPhysics()->SetContents( CONTENTS_SOLID ); + + // Spawn the energy beam + hhBeamSystem *beam = hhBeamSystem::SpawnBeam( GetOrigin(), spawnArgs.GetString("beam") ); + if( beam ) { + beam->SetOrigin(GetPhysics()->GetOrigin() - GetPhysics()->GetAxis()[2]*28); + beam->SetAxis(GetPhysics()->GetAxis()); + if (IsBound() || spawnArgs.GetBool("force_bind")) { + beam->Bind(this, false); // Only bind if we will be moving + } + beam->SetTargetLocation(beam->GetPhysics()->GetOrigin() + GetPhysics()->GetAxis()[2] * 56); + // Bound entities automatically removed upon destruction + } + + hhFxInfo fxInfo; + if (IsBound() || spawnArgs.GetBool("force_bind")) { + fxInfo.SetEntity( this ); // Only bind if we will be moving + } + fxInfo.RemoveWhenDone( false ); + fxInfo.NoRemoveWhenUnbound( true ); + BroadcastFxInfo( spawnArgs.GetString("fx_energybeam"), GetOrigin(), GetAxis(), &fxInfo, &EV_Broadcast_AssignFx ); + + StartSound( "snd_idle", SND_CHANNEL_IDLE, 0, true, NULL ); +} + +//========================================================================== +// +// hhFixedPod::Event_AssignFxSmoke +// +//========================================================================== +void hhFixedPod::Event_AssignFx( hhEntityFx* fx ) { + this->fx = fx; +} + +void hhFixedPod::Save(idSaveGame *savefile) const { + fx.Save(savefile); +} + +void hhFixedPod::Restore( idRestoreGame *savefile ) { + fx.Restore(savefile); +} + +//========================================================================== +// +// hhFixedPod::Killed +// +//========================================================================== +void hhFixedPod::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + fl.takedamage = false; + + // Activate targets + ActivateTargets( attacker ); + + GetPhysics()->SetContents( 0 ); + + // Need to post an event because already in physics code now, can't nest a projectile spawn from within physics code + // currently, because rigid body physics ::Evaluate is not reentrant friendly (has a static timer) + PostEventMS( &EV_ExplodedBy, 0, attacker ); +} + + +//========================================================================== +/// +// hhFixedPod::Explode +// +//========================================================================== +void hhFixedPod::Explode( idEntity *attacker ) { + hhFxInfo fxInfo; + + if (fx.IsValid()) { + fx->Hide(); + fx->PostEventMS(&EV_Remove, 0); + } + + // Spawn explosion + StopSound( SND_CHANNEL_IDLE, true ); + StartSound( "snd_explode", SND_CHANNEL_ANY, 0, true, NULL ); + + fxInfo.SetNormal( idVec3(0.0f, 0.0f, 1.0f) ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_detonate", GetOrigin(), GetAxis(), &fxInfo ); + + Hide(); + + PostEventMS( &EV_SpawnShrapnel, 10 ); + PostEventMS( &EV_ExplodeDamage, 250, attacker ); // NOTE: This even MUST occur before the remove event + PostEventMS( &EV_Remove, 500 ); // Remove after a small delay to allow sound commands to execute +} + +//========================================================================== +// +// hhFixedPod::SpawnDebris +// +//========================================================================== +void hhFixedPod::SpawnDebris() { + idEntity* ent = NULL; + idVec3 launchDir; + int amount = 0; + idDebris* debris = NULL; + const idDict *dict = NULL; + + int numShrapnel = spawnArgs.GetInt( "debris_count" ); + if( !numShrapnel ) { + return; + } + + for( const idKeyValue* kv = spawnArgs.MatchPrefix("def_debris", NULL); kv; kv = spawnArgs.MatchPrefix("def_debris", kv) ) { + if( !kv->GetValue().Length() ) { + continue; + } + + dict = gameLocal.FindEntityDefDict( kv->GetValue().c_str(), false ); + if( !dict ) { + continue; + } + + amount = hhMath::hhMax( 1, gameLocal.random.RandomInt(numShrapnel) ); + for ( int i = 0; i < amount; i++ ) { + launchDir = hhUtils::RandomVector(); + launchDir.Normalize(); + + gameLocal.SpawnEntityDef( *dict, &ent ); + if ( !ent || !ent->IsType( idDebris::Type ) ) { + gameLocal.Error( "'%s' is not an idDebris", kv->GetValue().c_str() ); + } + + debris = static_cast(ent); + debris->Create( this, GetOrigin(), launchDir.ToMat3() ); + debris->Launch(); + } + } +} + +//========================================================================== +// +// hhFixedPod::Event_ExplodeDamage +// +// Applies the radius damage slightly after the actual explosion, so that +// when one explodes it will cascade and explode other fixed pods. +//========================================================================== + +void hhFixedPod::Event_ExplodeDamage( idEntity *attacker ) { + gameLocal.RadiusDamage( GetPhysics()->GetOrigin(), this, attacker, this, this, spawnArgs.GetString("def_explodedamage") ); +} + +//========================================================================== +// +// hhFixedPod::Event_ExplodedBy +// +//========================================================================== +void hhFixedPod::Event_ExplodedBy( idEntity *activator ) { + Explode(activator); +} + +//========================================================================== +// +// hhFixedPod::Event_Trigger +// +//========================================================================== + +void hhFixedPod::Event_Trigger( idEntity *activator ) { + Explode(activator); +} + +//========================================================================== +// +// hhFixedPod::Event_SpawnDebris +// +//========================================================================== + +void hhFixedPod::Event_SpawnDebris() { + SpawnDebris(); +} + diff --git a/src/Prey/game_fixedpod.h b/src/Prey/game_fixedpod.h new file mode 100644 index 0000000..bc312cd --- /dev/null +++ b/src/Prey/game_fixedpod.h @@ -0,0 +1,31 @@ + +#ifndef __GAME_FIXEDPOD_H__ +#define __GAME_FIXEDPOD_H__ + +extern const idEventDef EV_ExplodedBy; + +class hhFixedPod : public idEntity { +public: + CLASS_PROTOTYPE( hhFixedPod ); + + hhFixedPod(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + + void Explode( idEntity *attacker ); + void SpawnDebris(); + +protected: + void Event_Trigger( idEntity *activator ); + void Event_ExplodeDamage( idEntity *attacker ); + void Event_ExplodedBy( idEntity *activator ); + void Event_SpawnDebris(); + void Event_AssignFx( hhEntityFx* fx ); + + idEntityPtr fx; +}; + + +#endif /* __GAME_FIXEDPOD_H__ */ diff --git a/src/Prey/game_forcefield.cpp b/src/Prey/game_forcefield.cpp new file mode 100644 index 0000000..8942db4 --- /dev/null +++ b/src/Prey/game_forcefield.cpp @@ -0,0 +1,625 @@ +//************************************************************************** +//** +//** GAME_FORCEFIELD.CPP +//** +//** Game code for the forcefield +//** +//************************************************************************** + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idEntity, hhForceField ) + EVENT( EV_Activate, hhForceField::Event_Activate ) +END_CLASS + +/* +=========== +hhForceField::Spawn +=========== +*/ +void hhForceField::Spawn(void) { + fl.takedamage = true; + SetShaderParm( SHADERPARM_TIMEOFFSET, 1.0f ); + SetShaderParm( SHADERPARM_MODE, 0.0f ); + + fade = 0.0f; + + activationRate = spawnArgs.GetFloat( "activationRate" ); + deactivationRate = spawnArgs.GetFloat( "deactivationRate" ); + undamageFadeRate = spawnArgs.GetFloat( "undamageFadeRate" ); + +// BecomeActive( TH_THINK|TH_TICKER ); + + cachedContents = CONTENTS_FORCEFIELD | CONTENTS_BLOCK_RADIUSDAMAGE | CONTENTS_SHOOTABLE; + + physicsObj.SetSelf( this ); + + if (spawnArgs.GetBool("isSimpleBox")) { + // Simple boxes are cheaper and can be bound to other objects + physicsObj.SetClipModel( new idClipModel(idTraceModel(GetPhysics()->GetBounds())), 1.0f ); + physicsObj.SetContents( cachedContents ); + physicsObj.SetOrigin( GetOrigin() ); + physicsObj.SetAxis( GetAxis() ); + SetPhysics( &physicsObj ); + } + else { + // Non-simple has real per-poly collision with it's model, uses default static physics because we don't have a tracemodel. + // This loses the activation of contacts on the object (so some things may not fall through when disabled), but in my tests, + // the spirit proxy falls through fine. NOTE: To fix the movables, etc. not falling when these are turned off, I'm manually + // Activating appropriate physics that overlap the bounds when Turned off. +// physicsObj.SetClipModel( new idClipModel(GetPhysics()->GetClipModel()), 1.0f ); +// physicsObj.SetContents( cachedContents ); +// physicsObj.SetOrigin( GetOrigin() ); +// physicsObj.SetAxis( GetAxis() ); +// SetPhysics( &physicsObj ); + + // Apparently the flags need to be on the material to make projectiles hit these. + GetPhysics()->SetContents(cachedContents); + } + + EnterOnState(); + damagedState = false; + + nextCollideFxTime = gameLocal.time; + + + fl.networkSync = true; //always want to sync over the net +} + +void hhForceField::Save( idSaveGame *savefile ) const { + savefile->WriteInt( fieldState ); + savefile->WriteBool( damagedState ); + savefile->WriteFloat( activationRate ); + savefile->WriteFloat( deactivationRate ); + savefile->WriteFloat( undamageFadeRate ); + savefile->WriteInt( applyImpulseAttempts ); + savefile->WriteInt( cachedContents ); + savefile->WriteFloat( fade ); + savefile->WriteInt( nextCollideFxTime ); + savefile->WriteStaticObject( physicsObj ); +} + +void hhForceField::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( reinterpret_cast ( fieldState ) ); + savefile->ReadBool( damagedState ); + savefile->ReadFloat( activationRate ); + savefile->ReadFloat( deactivationRate ); + savefile->ReadFloat( undamageFadeRate ); + savefile->ReadInt( applyImpulseAttempts ); + savefile->ReadInt( cachedContents ); + savefile->ReadFloat( fade ); + savefile->ReadInt( nextCollideFxTime ); + savefile->ReadStaticObject( physicsObj ); + + // Only restore physics if we were using it before + if (spawnArgs.GetBool("isSimpleBox")) { + RestorePhysics( &physicsObj ); + } +} + +void hhForceField::WriteToSnapshot( idBitMsgDelta &msg ) const { + physicsObj.WriteToSnapshot(msg); + msg.WriteBits(damagedState, 1); + msg.WriteFloat(activationRate); + msg.WriteFloat(deactivationRate); + msg.WriteFloat(undamageFadeRate); + msg.WriteBits(applyImpulseAttempts, 32); + msg.WriteBits(cachedContents, 32); + msg.WriteFloat(fade); + msg.WriteBits(nextCollideFxTime, 32); + msg.WriteBits(fieldState, 4); + msg.WriteBits(IsHidden(), 1); + + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_TIMEOFFSET]); + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_MODE]); +} + +void hhForceField::ReadFromSnapshot( const idBitMsgDelta &msg ) { + physicsObj.ReadFromSnapshot(msg); + damagedState = !!msg.ReadBits(1); + activationRate = msg.ReadFloat(); + deactivationRate = msg.ReadFloat(); + undamageFadeRate = msg.ReadFloat(); + applyImpulseAttempts = msg.ReadBits(32); + cachedContents = msg.ReadBits(32); + fade = msg.ReadFloat(); + nextCollideFxTime = msg.ReadBits(32); + fieldState = (States)msg.ReadBits(4); + bool hidden = !!msg.ReadBits(1); + if (IsHidden() != hidden) { + if (hidden) { + Hide(); + SetSkinByName( spawnArgs.GetString("skin_off" ) ); + GetPhysics()->SetContents( 0 ); + } + else { + Show(); + SetSkinByName( NULL ); + GetPhysics()->SetContents( cachedContents ); + } + } + + float f; + bool changed = false; + + f = msg.ReadFloat(); + changed = (changed || (renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] != f)); + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] = f; + + f = msg.ReadFloat(); + changed = (changed || (renderEntity.shaderParms[SHADERPARM_MODE] != f)); + renderEntity.shaderParms[SHADERPARM_MODE] = f; + + if (changed) { + UpdateModel(); + UpdateVisuals(); + } +} + +void hhForceField::ClientPredictionThink( void ) { + idEntity::ClientPredictionThink(); +} + +/* +=========== +hhForceField::ApplyImpulse +=========== +*/ +void hhForceField::ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &force ) { + + if (!ent->IsType(idActor::Type)) { + // Don't play sound for actor footsteps, landings. They play their own localized sounds for + // the sake of very large forcefields. + StartSound( "snd_impulse", SND_CHANNEL_ANY ); + } + EnterDamagedState(); + + // Apply the hit effect + trace_t trace; + idStr fxCollide = spawnArgs.GetString( "fx_collide" ); + + if ( fxCollide.Length() && gameLocal.time > nextCollideFxTime && point != GetOrigin()) { + // Trace to find normal + idVec3 dir = force; + dir.NormalizeFast(); + dir *= 200; + idVec3 start = point-dir; + idVec3 end = point+dir; + memset(&trace, 0, sizeof(trace)); + gameLocal.clip.TracePoint(trace, start, end, CONTENTS_FORCEFIELD, ent); + + if (trace.fraction < 1.0f) { + // Spawn fx oriented to normal of collision + hhFxInfo fxInfo; + fxInfo.SetNormal( trace.c.normal ); + BroadcastFxInfo( fxCollide, trace.c.point, mat3_identity, &fxInfo, 0, false ); //rww - changed to not broadcast + nextCollideFxTime = gameLocal.time + 200; + } + } +} + +/* +=========== +hhForceField::Ticker +=========== +*/ +void hhForceField::Ticker( void ) { + + if( fieldState == StatePreTurningOn ) { + idEntity* entityList[MAX_GENTITIES]; + idEntity* entity = NULL; + idPhysics* physics = NULL; + int numEntities = 0; + int numEntitiesImpulseAppliedTo = 0; + idVec3 force = DetermineForce() * GetAxis(); + + numEntities = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), MASK_SOLID, entityList, MAX_GENTITIES ); + for( int ix = 0; ix < numEntities; ++ix ) { + entity = entityList[ix]; + + if( !entity ) { + continue; + } + + if( entity == this ) { + continue; + } + + //Removing mass from any calculations regarding impulse + entity->ApplyImpulse( this, 0, entity->GetOrigin(), force * entity->GetPhysics()->GetMass() ); + numEntitiesImpulseAppliedTo++; + } + + --applyImpulseAttempts; + if( !numEntitiesImpulseAppliedTo || !applyImpulseAttempts ) { + fieldState = StateTurningOn; + GetPhysics()->SetContents( cachedContents ); + SetSkin( NULL ); + StartSound( "snd_start", SND_CHANNEL_ANY ); + + Show(); + + EnterTurningOnState(); + } + } else if( fieldState == StateTurningOn ) { + float deltaTime = MS2SEC( gameLocal.msec ); + + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] += activationRate * deltaTime; + if( renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] >= 1.0f ) { + EnterOnState(); + } + + UpdateVisuals(); + } else if( fieldState == StateTurningOff ) { + float deltaTime = MS2SEC( gameLocal.msec ); + + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] -= deactivationRate * deltaTime; + if( renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] <= 0.0f ) { + EnterOffState(); + } + + UpdateVisuals(); + } + + if( damagedState ) { + float deltaTime = MS2SEC( gameLocal.msec ); + + // Fade parm back to normal + fade -= undamageFadeRate * deltaTime; + if ( fade <= 0.0f ) { // Finished fading + fade = 0; + damagedState = false; + } + + SetShaderParm( SHADERPARM_MODE, fade ); + } + + if (!damagedState && (fieldState==StateOn || fieldState==StateOff)) { + BecomeInactive( TH_TICKER ); + } +} + +/* +=========== +hhForceField::DetermineThinnestAxis +=========== +*/ +int hhForceField::DetermineThinnestAxis() { + int best = 0; + idBounds bounds = GetPhysics()->GetBounds(); + for( int i = 1; i < 3; ++i ) { + if ( bounds[1][ i ] - bounds[0][ i ] < bounds[1][ best ] - bounds[0][ best ] ) { + best = i; + } + } + + return best; +} + +/* +=========== +hhForceField::DetermineForce +=========== +*/ +idVec3 hhForceField::DetermineForce() { + idVec3 force( vec3_origin ); + float forceMagnitude = spawnArgs.GetFloat( "forceMagnitude" ); + int dir = spawnArgs.GetBool( "positiveDir", "1" ) ? 1.0f : -1.0f; + + force[DetermineThinnestAxis()] = forceMagnitude * dir; + + return force; +} + +/* +=========== +hhForceField::IsAtRest + +Used to activate entites at rest +=========== +*/ +bool hhForceField::IsAtRest( int id ) const { + return ( fieldState != StateOff ); +} + +/* +=========== +hhForceField::Event_Activate +=========== +*/ +void hhForceField::Event_Activate( idEntity *activator ) { + switch( fieldState ) { + case StatePreTurningOn: + EnterTurningOffState(); + break; + case StateTurningOff: + EnterPreTurningOnState(); + break; + case StateTurningOn: + EnterTurningOffState(); + break; + case StateOn: + EnterTurningOffState(); + break; + case StateOff: + EnterPreTurningOnState(); + break; + } +} + +/* +=========== +hhForceField::EnterDamagedState +=========== +*/ +void hhForceField::EnterDamagedState() { + damagedState = true; + fade = 1.0f; + SetShaderParm( SHADERPARM_MODE, fade ); // Instantly change to the new shader + BecomeActive( TH_TICKER ); +} + +/* +=========== +hhForceField::EnterPreTurningOnState +=========== +*/ +void hhForceField::EnterPreTurningOnState() { + fieldState = StatePreTurningOn; + applyImpulseAttempts = spawnArgs.GetInt( "applyImpulseAttempts" ); + BecomeActive( TH_TICKER ); +} + +/* +=========== +hhForceField::EnterOnState +=========== +*/ +void hhForceField::EnterOnState() { + fieldState = StateOn; + StartSound( "snd_loop", SND_CHANNEL_IDLE ); + //HUMANHEAD PCF rww 05/15/06 - prevent stuck-in-forcefield by killing everything within + if (gameLocal.isMultiplayer) { + gameLocal.KillBoxMasked( this, CONTENTS_BODY ); + } + //HUMANHEAD END +} + +/* +=========== +hhForceField::EnterTurningOnState +=========== +*/ +void hhForceField::EnterTurningOnState() { + GetPhysics()->SetContents( cachedContents ); + SetSkin( NULL ); + StartSound( "snd_start", SND_CHANNEL_ANY ); + + Show(); + BecomeActive( TH_TICKER ); +} + +/* +=========== +hhForceField::EnterTurningOffState +=========== +*/ +void hhForceField::EnterTurningOffState() { + fieldState = StateTurningOff; + StopSound( SND_CHANNEL_IDLE ); + StartSound( "snd_stop", SND_CHANNEL_ANY ); + BecomeActive( TH_TICKER ); +} + +/* +=========== +hhForceField::EnterOffState +=========== +*/ +void hhForceField::EnterOffState() { + fieldState = StateOff; + if (spawnArgs.GetBool("isSimpleBox")) { + GetPhysics()->ActivateContactEntities(); + } + else { + // To allow complex shaped forcefields, we need to manually activate the physics of any contacts here + idClipModel *clipModels[ MAX_GENTITIES ]; + idClipModel *cm; + idBounds bounds = GetPhysics()->GetAbsBounds(); + bounds.ExpandSelf(bounds.GetRadius()*0.1f); // Expand the bounds by 10% to catch things on the surface + int num = gameLocal.clip.ClipModelsTouchingBounds( bounds, MASK_ALL, clipModels, MAX_GENTITIES ); + for ( int i=0;iGetEntity(); + if (hit && hit->GetPhysics()->IsAtRest() && + (hit->GetPhysics()->IsType(idPhysics_RigidBody::Type) || hit->GetPhysics()->IsType(idPhysics_AF::Type)) ) { + if ( !hit->IsType( hhVehicle::Type ) ) { //HUMANHEAD jsh PCF 5/26/06: fix 30hz vehicle console jittering + hit->GetPhysics()->Activate(); + } + } + } + } + + // HUMANHEAD PCF pdm 04/27/06: Unbind any bound projectiles + idEntity *ent; + idEntity *next; + for( ent = teamChain; ent != NULL; ent = next ) { + next = ent->GetTeamChain(); + if ( ent && ent->IsType( hhProjectile::Type ) ) { + ent->Unbind(); // bjk drops all bound projectiles such as arrows and mines + next = teamChain; + + //HUMANHEAD bjk PCF (4-28-06) - explode crawlers + if (ent->IsType(hhProjectileStickyCrawlerGrenade::Type)) { + static_cast(ent)->PostEventSec( &EV_Explode, 0.2f ); + } + } + } + + GetPhysics()->SetContents( 0 ); + Hide(); + SetSkinByName( spawnArgs.GetString("skin_off" ) ); // Set the force field to completely not draw +} + +//-------------------------------------------------------------------------- +// hhShuttleForcefield +//-------------------------------------------------------------------------- +CLASS_DECLARATION( idEntity, hhShuttleForceField ) + EVENT( EV_Activate, hhShuttleForceField::Event_Activate ) +END_CLASS + +void hhShuttleForceField::Spawn() { + nextCollideFxTime = gameLocal.time; + GetPhysics()->SetContents(CONTENTS_SOLID|CONTENTS_PLAYERCLIP); + + // Start in the on state + fieldState = StateOn; + fade.Init(gameLocal.time, 0, 1.0f, 1.0f); + SetShaderParm(SHADERPARM_TIMEOFFSET, fade.GetCurrentValue(gameLocal.time)); + + fl.networkSync = true; //rww +} + +void hhShuttleForceField::ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &force ) { + trace_t trace; + idStr fxCollide = spawnArgs.GetString( "fx_collide" ); + + if ( fxCollide.Length() && gameLocal.time > nextCollideFxTime && point != GetOrigin()) { + + // Trace to find normal + idVec3 dir = force; + dir.NormalizeFast(); + dir *= 200; + idVec3 start = point-dir; + idVec3 end = point+dir; + memset(&trace, 0, sizeof(trace)); + gameLocal.clip.TracePoint(trace, start, end, CONTENTS_SOLID, ent); + + if (trace.fraction < 1.0f) { + // Spawn fx oriented to normal of collision + hhFxInfo fxInfo; + fxInfo.SetNormal( trace.c.normal ); + BroadcastFxInfo( fxCollide, trace.c.point, mat3_identity, &fxInfo ); + nextCollideFxTime = gameLocal.time + 200; + + StartSound( "snd_collide", SND_CHANNEL_ANY, 0, true, NULL ); + ActivatePrefixed( "triggerCollide", this ); // bg: Feedback hook. + } + } +} + +void hhShuttleForceField::Ticker() { + SetShaderParm(SHADERPARM_TIMEOFFSET, fade.GetCurrentValue(gameLocal.time)); + if (fade.IsDone(gameLocal.time)) { + if (fieldState == StateTurningOn) { + // Entering On state + fieldState = StateOn; + GetPhysics()->SetContents(CONTENTS_SOLID|CONTENTS_PLAYERCLIP); + } + else if (fieldState == StateTurningOff) { + // Entering Off state + fieldState = StateOff; + GetPhysics()->SetContents(0); + } + + BecomeInactive(TH_TICKER); + } +} + +void hhShuttleForceField::Event_Activate(idEntity *activator) { + int duration = SEC2MS(spawnArgs.GetFloat("fade_duration")); + float currentFade = fade.GetCurrentValue(gameLocal.time); + if (fieldState == StateOn || fieldState == StateTurningOn) { + // Entering TurningOff state + fade.Init(gameLocal.time, duration, currentFade, 0.0f); + fieldState = StateTurningOff; + } + else if (fieldState == StateOff || fieldState == StateTurningOff) { + // Entering TurningOn state + fade.Init(gameLocal.time, duration, currentFade, 1.0f); + fieldState = StateTurningOn; + //HUMANHEAD PCF rww 05/15/06 - prevent stuck-in-forcefield by killing everything within + if (gameLocal.isMultiplayer) { + gameLocal.KillBoxMasked( this, CONTENTS_BODY ); + } + //HUMANHEAD END + } + BecomeActive( TH_TICKER ); +} + +void hhShuttleForceField::Save( idSaveGame *savefile ) const { + savefile->WriteInt( nextCollideFxTime ); + savefile->WriteInt( fieldState ); + savefile->WriteFloat( fade.GetStartTime() ); // idInterpolate + savefile->WriteFloat( fade.GetDuration() ); + savefile->WriteFloat( fade.GetStartValue() ); + savefile->WriteFloat( fade.GetEndValue() ); +} + +void hhShuttleForceField::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadInt( nextCollideFxTime ); + savefile->ReadInt( reinterpret_cast ( fieldState ) ); + + savefile->ReadFloat( set ); // idInterpolate + fade.SetStartTime( set ); + savefile->ReadFloat( set ); + fade.SetDuration( set ); + savefile->ReadFloat( set ); + fade.SetStartValue(set); + savefile->ReadFloat( set ); + fade.SetEndValue( set ); +} + +void hhShuttleForceField::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(GetPhysics()->GetContents(), 32); + + msg.WriteBits(nextCollideFxTime, 32); + msg.WriteBits(fieldState, 8); + + msg.WriteFloat(fade.GetStartTime()); + msg.WriteFloat(fade.GetDuration()); + msg.WriteFloat(fade.GetStartValue()); + msg.WriteFloat(fade.GetEndValue()); + + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_TIMEOFFSET]); + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_MODE]); +} + +void hhShuttleForceField::ReadFromSnapshot( const idBitMsgDelta &msg ) { + int contents = msg.ReadBits(32); + if (contents != GetPhysics()->GetContents()) { + GetPhysics()->SetContents(contents); + } + + nextCollideFxTime = msg.ReadBits(32); + fieldState = (States)msg.ReadBits(8); + + fade.SetStartTime(msg.ReadFloat()); + fade.SetDuration(msg.ReadFloat()); + fade.SetStartValue(msg.ReadFloat()); + fade.SetEndValue(msg.ReadFloat()); + + float f; + bool changed = false; + + f = msg.ReadFloat(); + changed = (changed || (renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] != f)); + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] = f; + + f = msg.ReadFloat(); + changed = (changed || (renderEntity.shaderParms[SHADERPARM_MODE] != f)); + renderEntity.shaderParms[SHADERPARM_MODE] = f; + + if (changed) { + UpdateVisuals(); + } +} + +void hhShuttleForceField::ClientPredictionThink( void ) { + idEntity::ClientPredictionThink(); +} diff --git a/src/Prey/game_forcefield.h b/src/Prey/game_forcefield.h new file mode 100644 index 0000000..72f65f6 --- /dev/null +++ b/src/Prey/game_forcefield.h @@ -0,0 +1,94 @@ +#ifndef __GAME_FORCEFIELD_H__ +#define __GAME_FORCEFIELD_H__ + +class hhForceField : public idEntity { + CLASS_PROTOTYPE( hhForceField ); + +public: + void Spawn(); + void ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &force ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +protected: + void Ticker(); + + int DetermineThinnestAxis(); + idVec3 DetermineForce(); + + virtual bool IsAtRest( int id ) const; + + void EnterDamagedState(); + void EnterPreTurningOnState(); + void EnterTurningOnState(); + void EnterOnState(); + void EnterTurningOffState(); + void EnterOffState(); + +protected: + void Event_Activate( idEntity *activator ); + +protected: + enum States { + StatePreTurningOn = 0, + StateTurningOn, + StateOn, + StateTurningOff, + StateOff + } fieldState; + bool damagedState; + + float activationRate; + float deactivationRate; + float undamageFadeRate; + + int applyImpulseAttempts; + + int cachedContents; + + float fade; + + int nextCollideFxTime; + + hhPhysics_StaticForceField physicsObj; +}; + + +class hhShuttleForceField : public idEntity { + CLASS_PROTOTYPE( hhShuttleForceField ); + +public: + void Spawn(); + void ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &force ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + void Ticker(); + +protected: + void Event_Activate(idEntity *activator); + + enum States { + StatePreTurningOn = 0, + StateTurningOn, + StateOn, + StateTurningOff, + StateOff + } fieldState; + +private: + int nextCollideFxTime; + idInterpolate fade; +}; + +#endif /* __GAME_FORCEFIELD_H__ */ diff --git a/src/Prey/game_fxinfo.cpp b/src/Prey/game_fxinfo.cpp new file mode 100644 index 0000000..5609647 --- /dev/null +++ b/src/Prey/game_fxinfo.cpp @@ -0,0 +1,343 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +CLASS_DECLARATION( idClass, hhFxInfo ) +END_CLASS + + +hhFxInfo::hhFxInfo() { + Reset(); +} + +hhFxInfo::hhFxInfo( const hhFxInfo* fxInfo ) { + Assign( fxInfo ); +} + +hhFxInfo::hhFxInfo( const hhFxInfo& fxInfo ) { + Assign( &fxInfo ); +} + +void hhFxInfo::Save(idSaveGame *savefile) const { + fxInfoFlags_s infoFlags = flags; + LittleBitField( &infoFlags, sizeof( infoFlags ) ); + savefile->Write( &infoFlags, sizeof( infoFlags ) ); + + savefile->WriteVec3( normal ); + savefile->WriteVec3( incomingVector ); + savefile->WriteVec3( bounceVector ); + savefile->WriteString( bindBone ); + entity.Save(savefile); +} + +void hhFxInfo::Restore( idRestoreGame *savefile ) { + savefile->Read( &flags, sizeof( flags ) ); + LittleBitField( &flags, sizeof( flags ) ); + + savefile->ReadVec3( normal ); + savefile->ReadVec3( incomingVector ); + savefile->ReadVec3( bounceVector ); + savefile->ReadString( bindBone ); + entity.Restore(savefile); +} + +void hhFxInfo::WriteToSnapshot( idBitMsgDelta &msg ) const { + /* + msg.WriteBits(flags.normalIsSet, 1); + msg.WriteBits(flags.incomingIsSet, 1); + msg.WriteBits(flags.bounceIsSet, 1); + msg.WriteBits(flags.start, 1); + msg.WriteBits(flags.removeWhenDone, 1); + msg.WriteBits(flags.toggle, 1); + msg.WriteBits(flags.onlyVisibleInSpirit, 1); + msg.WriteBits(flags.onlyInvisibleInSpirit, 1); + msg.WriteBits(flags.useWeaponDepthHack, 1); + msg.WriteBits(flags.bNoRemoveWhenUnbound, 1); + + msg.WriteFloat(normal.x); + msg.WriteFloat(normal.y); + msg.WriteFloat(normal.z); + msg.WriteFloat(incomingVector.x); + msg.WriteFloat(incomingVector.y); + msg.WriteFloat(incomingVector.z); + msg.WriteFloat(bounceVector.x); + msg.WriteFloat(bounceVector.y); + msg.WriteFloat(bounceVector.z); + msg.WriteString(bindBone); + msg.WriteBits(entity.GetSpawnId(), 32); + */ + + msg.WriteBits(flags.normalIsSet, 1); + msg.WriteBits(flags.removeWhenDone, 1); + msg.WriteFloat(normal.x); + msg.WriteFloat(normal.y); + msg.WriteFloat(normal.z); +} + +void hhFxInfo::ReadFromSnapshot( const idBitMsgDelta &msg ) { + /* + flags.normalIsSet = !!msg.ReadBits(1); + flags.incomingIsSet = !!msg.ReadBits(1); + flags.bounceIsSet = !!msg.ReadBits(1); + flags.start = !!msg.ReadBits(1); + flags.removeWhenDone = !!msg.ReadBits(1); + flags.toggle = !!msg.ReadBits(1); + flags.onlyVisibleInSpirit = !!msg.ReadBits(1); + flags.onlyInvisibleInSpirit = !!msg.ReadBits(1); + flags.useWeaponDepthHack = !!msg.ReadBits(1); + flags.bNoRemoveWhenUnbound = !!msg.ReadBits(1); + + normal.x = msg.ReadFloat(); + normal.y = msg.ReadFloat(); + normal.z = msg.ReadFloat(); + incomingVector.x = msg.ReadFloat(); + incomingVector.y = msg.ReadFloat(); + incomingVector.z = msg.ReadFloat(); + bounceVector.x = msg.ReadFloat(); + bounceVector.y = msg.ReadFloat(); + bounceVector.z = msg.ReadFloat(); + + char buf[128]; + msg.ReadString(buf, 128); + bindBone = buf; + entity.SetSpawnId(msg.ReadBits(32)); + */ + + flags.normalIsSet = !!msg.ReadBits(1); + flags.removeWhenDone = !!msg.ReadBits(1); + normal.x = msg.ReadFloat(); + normal.y = msg.ReadFloat(); + normal.z = msg.ReadFloat(); +} + +hhFxInfo& hhFxInfo::Assign( const hhFxInfo* fxInfo ) { + Reset(); + + if( !fxInfo ) { + return *this; + } + + if( fxInfo->NormalIsSet() ) { + SetNormal( fxInfo->GetNormal() ); + } + + if( fxInfo->IncomingVectorIsSet() ) { + SetIncomingVector( fxInfo->GetIncomingVector() ); + } + + if( fxInfo->BounceVectorIsSet() ) { + SetBounceVector( fxInfo->GetBounceVector() ); + } + + if( fxInfo->EntityIsSet() ) { + SetEntity( fxInfo->GetEntity() ); + } + + if( fxInfo->BindBoneIsSet() ) { + SetBindBone( fxInfo->GetBindBone() ); + } + + SetStart( fxInfo->StartIsSet() ); + + RemoveWhenDone( fxInfo->RemoveWhenDone() ); + + Toggle( fxInfo->Toggle() ); + + OnlyVisibleInSpirit( fxInfo->OnlyVisibleInSpirit() ); + OnlyInvisibleInSpirit( fxInfo->OnlyInvisibleInSpirit() ); + UseWeaponDepthHack( fxInfo->UseWeaponDepthHack() ); + NoRemoveWhenUnbound( fxInfo->NoRemoveWhenUnbound() ); + + return *this; +} + +hhFxInfo& hhFxInfo::operator=( const hhFxInfo* fxInfo ) { + return Assign( fxInfo ); +} + +hhFxInfo& hhFxInfo::operator=( const hhFxInfo& fxInfo ) { + return Assign( &fxInfo ); +} + +void hhFxInfo::SetNormal( const idVec3 &v ) { + assert( v.Length() ); + normal = v; + flags.normalIsSet = true; +} + +void hhFxInfo::SetIncomingVector( const idVec3 &v ) { + assert( v.Length() ); + incomingVector = v; + flags.incomingIsSet = true; +} + +void hhFxInfo::SetBounceVector( const idVec3 &v ) { + assert( v.Length() ); + bounceVector = v; + flags.bounceIsSet = true; +} + +void hhFxInfo::SetEntity( idEntity *e ) { + entity = e; +} + +void hhFxInfo::SetBindBone( const char* bindBone ) { + this->bindBone = bindBone; +} + +void hhFxInfo::SetStart(const bool start) { + flags.start = start; +} + +void hhFxInfo::RemoveWhenDone( const bool removeWhenDone ) { + flags.removeWhenDone = removeWhenDone; +} + +void hhFxInfo::Toggle( const bool tf ) { + flags.toggle = tf; +} + +void hhFxInfo::OnlyVisibleInSpirit( const bool spirit ) { + flags.onlyVisibleInSpirit = spirit; +} + +void hhFxInfo::OnlyInvisibleInSpirit( const bool spirit ) { + flags.onlyInvisibleInSpirit = spirit; +} + +void hhFxInfo::UseWeaponDepthHack( const bool weaponDepthHack ) { + flags.useWeaponDepthHack = weaponDepthHack; +} + +void hhFxInfo::Triggered( const bool tf ) { + flags.triggered = tf; +} + +const idVec3& hhFxInfo::GetNormal( ) const { + assert( flags.normalIsSet ); + return( normal ); +} + +const idVec3& hhFxInfo::GetIncomingVector( ) const { + assert( flags.incomingIsSet ); + return( incomingVector ); +} + +const idVec3& hhFxInfo::GetBounceVector( ) const { + assert( flags.bounceIsSet ); + return( bounceVector ); +} + +const char* hhFxInfo::GetBindBone() const { + return bindBone.c_str(); +} + +idEntity* const hhFxInfo::GetEntity( ) const { + return( entity.GetEntity() ); +} + +bool hhFxInfo::RemoveWhenDone() const { + return flags.removeWhenDone; +} + +bool hhFxInfo::StartIsSet() const { + return flags.start; +} + +bool hhFxInfo::NormalIsSet( ) const { + return flags.normalIsSet; +} + +bool hhFxInfo::IncomingVectorIsSet( ) const { + return flags.incomingIsSet; +} + +bool hhFxInfo::BounceVectorIsSet( ) const { + return flags.bounceIsSet; +} + +bool hhFxInfo::BindBoneIsSet() const { + return bindBone.Length() > 0; +} + +bool hhFxInfo::EntityIsSet() const { + return entity.IsValid(); +} + +bool hhFxInfo::Toggle() const { + return flags.toggle; +} + +bool hhFxInfo::OnlyVisibleInSpirit( void ) const { + return flags.onlyVisibleInSpirit; +} + +bool hhFxInfo::OnlyInvisibleInSpirit( void ) const { + return flags.onlyInvisibleInSpirit; +} + +bool hhFxInfo::UseWeaponDepthHack() const { + return flags.useWeaponDepthHack; +} + +bool hhFxInfo::Triggered() const { + return flags.triggered; +} + +void hhFxInfo::NoRemoveWhenUnbound( const bool noRemove ) { + flags.bNoRemoveWhenUnbound = noRemove; +} + +bool hhFxInfo::NoRemoveWhenUnbound() const { + return flags.bNoRemoveWhenUnbound; +} + +void hhFxInfo::Reset() { + entity = NULL; + normal.Zero(); bounceVector.Zero(); + incomingVector.Zero(); + bindBone.Empty(); + flags.normalIsSet = false; + flags.bounceIsSet = false; + flags.incomingIsSet = false; + flags.start = true; + flags.removeWhenDone = true; + flags.toggle = false; // jrm + flags.onlyVisibleInSpirit = false; + flags.onlyInvisibleInSpirit = false; // tmj + flags.useWeaponDepthHack = false; + flags.triggered = false; // mdl + flags.bNoRemoveWhenUnbound = false; // mdc +} + +bool hhFxInfo::GetAxisFor( int which, idVec3& dir ) const { + switch ( which ) { + case AXIS_NORMAL: + if ( NormalIsSet() ) { dir = GetNormal(); return true; } + else { + if ( g_debugFX.GetBool() ) gameLocal.Warning("Tried to get fxInfo Normal when it isn't set"); + } + break; + case AXIS_BOUNCE: + if ( BounceVectorIsSet() ) { dir = GetBounceVector(); return true; } + else { + if ( g_debugFX.GetBool() ) gameLocal.Warning("Tried to get fxInfo Bounce Vector when it isn't set"); + } + break; + case AXIS_CUSTOMLOCAL: + // handled in alternate path + if ( g_debugFX.GetBool() ) { + gameLocal.Warning("Using customlocal without axis"); + } + break; + case AXIS_INCOMING: + if ( IncomingVectorIsSet() ) { dir = GetIncomingVector(); return true; } + else { + if ( g_debugFX.GetBool() ) gameLocal.Warning("Tried to get fxInfo Incoming Vector when it isn't set"); + } + break; + } + return false; +} \ No newline at end of file diff --git a/src/Prey/game_fxinfo.h b/src/Prey/game_fxinfo.h new file mode 100644 index 0000000..31d2f52 --- /dev/null +++ b/src/Prey/game_fxinfo.h @@ -0,0 +1,175 @@ +#ifndef __HH_FX_INFO_H +#define __HH_FX_INFO_H + +class hhFxInfo : public idClass { + CLASS_PROTOTYPE( hhFxInfo ); + +public: + hhFxInfo(); + hhFxInfo( const hhFxInfo* fxInfo ); + hhFxInfo( const hhFxInfo& fxInfo ); + hhFxInfo& Assign( const hhFxInfo* fxInfo ); + hhFxInfo& operator=( const hhFxInfo* fxInfo ); + hhFxInfo& operator=( const hhFxInfo& fxInfo ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //HUMANHEAD rww + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + //HUMANHEAD END + + void SetNormal( const idVec3 &v ); + void SetIncomingVector( const idVec3 &v ); + void SetBounceVector( const idVec3 &v ); + void SetEntity( idEntity *e ); + void SetBindBone( const char* bindBone ); + void SetStart( const bool start ); + void RemoveWhenDone( const bool removeWhenDone ); + void Toggle( const bool tf ); + void OnlyVisibleInSpirit( const bool spirit ); + void OnlyInvisibleInSpirit( const bool spirit ); + void UseWeaponDepthHack( const bool weaponDepthHack ); + void Triggered( const bool tf ); + + void NoRemoveWhenUnbound( const bool noRemove ); + + const idVec3& GetNormal( ) const; + const idVec3& GetIncomingVector( ) const; + const idVec3& GetBounceVector( ) const; + const char* GetBindBone() const; + idEntity* const GetEntity( ) const; + + bool RemoveWhenDone() const; + bool StartIsSet() const; + bool NormalIsSet( ) const; + bool IncomingVectorIsSet( ) const; + bool BounceVectorIsSet( ) const; + bool BindBoneIsSet() const; + bool EntityIsSet() const; + bool Toggle() const; + bool OnlyVisibleInSpirit( void ) const; + bool OnlyInvisibleInSpirit( void ) const; + bool UseWeaponDepthHack() const; + bool Triggered() const; + + bool NoRemoveWhenUnbound() const; + + void Reset(); + + bool GetAxisFor( int which, idVec3& dir ) const; + + void WriteToBitMsg( idBitMsg* msg ) const; + void ReadFromBitMsg( const idBitMsg* bitMsg ); + +protected: + struct fxInfoFlags_s { + bool normalIsSet; + bool incomingIsSet; + bool bounceIsSet; + bool start; + bool removeWhenDone; + bool toggle; + bool onlyVisibleInSpirit; // CJR + bool onlyInvisibleInSpirit; // tmj + bool useWeaponDepthHack; + bool bNoRemoveWhenUnbound; //mdc + bool triggered; // mdl + } flags; + + idVec3 normal; + idVec3 incomingVector; + idVec3 bounceVector; + idStr bindBone; + idEntityPtr entity; +}; + +ID_INLINE void hhFxInfo::WriteToBitMsg( idBitMsg* msg ) const { + bool varIsSet = false; + + varIsSet = NormalIsSet(); + msg->WriteBool( varIsSet ); + if( varIsSet ) { + msg->WriteDir( GetNormal(), 24 ); + } + + varIsSet = IncomingVectorIsSet(); + msg->WriteBool( varIsSet ); + if( varIsSet ) { + msg->WriteDir( GetIncomingVector(), 24 ); + } + + varIsSet = BounceVectorIsSet(); + msg->WriteBool( varIsSet ); + if( varIsSet ) { + msg->WriteVec3( GetBounceVector() ); + } + + varIsSet = EntityIsSet(); + msg->WriteBool( varIsSet ); + if( varIsSet ) { + msg->WriteBits( GetEntity()->entityNumber, GENTITYNUM_BITS ); + } + + varIsSet = BindBoneIsSet(); + msg->WriteBool( varIsSet ); + if( varIsSet ) { + msg->WriteString( GetBindBone() ); + } + + msg->WriteBool( StartIsSet() ); + + msg->WriteBool( RemoveWhenDone() ); + + msg->WriteBool( Toggle() ); + + msg->WriteBool( OnlyVisibleInSpirit() ); + msg->WriteBool( OnlyInvisibleInSpirit() ); + msg->WriteBool( UseWeaponDepthHack() ); + msg->WriteBool( NoRemoveWhenUnbound() ); +} + +ID_INLINE void hhFxInfo::ReadFromBitMsg( const idBitMsg* msg ) { + bool varIsSet = false; + + varIsSet = msg->ReadBool(); + if( varIsSet ) { + SetNormal( msg->ReadDir(24) ); + } + + varIsSet = msg->ReadBool(); + if( varIsSet ) { + SetIncomingVector( msg->ReadDir(24) ); + } + + varIsSet = msg->ReadBool(); + if( varIsSet ) { + SetBounceVector( msg->ReadVec3() ); + } + + varIsSet = msg->ReadBool(); + if( varIsSet ) { + SetEntity( gameLocal.entities[ msg->ReadBits(GENTITYNUM_BITS) ] ); + } + + varIsSet = msg->ReadBool(); + if( varIsSet ) { + char boneName[256]; + msg->ReadString( boneName, 256 ); + SetBindBone( boneName ); + } + + SetStart( msg->ReadBool() ); + + RemoveWhenDone( msg->ReadBool() ); + + Toggle( msg->ReadBool() ); + + OnlyVisibleInSpirit( msg->ReadBool() ); + OnlyInvisibleInSpirit( msg->ReadBool() ); + UseWeaponDepthHack( msg->ReadBool() ); + NoRemoveWhenUnbound( msg->ReadBool() ); +} + +#endif \ No newline at end of file diff --git a/src/Prey/game_gibbable.cpp b/src/Prey/game_gibbable.cpp new file mode 100644 index 0000000..a228b51 --- /dev/null +++ b/src/Prey/game_gibbable.cpp @@ -0,0 +1,146 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/********************************************************************** + +hhGibbable + +**********************************************************************/ + +const idEventDef EV_Respawn(""); + +CLASS_DECLARATION( hhAnimatedEntity, hhGibbable ) + EVENT( EV_Activate, hhGibbable::Event_Activate) + EVENT( EV_PlayIdle, hhGibbable::Event_PlayIdle) + EVENT( EV_Respawn, hhGibbable::Event_Respawn) +END_CLASS + +// Called during idEntity::Spawn +void hhGibbable::SetModel( const char *modelname ) { + // NLATODO - Is this called still? + hhAnimatedEntity::SetModel( modelname ); + + bool bAnimates = spawnArgs.FindKey("anim idle") != NULL; +} + +void hhGibbable::Spawn(void) { + + bVertexColorFade = spawnArgs.GetBool("materialFade"); + if (bVertexColorFade) { + SetDeformation(DEFORMTYPE_VERTEXCOLOR, 1.0f); + } + + //HUMANHEAD: aob - Flynn wanted some gibbables to be triggered only + fl.takedamage = !spawnArgs.GetBool("noDamage", "0"); + + // setup the clipModel +// GetPhysics()->SetContents( CONTENTS_SOLID ); + GetPhysics()->SetContents( CONTENTS_BODY | CONTENTS_RENDERMODEL ); + + idleAnim = GetAnimator()->GetAnim("idle"); + painAnim = GetAnimator()->GetAnim("pain"); + + idleChannel = GetChannelForAnim( "idle" ); + painChannel = GetChannelForAnim( "pain" ); + + PostEventMS(&EV_PlayIdle, 1000); +} + +void hhGibbable::Save(idSaveGame *savefile) const { + savefile->WriteInt( idleAnim ); + savefile->WriteInt( painAnim ); + savefile->WriteInt( idleChannel ); + savefile->WriteInt( painChannel ); + savefile->WriteBool( bVertexColorFade ); +} + +void hhGibbable::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( idleAnim ); + savefile->ReadInt( painAnim ); + savefile->ReadInt( idleChannel ); + savefile->ReadInt( painChannel ); + savefile->ReadBool( bVertexColorFade ); +} + +bool hhGibbable::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + + // Adjust vertex color + if (bVertexColorFade) { + float fadeAlpha = idMath::ClampFloat(0.0f, 1.0f, ((float)health / spawnArgs.GetFloat("health"))); + SetDeformation(DEFORMTYPE_VERTEXCOLOR, fadeAlpha); + } + + if (painAnim) { + GetAnimator()->PlayAnim( painChannel, painAnim, gameLocal.time, 0); + } + + return( hhAnimatedEntity::Pain(inflictor, attacker, damage, dir, location) ); +} + +void hhGibbable::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + Explode(attacker); +} + +int hhGibbable::DetermineThinnestAxis() { + int best = 0; + idBounds bounds = GetPhysics()->GetBounds(); + for( int i = 1; i < 3; ++i ) { + if ( bounds[1][ i ] - bounds[0][ i ] < bounds[1][ best ] - bounds[0][ best ] ) { + best = i; + } + } + + return best; +} + +void hhGibbable::Explode(idEntity *activator) { + hhFxInfo fxInfo; + + Hide(); + fl.takedamage = false; + GetPhysics()->SetContents( 0 ); + ActivateTargets( activator ); + SetSkinByName(NULL); + if ( spawnArgs.GetFloat( "respawn", "0" ) ) { + PostEventSec( &EV_Respawn, spawnArgs.GetFloat( "respawn", "0" ) ); + } else { + PostEventMS( &EV_Remove, 200 ); // Remove after a small delay to allow sound commands to execute + } + StartSound( "snd_gib", SND_CHANNEL_ANY ); + + // Find thinnest axis in the bounds and use for fx normal + idVec3 thinnest = vec3_origin; + int axisIndex = DetermineThinnestAxis(); + thinnest[axisIndex] = 1.0f; + thinnest *= GetAxis(); + + fxInfo.RemoveWhenDone( true ); + fxInfo.SetNormal(thinnest); + // Spawn FX system for gib + BroadcastFxInfo( spawnArgs.GetString("fx_gib"), GetOrigin(), GetAxis(), &fxInfo ); + + // Spawn gibs + if (spawnArgs.FindKey("def_debrisspawner")) { + hhUtils::SpawnDebrisMass(spawnArgs.GetString("def_debrisspawner"), this ); + } +} + +void hhGibbable::Event_Respawn() { + GetPhysics()->SetContents( CONTENTS_BODY | CONTENTS_RENDERMODEL ); + fl.takedamage = true; + Show(); +} + +void hhGibbable::Event_Activate( idEntity *activator ) { + Explode(activator); +} + +void hhGibbable::Event_PlayIdle( void ) { + if (idleAnim) { + GetAnimator()->ClearAllAnims(gameLocal.time, 0); + GetAnimator()->CycleAnim( idleChannel, idleAnim, gameLocal.time, 0); + } +} + diff --git a/src/Prey/game_gibbable.h b/src/Prey/game_gibbable.h new file mode 100644 index 0000000..600793a --- /dev/null +++ b/src/Prey/game_gibbable.h @@ -0,0 +1,31 @@ +#ifndef __PREY_GIBBABLE_H +#define __PREY_GIBBABLE_H + +class hhGibbable : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhGibbable ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Explode(idEntity *activator); + virtual bool Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void SetModel( const char *modelname ); + +protected: + void Event_Activate( idEntity *activator ); + void Event_PlayIdle( void ); + void Event_Respawn( void ); + int DetermineThinnestAxis(); + +protected: + int idleAnim; + int painAnim; + + int idleChannel; + int painChannel; + bool bVertexColorFade; +}; + +#endif diff --git a/src/Prey/game_gravityswitch.cpp b/src/Prey/game_gravityswitch.cpp new file mode 100644 index 0000000..41cc127 --- /dev/null +++ b/src/Prey/game_gravityswitch.cpp @@ -0,0 +1,204 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_CheckAgain( "", NULL ); + +CLASS_DECLARATION( hhDamageTrigger, hhGravitySwitch ) + EVENT( EV_PostSpawn, hhGravitySwitch::Event_PostSpawn ) + EVENT( EV_CheckAgain, hhGravitySwitch::Event_CheckAgain ) +END_CLASS + + +void hhGravitySwitch::Spawn( void ) { + fl.networkSync = true; + + fl.clientEvents = true; + + CancelEvents(&EV_PostSpawn); // Parent actually already posted one + PostEventMS(&EV_PostSpawn, 0); +} + +void hhGravitySwitch::Event_PostSpawn(void) { + idVec3 origin = GetOrigin() - GetAxis()[0]*10.0f; + + // Determine axis of emitter + // NOTE: axis of gravityswitch is reversed, axis of emitters is: identity==up + idVec3 up(0.0f, 0.0f, 1.0f); + idVec3 back(-1.0f, 0.0f, 0.0f); + idMat3 axis = up.ToMat3().Inverse()*back.ToMat3().Inverse()*GetAxis(); //a mess to * axis to get the emitter to point the same way as the model + + idDict args; + args.Clear(); + args.SetVector("origin", origin); + args.SetMatrix("rotation", axis); + + const char *effectName = spawnArgs.GetString("def_effect", NULL); + if (effectName && *effectName) { + effect = static_cast(gameLocal.SpawnClientObject(effectName, &args) ); + if (effect.IsValid()) { + effect->Hide(); + } + } +} + +hhGravitySwitch::~hhGravitySwitch() { + SAFE_REMOVE( effect ); +} + +void hhGravitySwitch::Save(idSaveGame *savefile) const { + effect.Save(savefile); +} + +void hhGravitySwitch::Restore( idRestoreGame *savefile ) { + effect.Restore(savefile); +} + +void hhGravitySwitch::Damage(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef && damageDef->GetBool("radius")) { + return; // Gravity switches are immune to all splash damage + } + hhDamageTrigger::Damage(inflictor, attacker, dir, damageDefName, damageScale, location); +} + +idVec3 hhGravitySwitch::GetGravityVector() { + idVec3 vector; + + if (!spawnArgs.GetVector("vector", NULL, vector)) { + float strength = spawnArgs.GetFloat("strength", "1"); + idVec3 direction = GetPhysics()->GetAxis()[0]; + vector = direction * strength * DEFAULT_GRAVITY; + } + + return vector; +} + +void hhGravitySwitch::SetGravityVector(idEntity *activator) { + idVec3 newGravity = GetGravityVector(); + bool bSwitchedGravity = false; + + int numTargets = targets.Num(); + for ( int ix = 0; ix < numTargets; ix++) { + idEntity *ent = targets[ix].GetEntity(); + if (ent && ent->IsType(hhGravityZone::Type)) { + hhGravityZone *zone = static_cast(ent); + + idVec3 zoneGravity = zone->GetDestinationGravity(); + if (!zoneGravity.Compare(newGravity, VECTOR_EPSILON)) { + zone->SetGravityOnZone( newGravity ); + bSwitchedGravity = true; + } + } + } + + if (bSwitchedGravity) { + if (gameLocal.isMultiplayer && !gameLocal.isClient) { //rww - play sound when shot in MP + StartSound("snd_gravity_mpshot", SND_CHANNEL_ANY, 0, true); + } + ActivateTargets(activator); + BecomeActive(TH_TICKER); + } +} + +void hhGravitySwitch::Ticker() { + // See if our gravity zone(s) are following our gravity + int numTargets = 1; // only need to check one, they are all assumed to be the same. If not, we have problems anyway. + for ( int ix = 0; ix < numTargets; ix++) { + idEntity *ent = targets[ix].GetEntity(); + if (ent && ent->IsType(hhGravityZone::Type)) { + hhGravityZone *zone = static_cast(ent); + if (zone->GetDestinationGravity().Compare( GetGravityVector(), VECTOR_EPSILON )) { + // Zone is still following my gravity, emit smoke + if (effect.IsValid() && effect->IsHidden()) { + effect->Show(); + } + return; + } + } + } + + // If not, stop thinking about it. + BecomeInactive(TH_TICKER); + if (effect.IsValid() && !effect->IsHidden()) { + effect->Hide(); + } + + // Check again in a while in case another switch turns the zone onto my gravity + CancelEvents(&EV_CheckAgain); + PostEventMS(&EV_CheckAgain, 1000); +} + +void hhGravitySwitch::Event_CheckAgain() { + BecomeActive(TH_TICKER); // Check again +} + +void hhGravitySwitch::TriggerAction(idEntity *activator) { + + SetGravityVector(activator); + + // Handle default trigger behavior + // Hack, make targetlist appear to be empty so ActivateTargets() does nothing, we handle this ourself + int num = targets.Num(); + targets.SetNum(0, false); + hhDamageTrigger::TriggerAction(activator); + targets.SetNum(num, false); +} + +void hhGravitySwitch::Event_Enable() { + if (!bEnabled) { + StartSound("snd_gravity_enable", SND_CHANNEL_ANY, 0, true); + } + SetShaderParm(SHADERPARM_TIMEOFFSET, -MS2SEC(gameLocal.time)); + SetShaderParm(SHADERPARM_MISC, 1); + bEnabled = true; + fl.takedamage = true; + GetPhysics()->SetContents( CONTENTS_SHOOTABLE|CONTENTS_IKCLIP|CONTENTS_SHOOTABLEBYARROW ); + BecomeActive(TH_TICKER); +} + +void hhGravitySwitch::Event_Disable() { + if (bEnabled) { + StartSound("snd_gravity_disable", SND_CHANNEL_ANY, 0, true); + } + SetShaderParm(SHADERPARM_MISC, 0); + GetPhysics()->SetContents( CONTENTS_IKCLIP ); + fl.takedamage = false; + bEnabled = false; + BecomeInactive(TH_TICKER); + if (effect.IsValid()) { + effect->Hide(); + } +} + +void hhGravitySwitch::WriteToSnapshot( idBitMsgDelta &msg ) const { + GetPhysics()->WriteToSnapshot(msg); + msg.WriteBits(bEnabled, 1); + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_TIMEOFFSET]); + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_MISC]); + + if (effect.IsValid()) { + msg.WriteBits(effect->IsHidden(), 1); + } + else { + msg.WriteBits(0, 1); + } +} + +void hhGravitySwitch::ReadFromSnapshot( const idBitMsgDelta &msg ) { + GetPhysics()->ReadFromSnapshot(msg); + bEnabled = !!msg.ReadBits(1); + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] = msg.ReadFloat(); + renderEntity.shaderParms[SHADERPARM_MISC] = msg.ReadFloat(); + + bool fxHidden = !!msg.ReadBits(1); + if (effect.IsValid() && fxHidden != effect->IsHidden()) { + if (fxHidden) { + effect->Hide(); + } + else { + effect->Show(); + } + } +} diff --git a/src/Prey/game_gravityswitch.h b/src/Prey/game_gravityswitch.h new file mode 100644 index 0000000..e480b39 --- /dev/null +++ b/src/Prey/game_gravityswitch.h @@ -0,0 +1,35 @@ +#ifndef __GAME_GRAVITYSWITCH_H__ +#define __GAME_GRAVITYSWITCH_H__ + +class hhGravitySwitch : public hhDamageTrigger { +public: + CLASS_PROTOTYPE( hhGravitySwitch ); + + virtual ~hhGravitySwitch(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + // Overridden methods + virtual void Ticker(); + virtual void Damage(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location); + virtual void TriggerAction(idEntity *activator); + virtual void Event_Enable(); + virtual void Event_Disable(); + virtual void Event_PostSpawn(); + void Event_CheckAgain(); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + +protected: + void SetGravityVector(idEntity *activator); + idVec3 GetGravityVector(); + +private: + idEntityPtr effect; + +}; + +#endif // __GAME_GRAVITYSWITCH_H__ diff --git a/src/Prey/game_guihand.cpp b/src/Prey/game_guihand.cpp new file mode 100644 index 0000000..7193ea6 --- /dev/null +++ b/src/Prey/game_guihand.cpp @@ -0,0 +1,52 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +CLASS_DECLARATION( hhHand, hhGuiHand ) +END_CLASS + + +void hhGuiHand::Spawn(void) { + actionAnimDoneTime = 0; + fl.networkSync = true; +} + +void hhGuiHand::Save(idSaveGame *savefile) const { + savefile->WriteInt( actionAnimDoneTime ); +} + +void hhGuiHand::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( actionAnimDoneTime ); +} + +void hhGuiHand::WriteToSnapshot( idBitMsgDelta &msg ) const +{ + hhHand::WriteToSnapshot(msg); + + msg.WriteBits(actionAnimDoneTime, 32); +} + +void hhGuiHand::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhHand::ReadFromSnapshot(msg); + + actionAnimDoneTime = msg.ReadBits(32); +} + +void hhGuiHand::ClientPredictionThink( void ) { + hhHand::ClientPredictionThink(); +} + +void hhGuiHand::Action(void) { + PlayAnim( -1, action, &EV_Hand_Ready ); +} + +void hhGuiHand::SetAction(const char* str) { + action = str; +} + +bool hhGuiHand::IsValidFor( hhPlayer *who ) { + return( who->InGUIMode() ); +} diff --git a/src/Prey/game_guihand.h b/src/Prey/game_guihand.h new file mode 100644 index 0000000..02b5cfb --- /dev/null +++ b/src/Prey/game_guihand.h @@ -0,0 +1,33 @@ + +#ifndef __PREY_GAME_GUIHAND_H__ +#define __PREY_GAME_GUIHAND_H__ + +// Dumb forward decl +class hhPlayer; + +class hhGuiHand : public hhHand { + +public: + + CLASS_PROTOTYPE(hhGuiHand); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + virtual void Action(void); // Player clicked + virtual void SetAction(const char* str); //HUMANHEAD bjk + virtual bool IsValidFor( hhPlayer *who ); + +protected: + int actionAnimDoneTime; + const char* action; +}; + + +#endif /* __PREY_GAME_GUIHAND_H__ */ + diff --git a/src/Prey/game_gun.cpp b/src/Prey/game_gun.cpp new file mode 100644 index 0000000..cc2e43e --- /dev/null +++ b/src/Prey/game_gun.cpp @@ -0,0 +1,295 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//----------------------------------------------------------------------- +// +// hhGun +// +//----------------------------------------------------------------------- +const idEventDef EV_SetEnemy("setenemy", "e"); +const idEventDef EV_FireGunAt("fireat", "e"); + +CLASS_DECLARATION(idEntity, hhGun) + EVENT( EV_Activate, hhGun::Event_Activate ) + EVENT( EV_SetEnemy, hhGun::Event_SetEnemy ) + EVENT( EV_FireGunAt, hhGun::Event_FireAt ) +END_CLASS + + +float AngleBetweenVectors(const idVec3 &v1, const idVec3 &v2) { + float dot = (v1 * v2) / ( v1.Length() * v2.Length() ); + return RAD2DEG(idMath::ACos(dot)); +} + +void hhGun::Spawn(void) { + coneAngle = spawnArgs.GetFloat("coneAngle"); + burstCount = spawnArgs.GetInt("bursts"); + burstRate = ( int )( spawnArgs.GetFloat( "burstRate" ) * 1000.0f ); + fireRate = ( int )( spawnArgs.GetFloat( "fireRate" ) * 1000.0f ); + fireDeviation = ( int )( spawnArgs.GetFloat( "fireDeviation" ) * 1000.0f ); + targetOffset = spawnArgs.GetVector("targetOffset"); + targetRadius = spawnArgs.GetFloat("targetRadius"); + nextFireTime = 0; + nextBurstTime = 0; + nextEnemyTime = 0; + firing = false; + enemyRate = 200; + + GetPhysics()->SetContents(CONTENTS_SOLID); + fl.takedamage = true; + + SetEnemy(NULL); + if (spawnArgs.GetBool("enabled")) { + BecomeActive(TH_THINK); + } +} + +void hhGun::Save(idSaveGame *savefile) const { + enemy.Save(savefile); + savefile->WriteInt( enemyRate ); + savefile->WriteInt( nextEnemyTime ); + savefile->WriteVec3( targetOffset ); + savefile->WriteFloat( targetRadius ); + savefile->WriteInt( fireRate ); + savefile->WriteInt( fireDeviation ); + savefile->WriteInt( nextFireTime ); + savefile->WriteInt( burstRate ); + savefile->WriteInt( burstCount ); + savefile->WriteInt( nextBurstTime ); + savefile->WriteInt( curBurst ); + savefile->WriteBool( firing ); + savefile->WriteFloat( coneAngle ); +} + +void hhGun::Restore( idRestoreGame *savefile ) { + enemy.Restore(savefile); + savefile->ReadInt( enemyRate ); + savefile->ReadInt( nextEnemyTime ); + savefile->ReadVec3( targetOffset ); + savefile->ReadFloat( targetRadius ); + savefile->ReadInt( fireRate ); + savefile->ReadInt( fireDeviation ); + savefile->ReadInt( nextFireTime ); + savefile->ReadInt( burstRate ); + savefile->ReadInt( burstCount ); + savefile->ReadInt( nextBurstTime ); + savefile->ReadInt( curBurst ); + savefile->ReadBool( firing ); + savefile->ReadFloat( coneAngle ); +} + +void hhGun::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) { + SetEnemy(NULL); + fl.takedamage = false; + BecomeInactive(TH_THINK); + + const char *killedModel = spawnArgs.GetString("model_killed", NULL); + if (killedModel) { + SetModel(killedModel); + UpdateVisuals(); + } + else { + GetPhysics()->SetContents(0); + } + + // Spawn gibs + if (spawnArgs.FindKey("def_debrisspawner")) { + hhUtils::SpawnDebrisMass(spawnArgs.GetString("def_debrisspawner"), this ); + } + + StartSound( "snd_explode", SND_CHANNEL_ANY ); + + ActivateTargets( attacker ); +} + +void hhGun::SetEnemy(idEntity *ent) { + if (ent && ent->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast(ent); + enemy = player->InVehicle() ? player->GetVehicleInterface()->GetVehicle() : ent; + } + else { + enemy = ent; + } +} + +void hhGun::FindEnemy() { + if (enemy.IsValid() && enemy->GetHealth() > 0) { + return; + } + if ( !gameLocal.InPlayerPVS( this ) ) { + return; + } + + idEntity *bestEnt = NULL; + float bestDistSqr = idMath::INFINITY; + float radiusSqr = targetRadius*targetRadius; + idVec3 origin = GetPhysics()->GetOrigin(); + idMat3 axis = GetPhysics()->GetAxis(); + for ( int i = 0; i < MAX_CLIENTS ; i++ ) { + idEntity *ent = gameLocal.entities[ i ]; + + if (ent) { + idVec3 toEnt = ent->GetPhysics()->GetOrigin() + targetOffset - origin; + float distSqr = toEnt.LengthSqr(); + if (distSqr < bestDistSqr && distSqr < radiusSqr && + AngleBetweenVectors(toEnt, axis[0]) < coneAngle) { + bestDistSqr = distSqr; + bestEnt = ent; + } + } + } + + SetEnemy(bestEnt); +} + +idMat3 hhGun::GetAimAxis() { +#if 0 + // Fast approximation + const idDict *projectileDef = declManager->FindEntityDef( spawnArgs.GetString("def_projectile") ); + float projSpeed = idProjectile::GetVelocity( projectileDef ).Length(); + idVec3 firePos = GetPhysics()->GetOrigin(); + idVec3 enemyPos = enemy->GetPhysics()->GetOrigin() + targetOffset; + idVec3 enemyVel = enemy->GetPhysics()->GetLinearVelocity(); + idVec3 toEnemy = enemyPos - firePos; + float enemyDist = toEnemy.Length(); + float projTime = enemyDist / projSpeed; + idVec3 predictedEnemyPos = enemyPos + enemyVel * projTime; + idVec3 aim = predictedEnemyPos - firePos; + aim.Normalize(); + return aim.hhToMat3(); +#else +/* Projectile prediction: + +Let: + Pe(t) = position of enemy at time t Ve = velocity of enemy (known) + Pm(t) = position of missile at time t Vm = velocity of missile (magnitude known) + Pf = position firing from + +(1) Pe(t) = Pe(0) + Ve * t Enemy position function + +(2) Aim(t) = Pe(t) - Pf Aim function is vector from firing point to Pe(t) + + |Aim(t)| distance / distance/sec -> sec +(3) -------- = t + |Vm| + +(4) d = Pe(0) - Pf delta vector from firing point to initial enemy position + +(5) s = |Vm| + + Substituting and simplifying yields a quadratic function in terms of t, Ve, d, & s: + At² + Bt + C = 0 +*/ + const idDict *projectileDef = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_projectile") ); + if ( !projectileDef ) { + gameLocal.Error( "Unknown def_projectile: %s\n", spawnArgs.GetString("def_projectile") ); + } + float projSpeed = idProjectile::GetVelocity( projectileDef ).Length(); + idVec3 firePos = GetPhysics()->GetOrigin(); + idVec3 enemyPos = enemy->GetOrigin() + idVec3(0,0,32); + idVec3 enemyVel = enemy->GetPhysics()->GetLinearVelocity(); + idVec3 d = enemyPos - firePos; + idVec3 v = enemyVel; + float s = projSpeed; + + float a = v.x*v.x + v.y*v.y + v.z*v.x - s*s; // t2 term + float b = 2 * (d.x*v.x + d.y*v.y + d.z*v.z); // t term + float c = d.x*d.x + d.y*d.y + d.z*d.z; + + // Use quadratic formula to solve for t + float t1 = (-b + sqrt(b*b - 4*a*c) ) / (2*a); + float t2 = (-b - sqrt(b*b - 4*a*c) ) / (2*a); + float projTime = t1 > 0 ? t1 : t2; + + idVec3 predictedEnemyPos = enemyPos + enemyVel * projTime; + idVec3 aim = predictedEnemyPos - firePos; + aim.Normalize(); + return aim.hhToMat3(); +#endif +} + +bool hhGun::ValidEnemy() { + return (enemy.IsValid()) ? enemy->GetHealth() > 0 : false; +} + +void hhGun::Fire(idMat3 &axis) { + if (health > 0) { + hhUtils::LaunchProjectile(this, spawnArgs.GetString("def_projectile"), axis, GetOrigin()); + StartSound( "snd_fire", SND_CHANNEL_ANY ); + } +} + +void hhGun::Think(void) { + if (thinkFlags & TH_THINK) { + + // Temp: for placement + if (spawnArgs.GetBool("showCone")) { + float radius = targetRadius * tan(DEG2RAD(coneAngle)); + gameRenderWorld->DebugCone(colorGreen, + GetPhysics()->GetOrigin(), + GetPhysics()->GetAxis()[0] * targetRadius, + 0, radius); + } + + if (!ValidEnemy() && gameLocal.time >= nextEnemyTime ) { + FindEnemy(); + nextEnemyTime = gameLocal.time + enemyRate; + } + + if (ValidEnemy()) { + if (!firing && gameLocal.time >= nextFireTime ) { + firing = true; + curBurst = burstCount; + nextFireTime = gameLocal.time + fireRate + gameLocal.random.CRandomFloat()*fireDeviation; + nextBurstTime = gameLocal.time; + } + + if (firing && gameLocal.time >= nextBurstTime ) { + // Aim + idMat3 aimAxis = GetAimAxis(); + + // Burst if enemy is in cone + if ( AngleBetweenVectors(aimAxis[0], GetPhysics()->GetAxis()[0]) < coneAngle ) { + Fire(aimAxis); + if (--curBurst <= 0) { + firing = false; + } + } + else { + firing = false; + SetEnemy(NULL); + } + + nextBurstTime = gameLocal.time + burstRate; + } + } + } + idEntity::Think(); +} + +void hhGun::Event_Activate(idEntity *activator) { + if (targets.Num() && targets[0].IsValid()) { + Event_FireAt(targets[0].GetEntity()); + return; + } + + if (thinkFlags & TH_THINK) { + BecomeInactive(TH_THINK); + } + else { + BecomeActive(TH_THINK); + } +} + +void hhGun::Event_SetEnemy(idEntity *newEnemy) { + SetEnemy(newEnemy); +} + +void hhGun::Event_FireAt(idEntity *victim) { + idVec3 dir = victim->GetOrigin() - GetOrigin(); + dir.Normalize(); + Fire(dir.ToMat3()); +} diff --git a/src/Prey/game_gun.h b/src/Prey/game_gun.h new file mode 100644 index 0000000..270cc67 --- /dev/null +++ b/src/Prey/game_gun.h @@ -0,0 +1,46 @@ + +#ifndef __GAME_GUN_H__ +#define __GAME_GUN_H__ + +class hhGun : public idEntity { +public: + CLASS_PROTOTYPE( hhGun ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think( void ); + virtual void Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location); + void SetEnemy(idEntity *ent); + void FindEnemy(); + idMat3 GetAimAxis(); + bool ValidEnemy(); + +protected: + void Fire(idMat3 &axis); + + void Event_Activate(idEntity *activator); + void Event_SetEnemy(idEntity *newEnemy); + void Event_FireAt(idEntity *victim); + +protected: + idEntityPtr enemy; + int enemyRate; + int nextEnemyTime; + idVec3 targetOffset; + float targetRadius; + + int fireRate; + int fireDeviation; + int nextFireTime; + + int burstRate; + int burstCount; + int nextBurstTime; + int curBurst; + bool firing; + float coneAngle; +}; + +#endif // __GAME_GUN_H__ diff --git a/src/Prey/game_hand.cpp b/src/Prey/game_hand.cpp new file mode 100644 index 0000000..502fd26 --- /dev/null +++ b/src/Prey/game_hand.cpp @@ -0,0 +1,1155 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +const idEventDef EV_Hand_DoRemove( "DoHandRemove", NULL ); +const idEventDef EV_Hand_DoAttach( "DoHandAttach", "e" ); +const idEventDef EV_Hand_Remove( "RemoveHand", NULL ); + +const idEventDef EV_Hand_Ready( "" ); +const idEventDef EV_Hand_Lowered( "" ); +const idEventDef EV_Hand_Raise( "" ); + +CLASS_DECLARATION( hhAnimatedEntity, hhHand ) + EVENT ( EV_Hand_DoRemove, hhHand::Event_DoHandRemove ) + EVENT ( EV_Hand_DoAttach, hhHand::Event_DoHandAttach ) + EVENT ( EV_Hand_Remove, hhHand::Event_RemoveHand ) + EVENT ( EV_Hand_Ready, hhHand::Event_Ready ) + EVENT ( EV_Hand_Lowered, hhHand::Event_Lowered ) + EVENT ( EV_Hand_Raise, hhHand::Event_Raise ) +END_CLASS + +/* +============ +hhHand::~hhHand +============ +*/ +hhHand::~hhHand() { + bool shouldWarn = (gameLocal.GameState() != GAMESTATE_SHUTDOWN && !gameLocal.isClient); + //rww - don't warn about this on client, since snapshot entities are free to be removed + //at any time. + + // Check if the hand deleted is still in the hands list + if ( gameLocal.hands.Find( this ) ) { + if ( shouldWarn ) { + gameLocal.Warning( "The hand (%p/%s) was deleted before being removed", + this, spawnArgs.GetString( "classname" ) ); + } + gameLocal.hands.Remove( this ); + } + + if ( owner != NULL ) { + if ( shouldWarn ) { + gameLocal.Warning( "A hand (%p) was deleted while having an owner", this ); + } + } + + StopSound( SND_CHANNEL_ANY ); +} + + +/* +============ +hhHand::Spawn +============ +*/ +void hhHand::Spawn( void ) { + gameLocal.hands.Append( idEntityPtr (this) ); + + owner = NULL; + previousHand = NULL; + + renderEntity.weaponDepthHack = true; + + animEvent = NULL; + + priority = spawnArgs.GetInt("priority"); + + status = HS_UNKNOWN; + idealState = HS_READY; + + animBlendFrames = 4; // needs blending + animDoneTime = 0; + + attached = false; + attachTime = -1; + + spawnArgs.GetBool( "replace_previous", "0", replacePrevious ); + + spawnArgs.GetBool( "lower_weapon", "1", lowerWeapon ); + spawnArgs.GetBool( "aside_weapon", "1", asideWeapon ); + + handedness = spawnArgs.GetInt( "handedness", "2" ); + + physicsObj.SetSelf( this ); + physicsObj.SetClipModel( NULL, 1.0f ); + physicsObj.SetOrigin( GetOrigin() ); + physicsObj.SetAxis( GetAxis() ); + SetPhysics( &physicsObj ); + + // Lower overrides aside + if ( lowerWeapon && asideWeapon ) { + asideWeapon = false; + } + + Hide(); +} + +void hhHand::Save(idSaveGame *savefile) const { + //savefile->WriteObject( owner ); + owner.Save(savefile); + savefile->WriteInt( priority ); + savefile->WriteObject( previousHand ); + savefile->WriteBool( lowerWeapon ); + savefile->WriteBool( asideWeapon ); + savefile->WriteBool( attached ); + savefile->WriteBool( replacePrevious ); + savefile->WriteInt( attachTime ); + savefile->WriteInt( handedness ); + savefile->WriteInt( animDoneTime ); + savefile->WriteInt( animBlendFrames ); + savefile->WriteInt( status ); + savefile->WriteInt( idealState ); + savefile->WriteEventDef(animEvent); + savefile->WriteStaticObject( physicsObj ); +} + +void hhHand::Restore( idRestoreGame *savefile ) { + //savefile->ReadObject( reinterpret_cast(owner) ); + owner.Restore(savefile); + savefile->ReadInt( priority ); + savefile->ReadObject( reinterpret_cast(previousHand) ); + savefile->ReadBool( lowerWeapon ); + savefile->ReadBool( asideWeapon ); + savefile->ReadBool( attached ); + savefile->ReadBool( replacePrevious ); + savefile->ReadInt( attachTime ); + savefile->ReadInt( handedness ); + savefile->ReadInt( animDoneTime ); + savefile->ReadInt( animBlendFrames ); + savefile->ReadInt( status ); + savefile->ReadInt( idealState ); + savefile->ReadEventDef(animEvent); + savefile->ReadStaticObject( physicsObj ); + RestorePhysics( &physicsObj ); +} + +//rww - network code +void hhHand::WriteToSnapshot( idBitMsgDelta &msg ) const { + //physicsObj.WriteToSnapshot(msg); + //rww - trying to keep this all local + + WriteBindToSnapshot(msg); + + msg.WriteBits(owner.GetSpawnId(), 32); + + if (previousHand) { + msg.WriteBits(gameLocal.GetSpawnId(previousHand), 32); + } + else { + msg.WriteBits(0, 32); + } + + msg.WriteBits(attached, 1); + msg.WriteBits(replacePrevious, 1); + //msg.WriteBits(attachTime, 32); + //msg.WriteBits(animDoneTime, 32); + //msg.WriteBits(animBlendFrames, 32); + + assert(idealState < (1<<4)); + msg.WriteBits(idealState, 4); + + assert(status < (1<<4)); + msg.WriteBits(status, 4); + + msg.WriteBits(IsHidden(), 1); + + assert(gameLocal.hands.Num() < (1<<4)); + msg.WriteBits(gameLocal.hands.Num(), 4); + int i = 0; + while (i < gameLocal.hands.Num()) { + hhHand *hand = gameLocal.hands[i].GetEntity(); + + if (hand) { + msg.WriteBits(gameLocal.GetSpawnId(hand), 32); + } + else { + msg.WriteBits(0, 32); + } + + i++; + } + + msg.WriteBits(renderEntity.allowSurfaceInViewID, GENTITYNUM_BITS); + + idAnimatedEntity::WriteToSnapshot(msg); +} + +void hhHand::ReadFromSnapshot( const idBitMsgDelta &msg ) { + //physicsObj.ReadFromSnapshot(msg); + //rww - trying to keep this all local + + ReadBindFromSnapshot(msg); + + if (owner.SetSpawnId(msg.ReadBits(32))) { + physicsObj.SetSelfOwner(owner.GetEntity()); + if (owner.IsValid() && owner.GetEntity()) { + DoHandAttach(owner.GetEntity()); + } else { + owner = NULL; + } + } + + //Show(); + + idEntityPtr newPrevHand; + int prevHandSpawnId = msg.ReadBits(32); + if (newPrevHand.SetSpawnId(prevHandSpawnId)) { + previousHand = newPrevHand.GetEntity(); + } + else { + previousHand = NULL; + } + + attached = !!msg.ReadBits(1); + replacePrevious = !!msg.ReadBits(1); + //attachTime = msg.ReadBits(32); + //animDoneTime = msg.ReadBits(32); + //animBlendFrames = msg.ReadBits(32); + + idealState = msg.ReadBits(4); + + int nextStatus = msg.ReadBits(4); + if (status != nextStatus) { + if (nextStatus == HS_LOWERING || nextStatus == HS_LOWERED) { + PutAway(); + } + else if (nextStatus == HS_RAISING) { + Raise(); + } + else if (nextStatus == HS_READY) { + //PlayAnim( -1, "idle" ); + Ready(); + } + status = nextStatus; + } + + bool hidden = !!msg.ReadBits(1); + if (hidden != IsHidden()) { + if (hidden) { + Hide(); + } else { + Show(); + } + } + + int numHands = msg.ReadBits(4); + gameLocal.hands.SetNum(numHands); + int i = 0; + while (i < numHands) { + int handSpawnId = msg.ReadBits(32); + gameLocal.hands[i].SetSpawnId( handSpawnId ); + + i++; + } + + renderEntity.allowSurfaceInViewID = msg.ReadBits(GENTITYNUM_BITS); + + idAnimatedEntity::ReadFromSnapshot(msg); + + /* + if (msg.HasChanged()) { + Present(); + } + */ +} + +void hhHand::ClientPredictionThink( void ) { + BecomeActive(TH_THINK|TH_ANIMATE); + UpdateVisuals(); + idAnimatedEntity::ClientPredictionThink(); +} + +bool hhHand::ClientReceiveEvent( int event, int time, const idBitMsg &msg ) { + switch (event) { + case EVENT_REMOVEHAND: { + RemoveHand(); + return true; + } + default: { + return hhAnimatedEntity::ClientReceiveEvent(event, time, msg); + } + } +} + +/* +============ +hhHand::Event_Ready +============ +*/ +void hhHand::Event_Ready() { + Show(); + Ready(); +} + + +/* +=========== +hhHand::Event_Lowered +=========== +*/ +void hhHand::Event_Lowered() { + status = HS_LOWERED; + Hide(); +} + + +/* +============ +hhHand::Event_Raise +============ +*/ +void hhHand::Event_Raise() { + Raise(); +} + + +/* +============ +hhHand::Ready +============ +*/ +void hhHand::Ready() { + + switch ( idealState ) { + + case HS_READY: + CycleAnim(-1, "idle", 500); + status = HS_READY; + break; + + case HS_LOWERED: + PutAway(); + break; + + default: + gameLocal.Warning( "hhHand::Ready: Unsupported ideal state: %d", idealState ); + break; + + } +} + + +/* +============ +hhHand::Raise +============ +*/ +void hhHand::Raise() { + + Show(); + + if ( IsRaising() ) { // If already raising, return + return; + } + + status = HS_RAISING; + PlayAnim( -1, "raise", &EV_Hand_Ready ); +} + + +/* +============ +hhHand::PutAway +============ +*/ +void hhHand::PutAway() { + + if ( IsLowering() ) { + // already being put away, so don't play more than one put away animations + return; + } + + status = HS_LOWERING; + PlayAnim( -1, "putaway", &EV_Hand_Lowered ); +} + + +/* +=============== +hhHand::CycleAnim +=============== +*/ +void hhHand::CycleAnim( int channel, const char *animname, int blendTime ) { + if ( channel < 0 ) { + channel = GetChannelForAnim( animname ); + } + + int anim = GetAnimator()->GetAnim( animname ); + if ( anim ) { + GetAnimator()->CycleAnim( channel, anim, gameLocal.time, blendTime ); + } +} + +/* +=============== +hhHand::PlayAnim +=============== +*/ +void hhHand::PlayAnim( int channel, const char *animname, const idEventDef *event ) { + int anim; + + // HUMANHEAD nla + if ( animEvent ) { + CancelEvents( animEvent ); + } + animEvent = event; + + if ( channel < 0 ) { + channel = GetChannelForAnim( animname ); + } + // HUMANHEAD END + + anim = GetAnimator()->GetAnim( animname ); + if ( anim ) { + GetAnimator()->PlayAnim( channel, anim, gameLocal.time, FRAME2MS( animBlendFrames ) ); + animDoneTime = GetAnimator()->CurrentAnim( channel )->GetEndTime(); + } else { + // This is a valid case, some hands (mounted gun) don't animate + GetAnimator()->Clear( channel, gameLocal.time, FRAME2MS( animBlendFrames ) ); + animDoneTime = 0; + } + animBlendFrames = 4; + + // HUMANHEAD nla + if ( animEvent ) { + PostEventMS( animEvent, animDoneTime - gameLocal.time ); + } + // HUMANHEAD END +} + + +/* +============ +hhHand::AttachHand +Returns false if the hand can not be attached +============ +*/ +bool hhHand::AttachHand( hhPlayer *player, bool attachNow ) { + assert(player); + + if ( !CheckHandAttach( player ) ) { + return( false ); + } + + int loweringFinished = LowerHandOrWeapon( player ); + SetOwner( player ); + Bind( player, true ); + + if ( loweringFinished >= 0 ) { + player->handNext = this; + // If the attachment should wait, and we don't want to force it to happen now + if ( loweringFinished - gameLocal.time > 0 && !attachNow ) { + PostEventMS( &EV_Hand_DoAttach, loweringFinished - gameLocal.time, player ); + } + else { + Event_DoHandAttach( player ); + } + attachTime = loweringFinished; + } + + return( true ); +} + +/* +============ +hhHand::LowerHandOrWeapon + Used when the hand adds itself. + returns the time when the lowering will be done + will be -1 if the hand has already been attached. +============ +*/ +int hhHand::LowerHandOrWeapon( hhPlayer *player ) { + int animDone = gameLocal.time; + int animDone2; + + // If there is a 'next' hand, we can just replace it, as it is already playing the down anim for the hand + if ( player->handNext.IsValid() ) { + animDone = player->handNext->GetAttachTime(); + + gameLocal.hands.Remove( player->handNext.GetEntity() ); + + // Clear the old hand of any pending events, and then remove it + player->handNext->SetOwner( NULL ); + player->handNext->CancelEvents( NULL ); + player->handNext->ProcessEvent( &EV_Remove ); + + player->handNext = NULL; + } + // Else, If there is just a hand, play the down anim on it or replace it + else if ( player->hand.IsValid() ) { + //! Work out this logic more fully + if ( replacePrevious ) { + ReplaceHand( player ); + animDone = -1; + } + // If it isn't on it's way down, move it down + if ( !player->hand->IsLowered() ) { + player->hand->PutAway(); + animDone = player->hand->GetAnimDoneTime(); + } + } + + animDone2 = HandleWeapon( player, this, animDone - gameLocal.time ); + if ( ( animDone >= 0 ) && ( animDone2 > animDone ) ) { + animDone = animDone2; + } + // If there is a weapon play the down anim on it + /* + if ( player->hhweapon ) { + if ( lowerWeapon && !player->hhweapon->IsLowered() ) { + player->hhweapon->PutAway(); + animDone2 = player->hhweapon->GetAnimDoneTime(); + if ( animDone2 > animDone ) { + animDone = animDone2; + } + } + else if ( asideWeapon && !player->hhweapon->IsAside() ) { + player->hhweapon->PutAside(); + // When aside, we want to have it play right away, so dont' change animDone + } + // gameLocal.Printf("Scheduled down for %.2f\n", animDone); + } + */ + + return( animDone ); +} + +/* +============== +RaiseHandOrWeapon + + Assumes the hand is all the way down +============== +*/ +void hhHand::RaiseHandOrWeapon( hhPlayer *player ) { + hhHand *theHand = NULL; + int raiseDelay = 0; + + // Get the next hand + theHand = this->GetPreviousHand( player ); + + raiseDelay = HandleWeapon( player, theHand ); + + if ( theHand && ( !theHand->IsRaising() || !theHand->IsReady() ) ) { + theHand->PostEventMS( &EV_Hand_Raise, raiseDelay - gameLocal.time ); + } +} + + +/* +================ +hhHand::Reraise + Raises the hand again. +================ +*/ +void hhHand::Reraise( ) { + hhPlayer *thePlayer = NULL; + int raiseDelay = 0; + + CancelEvents( &EV_Remove ); + CancelEvents( &EV_Hand_Raise ); + + if ( owner.IsValid() && owner.GetEntity() && owner->IsType( hhPlayer::Type ) ) { + thePlayer = ( hhPlayer * ) owner.GetEntity(); + } + else { + gameLocal.Printf( "hhHand::Reraise: Warning tried to reraise when not on an hhPlayer (%p)\n", owner ); + return; + } + + raiseDelay = HandleWeapon( thePlayer, this ); + + if ( !IsRaising() || !IsReady() ) { + PostEventMS( &EV_Hand_Raise, raiseDelay - gameLocal.time ); + } +} + + +/* +=========== +hhHand::GetPreviousHand +=========== +*/ +hhHand *hhHand::GetPreviousHand( hhPlayer *player ) { + hhHand *prevHand = NULL; + + // If there is no player, just give them our previous hand + if ( player == NULL ) { + return( previousHand ); + } + + // If we are the next hand, up the current hand + if ( player->handNext == this ) { + prevHand = player->hand.GetEntity(); + } + // Else If we are the current hand + else if ( player->hand == this ) { + // If we have a previous hand, raise it if it isn't already up or being raised + if ( previousHand ) { + prevHand = previousHand; + } + } + // Else we are neither, throw a warning. How are we here? + else { + gameLocal.Warning( "ERROR GetPreviousHand: ERROR: Tried to get a previous hand when we (%p) are not on the player (%p/%p)", + this, player->handNext, player->hand ); + } + + return( prevHand ); +} + + +/* +============ +hhHand::HandleWeapon + Set the weapon in the proper place for the hand + Assumes the hand in question will be raised. + Returns time (MS) to wait until the hand can be raised. Only in the + case of the weapon being lowered does it really matter/ > 0 +============ +*/ +int hhHand::HandleWeapon( hhPlayer *player, hhHand *hand, int weaponDelay, bool doNow ) { + int raiseHandDelay = gameLocal.time; + bool weaponReady = ( hand == NULL ) && !player->ChangingWeapons(); + + // If we don't have a weapon, then just return + if ( !player->weapon.IsValid() || player->GetIdealWeapon() == 0 ) { + return( 0 ); + } + + // If we have a hand to compare to + if ( hand ) { + // If the hand wants the weapon lowered + if ( hand->lowerWeapon || ( hand->handedness & player->weapon->GetHandedness() ) ) { + player->weapon->PutAway(); + if ( doNow ) { + player->weapon->SetState( "Down", 0 ); + } + else { + // Foce the gun to go, so we can know when it'll be done playing the anim. + player->weapon->UpdateScript(); + raiseHandDelay = player->weapon->GetAnimDoneTime(); + } + /* + // If it isn't lowered, lower it + if ( !player->weapon->IsLowered() ) { + player->weapon->PutAway(); + raiseHandDelay = player->weapon->GetAnimDoneTime(); + } + */ + } //. lower the weapon + // If the hand wants the weapon to the side + else if ( hand->asideWeapon ) { + player->weapon->PutAside(); + if ( doNow ) { + player->weapon->SetState( "Aside", 0 ); + } + // We only want a delay if the weapon should pop up? + else if ( player->weapon->IsLowered() ) { + // Foce the gun to go, so we can know when it'll be done playing the anim. + player->weapon->UpdateScript(); + raiseHandDelay = player->weapon->GetAnimDoneTime(); + player->weapon->SetState( "PutAside", 4 ); + } + /* + // If it is lowered, + if ( player->weapon->IsLowered() ) { + // First raise it, then set it aside + int done = weaponDelay; + idAnim *anim = player->weapon->GetAnimator()->GetAnim( "raise" ); // UGLY Hack, as hardcoded to the name + + player->weapon->PostEventMS( &EV_Weapon_WeaponRising, weaponDelay ); + if ( anim ) { done = anim->Length(); } + player->weapon->PostEventMS( &EV_Weapon_Aside, done ); + } + // Else if it is ready/upright and not aside + else if ( !player->weapon->IsAside() ) { + // Just set it aside + player->weapon->PutAside(); + } + */ + } + // Weapon should be in ready mode + else { + weaponReady = true; + } + } // Valid hand + + // If we don't have a hand, so just make sure the weapon is raised + if ( weaponReady ) { + player->weapon->Raise(); + if ( doNow ) { + player->weapon->SetState( "Idle", 0 ); + } + + /* + + // Clear any Aside events that may be posted. Happens when it was lowered, and the previous hand wanted aside + player->weapon->CancelEvents( &EV_Weapon_Aside ); + + // If the weapon is aside, put it upright + if ( player->weapon->IsAside() ) { + player->weapon->PutUpright(); + } + // Else if it is lowering, raise it + else if ( !player->weapon->IsRising() && player->weapon->IsLowered() ) { + player->weapon->Raise(); + } + // Else if we aren't ready, how can we not be??!? + else if ( player->weapon->IsLowered() || player->weapon->IsLowered() ) { + gameLocal.Warning( "hhHand::HandleWeapon ERROR: Have a weapon that is lowered/lowering when shouldn't be!" ); + } + */ + } + + return( raiseHandDelay ); +} + + +/* +============ +hhHand::RemoveHand + Plays the animations and schedules pointer changes +============ +*/ +bool hhHand::RemoveHand( void ) { + hhPlayer *player; + int animDone = gameLocal.time; + + if ( owner.IsValid() && owner.GetEntity() ) { + if ( owner->IsType( hhPlayer::Type ) ) { + player = static_cast( owner.GetEntity() ); + } + else { + gameLocal.Warning( "ERROR: RemoveHand: Tried to remove from a non player" ); + return( false ); + } + } + else { + gameLocal.Warning( "ERROR: RemoveHand: Tried to remove with no owner!!" ); + return( false ); + } + + if ( !CheckHandRemove( player ) ) { + return( false ); + } + + // Unaside is asap if we asided it, and the prev hand isn't gonna want it asided + // NLA - If we want to upright the GUI + hhHand *prevHand = GetPreviousHand( player ); + if ( ( !prevHand || !prevHand->asideWeapon ) && + ( asideWeapon && player->weapon.IsValid() && player->weapon->IsAside() ) ){ + player->weapon->PutUpright(); + } + + CancelEvents( &EV_Hand_DoRemove ); + + // Play down anim + if( !IsLowering() ) { + PutAway(); + animDone = GetAnimDoneTime(); + PostEventMS( &EV_Hand_DoRemove, animDone - gameLocal.time ); + } + else if ( IsLowered() ) { // If already down, just remove it now + Event_DoHandRemove(); + } + + return( true ); +} + + +/* +============ +hhHand::SetOwner +============ +*/ +void hhHand::SetOwner( idActor *owner ) { + + this->owner = owner; + + if (owner) { + if( GetPhysics() && GetPhysics()->IsType(hhPhysics_StaticWeapon::Type) ) { + static_cast(GetPhysics())->SetSelfOwner( owner ); + } + + // only show the surface in player view + renderEntity.allowSurfaceInViewID = owner->entityNumber+1; + } +} + + +/* +============ +hhHand::ReplaceHand +============ +*/ +void hhHand::ReplaceHand( hhPlayer *player) { + //int animDone; + hhHand *hand = NULL; + + //! What to do if hand is null? + if( !player || !player->hand.IsValid() ) { + return; + } + hand = player->hand.GetEntity(); + + // Copy over any key info needed + this->previousHand = hand->previousHand; + + HandleWeapon( player, this ); + + player->hand = this; + hand->attached = false; + hand->owner = NULL; + hand->Hide(); + hand->PostEventMS( &EV_Remove, 0 ); + + gameLocal.hands.Remove( hand ); + + attached = true; + Show(); + Raise(); +} + + +/* +============ +hhHand::CheckHandAttach +============ +*/ +bool hhHand::CheckHandAttach( hhPlayer *owner ) { + bool debug = 0; + + if ( attached ) { + gameLocal.Warning( "Tried to attach an already attached hand." ); + return( false ); + } + + if( !owner ) { + if ( debug ) { gameLocal.Printf( "Out because of owner\n" ); } + return( false ); + } + + // More important hand there. Abort + if ( owner->hand.IsValid() && ( GetPriority() < owner->hand->GetPriority() ) ) { + if ( debug ) { gameLocal.Printf( "Out because of hand priority\n" ); } + return( false ); + } + + // More important hand about to be there. Abort + if ( owner->handNext.IsValid() && ( GetPriority() < owner->handNext->GetPriority() ) ) { + if ( debug ) { gameLocal.Printf( "Out because of next hand priority\n" ); } + return( false ); + } + + //? What do to if both equal priority? + if ( owner->hand.IsValid() && ( GetPriority() == owner->hand->GetPriority() ) ) { + // Same hand, abort! + if ( ( owner->hand->spawnArgs.GetString( "classname" ) == spawnArgs.GetString( "classname" ) ) && ( owner->hand != this ) ) { + if ( debug ) { gameLocal.Printf( "Out because of same hand\n" ); } + return( false ); + } + } + + //? What do to if both equal priority? + if ( owner->handNext.IsValid() && ( GetPriority() == owner->handNext->GetPriority() ) ) { + // Same hand, abort! + hhHand* currentHand = owner->handNext.GetEntity(); + if ( ( owner->handNext->spawnArgs.GetString( "classname" ) == spawnArgs.GetString( "classname" ) ) && ( owner->handNext != this ) ) { + if ( debug ) { gameLocal.Printf( "Out because of same next hand\n" ); } + return( false ); + } + } + + return( true ); +} + + +/* +============ +hhHand::CheckHandRemove +============ +*/ +bool hhHand::CheckHandRemove( hhPlayer *player ) { + + // Check have a valid player + if ( player == NULL ) { + gameLocal.Warning( "ERROR: DoHandRemove: We are not attached to a player!"); + return( false ); + } + + // We aren't here! + if ( IsAttached() && ( player->hand != this ) ) { + gameLocal.Warning( "ERROR: DoHandRemove: Tried to detach from an player we are not assigned to %d %p %p", + (int) IsAttached(), player->hand, this ); + return( false ); + } + + return( true ); +} + +/* +============ +hhHand::SetModel +============ +*/ +void hhHand::SetModel( const char *modelname ) { + hhAnimatedEntity::SetModel( modelname ); +} + +/* +============ +hhHand::Present +============ +*/ +void hhHand::Present() { + hhAnimatedEntity::Present(); +} + +/* +============ +hhHand::Event_DoHandAttach +============ +*/ +void hhHand::Event_DoHandAttach( idEntity *owner ) { + DoHandAttach( owner ); +} + +/* +============ +hhHand::DoHandAttach +============ +*/ +bool hhHand::DoHandAttach( idEntity *owner ) { + hhPlayer *player; + + if ( owner && owner->IsType( hhPlayer::Type ) ) { + player = static_cast( owner ); + } + else { + gameLocal.Warning("ERROR: Tried to attach to a non-player"); + return( false ); + } + + if ( !CheckHandAttach( player ) ) { + PostEventMS( &EV_Remove, 0 ); + return( false ); + } + + // Sanity check, we should be the top dogh + if ( player->handNext != this && !gameLocal.isClient ) { //rww - don't care on client, because we cannot rely on handNext + gameLocal.Warning( "ERROR: We (%p) should be the next hand (%p) but aren't", + this, player->handNext ); + } + + // Make sure nothing has changed + HandleWeapon( player, this ); + + // We are top dog, replace! :) + if ( !replacePrevious ) { + previousHand = player->hand.GetEntity(); + } + else { + previousHand = NULL; + } + player->hand = this; + player->handNext = NULL; + + attached = true; + attachTime = -1; + + Raise(); + Show(); + + return( true ); +} + +/* +============ +hhHand::Event_DoHandRemove +============ +*/ +void hhHand::Event_DoHandRemove( void ) { + DoHandRemove( ); +} + + +/* +============ +hhHand::ForceRemove + - Should only be called when you don't need other hands/weaspons adjusted +============ +*/ +void hhHand::ForceRemove( void ) { + + gameLocal.hands.Remove( this ); + + owner = NULL; + + Hide(); +} + +/* +============ +hhHand::DoHandRemove +============ +*/ +bool hhHand::DoHandRemove( void ) { + hhPlayer *player; + + gameLocal.hands.Remove( this ); + + if ( owner.IsValid() && owner.GetEntity() && owner->IsType( hhPlayer::Type ) ) { + player = static_cast( owner.GetEntity() ); + } + else { + gameLocal.Warning( "ERROR: DoHandRemove: Tried to remove from a non player" ); + return( false ); + } + + // Play up anim + RaiseHandOrWeapon( player ); + + if ( !CheckHandRemove( player ) ) { + return( false ); + } + + // Do the actual removal logic + Hide(); + PostEventMS( &EV_Remove, 0 ); + + if ( IsAttached() ) { + player->hand = previousHand; + previousHand = NULL; + + attached = false; + } + + // If we were the next hand, clear it + if ( player->handNext == this ) { + player->handNext = NULL; + } + + owner = NULL; + + return( true ); +} + +/* +============ +hhHand::Event_RemoveHand + Plays the animations and schedules pointer changes +============ +*/ +void hhHand::Event_RemoveHand( void ) { + RemoveHand(); +} + +/* +=========== +hhHand::AddHand +=========== +*/ +hhHand *hhHand::AddHand( hhPlayer *player, const char *handclass, bool attachNow ) { + idDict args; + hhHand *hand = NULL; + + args.SetVector( "origin", player->GetEyePosition() ); + args.SetMatrix( "rotation", player->viewAngles.ToMat3() ); + + hand = static_cast< hhHand * >( gameLocal.SpawnObject( handclass, &args) ); + if ( hand ) { + hand->SetOwner( player ); + // We have a prob if you can't attach. Should return NULL + if ( !hand->AttachHand( player, attachNow ) ) { + hand->SetOwner( NULL ); + + gameLocal.hands.Remove( hand ); + + hand->PostEventMS( &EV_Remove, 0 ); + + return( NULL ); + } + } + + return( hand ); +} + + +/* +============ +hhHand::PrintHandInfo +============ +*/ +void hhHand::PrintHandInfo( idPlayer *player ) { + hhPlayer *hhplayer = NULL; + hhHand *hand = NULL; + int count = 0; + idList< idEntityPtr< hhHand > > orphaned; + + if ( player->IsType( hhPlayer::Type ) ) { + hhplayer = (hhPlayer *) player; + } + else { + return; + } + + orphaned = gameLocal.hands; + + gameLocal.Printf( "Weapon: %p Class: %s State: %d\n", player->weapon, + (const char *) player->weapon->GetDict()->GetString("classname"), + (int) player->weapon->GetStatus() ); + + hand = hhplayer->hand.GetEntity(); + while ( hand != NULL ) { + count++; + gameLocal.Printf( "Hand %d: %p Class: %s State: %d\n", count, hand, + hand->spawnArgs.GetString("classname"), (int) hand->GetStatus() ); + orphaned.Remove( hand ); + hand = hand->previousHand; + } + + count = 0; + for ( int i = 0; i < orphaned.Num(); ++i ){ + count++; + hand = orphaned[ i ].GetEntity(); + gameLocal.Printf( "Orphaned Hand %d: %p Class: %s State: %d\n", count, hand, + hand->spawnArgs.GetString("classname"), (int) hand->GetStatus() ); + } +} + + +/* +================ +hhHand::GetMasterDefaultPosition +================ +*/ +void hhHand::GetMasterDefaultPosition( idVec3 &masterOrigin, idMat3 &masterAxis ) const { + idActor* actor = NULL; + idEntity* master = GetBindMaster(); + + if( master ) { + if( master->IsType(idActor::Type) ) { + actor = static_cast( master ); + actor->DetermineOwnerPosition( masterOrigin, masterAxis ); + + masterOrigin = actor->ApplyLandDeflect( masterOrigin, 1.1f ); + } else { + hhAnimatedEntity::GetMasterDefaultPosition( masterOrigin, masterAxis ); + } + } +} diff --git a/src/Prey/game_hand.h b/src/Prey/game_hand.h new file mode 100644 index 0000000..e013912 --- /dev/null +++ b/src/Prey/game_hand.h @@ -0,0 +1,171 @@ + +#ifndef __PREY_GAME_HAND_H__ +#define __PREY_GAME_HAND_H__ + +// Forward declar +class hhPlayer; + +extern const idEventDef EV_Hand_DoRemove; +extern const idEventDef EV_Hand_DoAttach; +extern const idEventDef EV_Hand_Remove; +extern const idEventDef EV_Hand_Ready; +extern const idEventDef EV_Hand_Lowered; +extern const idEventDef EV_Hand_Raise; + +typedef enum { + HS_UNKNOWN, + HS_READY, + HS_LOWERING, + HS_LOWERED, + HS_RAISING +} handStatus_t; + + +class hhHand : public hhAnimatedEntity { + +public: + + CLASS_PROTOTYPE(hhHand); + + virtual ~hhHand(); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + enum { + EVENT_REMOVEHAND = hhAnimatedEntity::EVENT_MAXEVENTS, + EVENT_MAXEVENTS + }; + virtual bool ClientReceiveEvent( int event, int time, const idBitMsg &msg ); + + static hhHand * AddHand( hhPlayer *player, const char *classname, bool attachNow = false ); + + static void PrintHandInfo( idPlayer *player ); + + virtual void SetModel( const char *modelname ); + + virtual void Present(); + + virtual void Action( void ) { }; + virtual void SetAction( const char* str ) {}; //HUMANHEAD bjk + + int GetPriority( void ) { return( priority ); }; + + int GetStatus( ) { return( status ); }; + + void Reraise( ); + + bool IsReady() { return( status == HS_READY ); } + bool IsLowering() { return( status == HS_LOWERING || status == HS_LOWERED ); } + bool IsLowered() { return( status == HS_LOWERED ); } + bool IsRaising() { return( status == HS_RAISING ); } + + virtual void Raise(); + virtual void PutAway(); + virtual void Ready(); + + void Event_Ready(); + void Event_Lowered(); + void Event_Raise(); + + + void PlayAnim( int channel, const char *animName, const idEventDef *animEvent = NULL ); + void CycleAnim( int channel, const char *animname, int blendTime ); + + bool AttachHand( hhPlayer *player, bool attachNow = false ); + bool RemoveHand( void ); + + bool IsAttached() { return attached; } + + int LowerHandOrWeapon( hhPlayer *player ); + void RaiseHandOrWeapon( hhPlayer *player ); + int HandleWeapon( hhPlayer *player, hhHand *topHand, int weaponDelay = 0, bool doNow = false ); + + // Replace the player hand with this hand. Does a quick swap + void ReplaceHand( hhPlayer *player ); + + int GetAttachTime( ) { return( attachTime ); }; + + // Functions that could be put in common base class + int GetAnimDoneTime() { return( animDoneTime ); }; + + void SetOwner( idActor *owner ); + void GetMasterDefaultPosition( idVec3 &masterOrigin, idMat3 &masterAxis ) const; + + virtual bool IsValidFor( hhPlayer *who ) { return( true ); } + + // These used to be protected. Call only if you know what you are doing! + hhHand * GetPreviousHand( hhPlayer *player = NULL ); + + void Event_DoHandAttach( idEntity *player ); + + void ForceRemove( void ); + +protected: + void Event_RemoveHand( void ); + + // Event only methods. Should NOT be called directly + void Event_DoHandRemove( void ); + + bool DoHandAttach( idEntity *player ); + bool DoHandRemove( void ); + + +protected: + bool CheckHandAttach( hhPlayer *player ); + bool CheckHandRemove( hhPlayer *player ); + +protected: + // Who we are associated with + idEntityPtr owner; + + // What is the priority of this hand? + int priority; + + // Previous hand. Will restore on closing down! + hhHand *previousHand; + + // Should we lower the weapon for this hand? + bool lowerWeapon; + + // If we don't lower the weapon, do we put it aside? + bool asideWeapon; + + // DEBUG - We should never delete w/out being attached + bool attached; + + // Should we replace the previous hand instead of putting it on the hand stack? + bool replacePrevious; + + // What time do we attach? + int attachTime; + + // What hands do we represent? (1 = right, 2 = left, 3 = both) + int handedness; + + // When will we be done animating? + int animDoneTime; + + // How many frames should we blend? + int animBlendFrames; + + // Status of the hand; + int status; + + // What state we would like to be + int idealState; + + // Last event posted with an anim + const idEventDef * animEvent; + + hhPhysics_StaticWeapon physicsObj; +}; + +#endif /* __PREY_GAME_HAND_H__ */ + diff --git a/src/Prey/game_handcontrol.cpp b/src/Prey/game_handcontrol.cpp new file mode 100644 index 0000000..05ed0cb --- /dev/null +++ b/src/Prey/game_handcontrol.cpp @@ -0,0 +1,110 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +CLASS_DECLARATION( hhHand, hhControlHand ) +END_CLASS + + +void hhControlHand::Spawn() { + bProcessControls = false; + oldStatus = 0; + + anims[ 0 ][ 0 ] = GetAnimator()->GetAnim("bottom_backward"); + anims[ 0 ][ 1 ] = GetAnimator()->GetAnim("bottom_center"); + anims[ 0 ][ 2 ] = GetAnimator()->GetAnim("bottom_forward"); + anims[ 1 ][ 0 ] = GetAnimator()->GetAnim("center_backward"); + anims[ 1 ][ 1 ] = GetAnimator()->GetAnim("center_center"); + anims[ 1 ][ 2 ] = GetAnimator()->GetAnim("center_forward"); + anims[ 2 ][ 0 ] = GetAnimator()->GetAnim("top_backward"); + anims[ 2 ][ 1 ] = GetAnimator()->GetAnim("top_center"); + anims[ 2 ][ 2 ] = GetAnimator()->GetAnim("top_forward"); + + fl.networkSync = true; +} + +void hhControlHand::Save(idSaveGame *savefile) const { + savefile->Write( anims, sizeof(int)*HAND_MATRIX_WIDTH*HAND_MATRIX_HEIGHT ); + savefile->WriteBool( bProcessControls ); + savefile->WriteInt( oldStatus ); +} + +void hhControlHand::Restore( idRestoreGame *savefile ) { + savefile->Read( anims, sizeof(int)*HAND_MATRIX_WIDTH*HAND_MATRIX_HEIGHT ); + savefile->ReadBool( bProcessControls ); + savefile->ReadInt( oldStatus ); +} + +void hhControlHand::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(bProcessControls, 1); + msg.WriteBits(oldStatus, 32); + + hhHand::WriteToSnapshot(msg); +} + +void hhControlHand::ReadFromSnapshot( const idBitMsgDelta &msg ) { + bProcessControls = !!msg.ReadBits(1); + oldStatus = msg.ReadBits(32); + + hhHand::ReadFromSnapshot(msg); +} + +void hhControlHand::ClientPredictionThink( void ) { + RunPhysics(); + + // HUMANHEAD pdm + if (thinkFlags & TH_TICKER) { + Ticker(); + } + + UpdateAnimation(); + UpdateVisuals(); + Present(); +} + + +void hhControlHand::Raise( void ) { + hhHand::Raise(); + + SetShaderParm(4, -MS2SEC(gameLocal.time)); // time + SetShaderParm(5, 1.0f); // Dir +} + +void hhControlHand::Ready() { + hhHand::Ready(); + bProcessControls = true; +} + +void hhControlHand::PutAway( void ) { + hhHand::PutAway(); + + SetShaderParm(4, -MS2SEC(gameLocal.time)); // time + SetShaderParm(5, -1.0f); // Dir +} + +void hhControlHand::UpdateControlDirection(idVec3 &dir) { + int anim; + + int curStatus = dir.DirectionMask(); + + if (bProcessControls && oldStatus != curStatus) { + // Determine which anim group to play. (Up/Down/Normal & Forward/Center/Back) + int z_index = dir.z < 0 ? 0 : dir.z > 0 ? 2 : 1; + int x_index = dir.x < 0 ? 0 : dir.x > 0 ? 2 : 1; + + anim = anims[ z_index ][ x_index ]; + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, 250); + + // Now determine which left and right to play + float left = dir.y > 0 ? 1.0f : 0.0f; + float right = dir.y < 0 ? 1.0f : 0.0f; + + GetAnimator()->CurrentAnim( ANIMCHANNEL_ALL )->SetSyncedAnimWeight( 0, left ); + GetAnimator()->CurrentAnim( ANIMCHANNEL_ALL )->SetSyncedAnimWeight( 1, 1.0f ); + GetAnimator()->CurrentAnim( ANIMCHANNEL_ALL )->SetSyncedAnimWeight( 2, right ); + + oldStatus = curStatus; + } +} diff --git a/src/Prey/game_handcontrol.h b/src/Prey/game_handcontrol.h new file mode 100644 index 0000000..1cb3243 --- /dev/null +++ b/src/Prey/game_handcontrol.h @@ -0,0 +1,34 @@ + +#ifndef __PREY_GAME_HAND_CONTROL_H__ +#define __PREY_GAME_HAND_CONTROL_H__ + +#define HAND_MATRIX_WIDTH 3 +#define HAND_MATRIX_HEIGHT 3 + +class hhControlHand : public hhHand { +public: + CLASS_PROTOTYPE(hhControlHand); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + void UpdateControlDirection(idVec3 &dir); + virtual void Ready(); + virtual void Raise(); + virtual void PutAway(); + +protected: + // Matrix of anims for up/down/normal & left/ceneter/right + int anims[ HAND_MATRIX_WIDTH ][ HAND_MATRIX_HEIGHT ]; + + bool bProcessControls; + int oldStatus; +}; + +#endif diff --git a/src/Prey/game_healthbasin.cpp b/src/Prey/game_healthbasin.cpp new file mode 100644 index 0000000..aa7b360 --- /dev/null +++ b/src/Prey/game_healthbasin.cpp @@ -0,0 +1,246 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_GiveHealth( "giveHealth" ); + +const idEventDef EV_IdleMode( "" ); +const idEventDef EV_FinishedPuke( "" ); +const idEventDef EV_Expended( "" ); + + +CLASS_DECLARATION( hhAnimatedEntity, hhHealthBasin ) + EVENT( EV_Activate, hhHealthBasin::Event_Activate ) + EVENT( EV_GiveHealth, hhHealthBasin::Event_GiveHealth ) + EVENT( EV_Broadcast_AppendFxToList, hhHealthBasin::Event_AppendFxToIdleList ) + + EVENT( EV_IdleMode, hhHealthBasin::Event_IdleMode ) + EVENT( EV_FinishedPuke, hhHealthBasin::Event_FinishedPuking ) + EVENT( EV_Expended, hhHealthBasin::Event_Expended ) +END_CLASS + + +void hhHealthBasin::Event_IdleMode() { + PlayCycle( "idle" ); + StartSound( "snd_idle", SND_CHANNEL_IDLE ); + BasinMode = BASIN_Idle; +} + +void hhHealthBasin::Event_FinishedPuking() { + if( currentHealth <= 0.f ) { + Event_Expended(); + } + else { + PostEventMS( &EV_IdleMode, PlayAnim("transidle", 4) ); + } +} + +void hhHealthBasin::Event_Expended() { + PlayAnim( "transdeath" ); + StartSound( "snd_die", SND_CHANNEL_ANY ); + hhUtils::RemoveContents( idleFxList, true ); //MDC: This doesn't seem to be working, possibly something with the broadcast fx + BasinMode = BASIN_Expended; +} +/* +================ +hhHealthBasin::hhHealthBasin +================ +*/ +hhHealthBasin::hhHealthBasin() { +} + +/* +================ +hhHealthBasin::Spawn +================ +*/ +void hhHealthBasin::Spawn() { + if ( g_wicked.GetBool() ) { // CJR: Don't spawn health basins in wicked mode + PostEventMS( &EV_Remove, 0 ); + } + + currentHealth = spawnArgs.GetFloat( "maxHealth" ); + activator = NULL; + fl.takedamage = true; + + verificationAbsBounds.Clear(); + GetPhysics()->SetContents( CONTENTS_BODY ); + + SpawnTrigger(); + SpawnFx(); + + PostEventMS( &EV_IdleMode, 0 ); +} + +void hhHealthBasin::Save( idSaveGame *savefile ) const { + savefile->WriteInt( BasinMode ); + activator.Save( savefile ); + savefile->WriteFloat( currentHealth ); + savefile->WriteBounds( verificationAbsBounds ); + + int num = idleFxList.Num(); + savefile->WriteInt( num ); + for( int i = 0; i < num; i++ ) { + idleFxList[i].Save( savefile ); + } +} + +void hhHealthBasin::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( reinterpret_cast ( BasinMode ) ); + activator.Restore( savefile ); + savefile->ReadFloat( currentHealth ); + savefile->ReadBounds( verificationAbsBounds ); + + int num; + savefile->ReadInt( num ); + idleFxList.Clear(); + idleFxList.SetNum( num ); + for( int i = 0; i < num; i++ ) { + idleFxList[i].Restore( savefile ); + } +} + +/* +================ +hhHealthBasin::~hhHealthBasin +================ +*/ +hhHealthBasin::~hhHealthBasin() { + hhUtils::RemoveContents( idleFxList, true ); +} + +/* +=============== +hhHealthBasin::SpawnTrigger +=============== +*/ +void hhHealthBasin::SpawnTrigger() { + idDict args; + + args.Set( "target", name.c_str() ); + args.Set( "mins", spawnArgs.GetString("triggerMins") ); + args.Set( "maxs", spawnArgs.GetString("triggerMaxs") ); + args.SetVector( "origin", GetOrigin() ); + args.SetMatrix( "rotation", GetAxis() ); + gameLocal.SpawnObject( spawnArgs.GetString("def_trigger"), &args ); + + verificationAbsBounds.FromTransformedBounds( idBounds(spawnArgs.GetVector("triggerMins"), spawnArgs.GetVector("triggerMaxs") ) + idBounds( vec3_zero, idVec3(75.0f, 0.0f, 0.0f)), GetOrigin(), GetAxis() ); +} + +/* +=============== +hhHealthBasin::SpawnFx +=============== +*/ +void hhHealthBasin::SpawnFx() { + BroadcastFxInfoAlongBonePrefixUnique( &spawnArgs, "fx_idle", "joint_idleFx", NULL, &EV_Broadcast_AppendFxToList ); +} + +/* +=============== +hhHealthBasin::PlayAnim +=============== +*/ +int hhHealthBasin::PlayAnim( const char* pName, int iBlendTime ) { + int pAnim = 0; + + ClearAnims( iBlendTime ); + + if( !pName && !pName[0] ) { + return 0; + } + + pAnim = GetAnimator()->GetAnim( pName ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, pAnim, gameLocal.time, FRAME2MS( iBlendTime ) ); + + return (pAnim != NULL) ? GetAnimator()->GetAnim( pAnim )->Length() : 0; +} + +/* +=============== +hhHealthBasin::PlayCycle +=============== +*/ +void hhHealthBasin::PlayCycle( const char* pName, int iBlendTime ) { + ClearAnims( iBlendTime ); + + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, GetAnimator()->GetAnim(pName), gameLocal.time, FRAME2MS( iBlendTime ) ); +} + +/* +=============== +hhHealthBasin::ClearAnims +=============== +*/ +void hhHealthBasin::ClearAnims( int iBlendTime ) { + GetAnimator()->ClearAllAnims( gameLocal.time, FRAME2MS( iBlendTime ) ); +} + +/* +=============== +hhHealthBasin::ActivatorVerified +=============== +*/ +bool hhHealthBasin::ActivatorVerified( const idEntityPtr& Activator ) { + if( !Activator.IsValid() || !Activator->GetPhysics()->GetAbsBounds().IntersectsBounds(verificationAbsBounds) ) { + return false; + } + //mdc - also check the health to be a valid activator + if( Activator->health < Activator->GetMaxHealth() ) { + return true; + } + return false; +} + +/* +=============== +hhHealthBasin::Event_AppendFxToIdleList +=============== +*/ +void hhHealthBasin::Event_AppendFxToIdleList( hhEntityFx* fx ) { + idleFxList.Append( fx ); +} + +/* +=============== +hhHealthBasin::Event_Activate +=============== +*/ +void hhHealthBasin::Event_Activate( idEntity *activatedby ) { + if( BasinMode == BASIN_Idle ) { //only allow activation when idle + activator = static_cast(activatedby); + if( ActivatorVerified(activator) ) { //mdc: only begin puking if valid activator (including less than nominal health) + BasinMode = BASIN_Puking; + StopSound( SND_CHANNEL_IDLE ); + StartSound( "snd_puke", SND_CHANNEL_ANY ); + PostEventMS( &EV_FinishedPuke, PlayAnim("puke") ); + } + } +} + +/* +=============== +hhHealthBasin::Event_GiveHealth + +Called from frame command +=============== +*/ +void hhHealthBasin::Event_GiveHealth() { + float amountApplied = 0.0f; + + assert( currentHealth > 0.0f ); + + //Make sure activator is still in the trigger volume, and that they still need health.. + if( !ActivatorVerified(activator) ) { + return; + } + + int oldHealth = activator->health; + if( activator->Give("health", va("%.2f", currentHealth)) ) { + amountApplied = activator->health - oldHealth; + currentHealth -= amountApplied; + + ActivateTargets( activator.GetEntity() ); + } +} \ No newline at end of file diff --git a/src/Prey/game_healthbasin.h b/src/Prey/game_healthbasin.h new file mode 100644 index 0000000..a11e124 --- /dev/null +++ b/src/Prey/game_healthbasin.h @@ -0,0 +1,54 @@ +#ifndef __HH_HEALTH_BASIN_H +#define __HH_HEALTH_BASIN_H + +/* +TODO: + + effects do not delete when using hhUtils::RemoveContents + + Need to have player face basin to allow use +*/ + +class hhHealthBasin : public hhAnimatedEntity { + CLASS_PROTOTYPE( hhHealthBasin ); + +public: + hhHealthBasin(); + virtual ~hhHealthBasin(); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void SpawnTrigger(); + void SpawnFx(); + + bool ActivatorVerified( const idEntityPtr& Activator ); + + int PlayAnim( const char* pName, int iBlendTime = 0 ); + void PlayCycle( const char* pName, int iBlendTime = 0 ); + void ClearAnims( int iBlendTime = 0 ); + +protected: + void Event_AppendFxToIdleList( hhEntityFx* fx ); + void Event_Activate( idEntity *pActivator ); + void Event_IdleMode(); + void Event_FinishedPuking(); + void Event_Expended(); + void Event_GiveHealth(); + +protected: + enum EBasinMode { + BASIN_Idle, + BASIN_Puking, + BASIN_Expended, + } BasinMode; + + idEntityPtr activator; + float currentHealth; + + idBounds verificationAbsBounds; + + idList< idEntityPtr > idleFxList; +}; + +#endif diff --git a/src/Prey/game_healthspore.cpp b/src/Prey/game_healthspore.cpp new file mode 100644 index 0000000..02a9886 --- /dev/null +++ b/src/Prey/game_healthspore.cpp @@ -0,0 +1,121 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define DEFAULT_SPORE_RESPAWN 15000 + +const idEventDef EV_RespawnSpore( "" ); + +CLASS_DECLARATION( idEntity, hhHealthSpore ) + EVENT( EV_Touch, hhHealthSpore::Event_Touch ) + EVENT( EV_RespawnSpore, hhHealthSpore::Event_RespawnSpore ) +END_CLASS + +/* +================ +hhHealthSpore::Spawn +================ +*/ +void hhHealthSpore::Spawn() { + if ( g_wicked.GetBool() ) { // CJR: Don't spawn health spores in wicked mode + PostEventMS( &EV_Remove, 0 ); + } + + GetPhysics()->SetContents( CONTENTS_TRIGGER ); + SetShaderParm( SHADERPARM_MODE, 1.0f ); // Enable the additive glow on the spore + + fl.networkSync = true; +} + +/* +================ +hhHealthSpore::Event_RespawnSpore +================ +*/ +void hhHealthSpore::Event_RespawnSpore() { + if (gameLocal.isClient) { + return; + } + + GetPhysics()->SetContents( CONTENTS_TRIGGER ); //restore contents + SetShaderParm( SHADERPARM_MODE, 1.0f ); //restore additive pass +} + +void hhHealthSpore::Save(idSaveGame *savefile) const { +} + +void hhHealthSpore::Restore( idRestoreGame *savefile ) { +} + +void hhHealthSpore::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_MODE]); +} + +void hhHealthSpore::ReadFromSnapshot( const idBitMsgDelta &msg ) { + renderEntity.shaderParms[SHADERPARM_MODE] = msg.ReadFloat(); + UpdateVisuals(); +} + +/* +================ +hhHealthSpore::ApplyEffect +================ +*/ +void hhHealthSpore::ApplyEffect( idActor* pActor ) { + if( pActor ) { + int oldHealth = pActor->health; + const char *itemHealthKey = "health"; + if (gameLocal.isMultiplayer) { //rww + itemHealthKey = "health_mp"; + } + pActor->Give( "health", spawnArgs.GetString(itemHealthKey) ); + } +} + +/* +================ +hhHealthSpore::Event_Touch +================ +*/ +void hhHealthSpore::Event_Touch( idEntity* pOther, trace_t* pTraceInfo ) { + hhFxInfo fxInfo; + idActor* pActor = NULL; + + if( pOther && pOther->IsType(idActor::Type) ) { + pActor = static_cast( pOther ); + + //rww - do not go above 100 in mp, even when maxhealth has been raised + if (gameLocal.isMultiplayer && pActor->IsType(hhPlayer::Type) && pActor->health >= MAX_HEALTH_NORMAL_MP) { + return; + } + + if( !pActor->IsDamaged() || pActor->health <= 0 ) { + return; + } + } + + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_detonate", GetOrigin(), GetAxis(), &fxInfo ); + + SetShaderParm( SHADERPARM_MODE, 0.0f ); // Disable the additive glow on the spore + + ApplyEffect( pActor ); + + ActivateTargets( pActor ); + + //rww - broadcast + StartSound( "snd_explode", SND_CHANNEL_ANY, 0, true ); + + GetPhysics()->SetContents( 0 ); //MDC - clear our contents, so we can not get re-touched. + fl.refreshReactions = false; // JRM - no since telling AI anymore + + if (gameLocal.isMultiplayer) { //rww - respawn functionality for mp + int respawnTime; + if (!spawnArgs.GetInt("respawn", "0", respawnTime)) { + respawnTime = DEFAULT_SPORE_RESPAWN; + } + PostEventMS(&EV_RespawnSpore, respawnTime); + } +} \ No newline at end of file diff --git a/src/Prey/game_healthspore.h b/src/Prey/game_healthspore.h new file mode 100644 index 0000000..c6f9067 --- /dev/null +++ b/src/Prey/game_healthspore.h @@ -0,0 +1,23 @@ +#ifndef __HH_HEALTH_SPORE_H +#define __HH_HEALTH_SPORE_H + +class hhHealthSpore : public idEntity { + CLASS_PROTOTYPE( hhHealthSpore ); + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - netcode + void WriteToSnapshot( idBitMsgDelta &msg ) const; + void ReadFromSnapshot( const idBitMsgDelta &msg ); + +protected: + void ApplyEffect( idActor* pActor ); + + void Event_Touch( idEntity* pOther, trace_t* pTraceInfo ); + virtual void Event_RespawnSpore(); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_inventory.cpp b/src/Prey/game_inventory.cpp new file mode 100644 index 0000000..8acd43c --- /dev/null +++ b/src/Prey/game_inventory.cpp @@ -0,0 +1,579 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +void hhInventory::Clear() { + memset( &requirements, 0, sizeof( requirements ) ); + maxSpirit = 0; + bHasDeathwalked = false; + storedHealth = 0; + energyType = "energy_plasma"; + memset( altMode, 0, sizeof( altMode ) ); + memset( weaponRaised, 0, sizeof( weaponRaised ) ); + memset( lastShot, 0, sizeof( lastShot ) ); //HUMANHEAD bjk PATCH 7-27-06 + zoomFov = 0; + + idInventory::Clear(); +} + +void hhInventory::Save(idSaveGame *savefile) const { + idInventory::Save( savefile ); + + savefile->WriteBool(bHasDeathwalked); + savefile->Write(&maxSpirit, sizeof(maxSpirit)); + savefile->Write(&requirements, sizeof(requirements)); + savefile->WriteInt( storedHealth ); + savefile->WriteString( energyType.c_str() ); + savefile->Write(&altMode, sizeof(altMode)); + savefile->Write(&weaponRaised, sizeof(weaponRaised)); + savefile->WriteInt( zoomFov ); +} + +void hhInventory::Restore( idRestoreGame *savefile ) { + idInventory::Restore( savefile ); + + savefile->ReadBool(bHasDeathwalked); + savefile->Read(&maxSpirit, sizeof(maxSpirit)); + savefile->Read(&requirements, sizeof(requirements)); + savefile->ReadInt( storedHealth ); + savefile->ReadString( energyType ); + savefile->Read(&altMode, sizeof(altMode)); + savefile->Read(&weaponRaised, sizeof(weaponRaised)); + memset( lastShot, 0, sizeof( lastShot ) ); //HUMANHEAD bjk PATCH 7-27-06 + savefile->ReadInt( zoomFov ); +} + +/* +============== +hhInventory::GetPersistantData + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +============== +*/ +void hhInventory::GetPersistantData( idDict &dict ) { + int i; + int num; + idDict *item; + idStr key; + const idKeyValue *kv; + const char *name; + + // don't bother with powerups or the clip + + // maxhealth, maxspirit + dict.SetInt( "maxhealth", maxHealth); + dict.SetInt( "max_ammo_spiritpower", maxSpirit); + dict.SetBool( "bHasDeathwalked", bHasDeathwalked ); + dict.SetInt( "storedHealth", storedHealth ); + dict.Set( "energyType", energyType.c_str() ); //HUMANHEAD bjk + dict.SetInt( "zoomFov", zoomFov ); //HUMANHEAD bjk + + // ammo + for( i = 0; i < AMMO_NUMTYPES; i++ ) { + name = idWeapon::GetAmmoNameForNum( ( ammo_t )i ); + if ( name ) { + dict.SetInt( name, ammo[ i ] ); + } + } + + //HUMANHEAD bjk: weapons + for( i = 0; i < MAX_WEAPONS; i++ ) { + sprintf( key, "altMode_%i", i ); + dict.SetBool( key, altMode[i] ); + sprintf( key, "weaponRaised_%i", i ); + dict.SetBool( key, weaponRaised[i] ); + } + //HUMANHEAD END + + // items + num = 0; + for( i = 0; i < items.Num(); i++ ) { + item = items[ i ]; + + // copy all keys with "inv_" + kv = item->MatchPrefix( "inv_" ); + if ( kv ) { + while( kv ) { + sprintf( key, "item_%i %s", num, kv->GetKey().c_str() ); + dict.Set( key, kv->GetValue() ); + kv = item->MatchPrefix( "inv_", kv ); + } + + // HUMANHEAD CJR: copy all keys with "def_" + // Needed for Hunter Hand GUI + kv = item->MatchPrefix( "def_" ); + if ( kv ) { + while( kv ) { + sprintf( key, "item_%i %s", num, kv->GetKey().c_str() ); + dict.Set( key, kv->GetValue() ); + kv = item->MatchPrefix( "def_", kv ); + } + } // HUMANHEAD END + + // HUMANHEAD CJR: copy all keys with "passtogui_" + // Needed for Hunter Hand GUI + kv = item->MatchPrefix( "passtogui_" ); + if ( kv ) { + while( kv ) { + sprintf( key, "item_%i %s", num, kv->GetKey().c_str() ); + dict.Set( key, kv->GetValue() ); + kv = item->MatchPrefix( "passtogui_", kv ); + } + } // HUMANHEAD END + + num++; + } + } + dict.SetInt( "items", num ); + + // weapons + dict.SetInt( "weapon_bits", weapons ); + + dict.SetInt( "levelTriggers", levelTriggers.Num() ); + for ( i = 0; i < levelTriggers.Num(); i++ ) { + sprintf( key, "levelTrigger_Level_%i", i ); + dict.Set( key, levelTriggers[i].levelName ); + sprintf( key, "levelTrigger_Trigger_%i", i ); + dict.Set( key, levelTriggers[i].triggerName ); + } +} + + +/* +============== +hhInventory::RestoreInventory + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +============== +*/ +void hhInventory::RestoreInventory( idPlayer *owner, const idDict &dict ) { + int i; + int num; + idDict *item; + idStr key; + idStr itemname; + const idKeyValue *kv; + const char *name; + + Clear(); + + // health + maxHealth = dict.GetInt( "maxhealth", "100" ); + bHasDeathwalked = dict.GetBool( "bHasDeathwalked" ); + storedHealth = dict.GetInt( "storedHealth", "0" ); + energyType = dict.GetString( "energyType", "energy_plasma" ); + zoomFov = dict.GetInt( "zoomFov" ); + + // the clip and powerups aren't restored + + // max spirit + maxSpirit = dict.GetInt( "max_ammo_spiritpower" ); + + // ammo + for( i = 0; i < AMMO_NUMTYPES; i++ ) { + name = idWeapon::GetAmmoNameForNum( ( ammo_t )i ); + if ( name ) { + ammo[ i ] = dict.GetInt( name ); + } + } + + //HUMANHEAD bjk: weapons + for( i = 0; i < MAX_WEAPONS; i++ ) { + sprintf( key, "altMode_%i", i ); + altMode[i] = dict.GetBool( key ); + sprintf( key, "weaponRaised_%i", i ); + weaponRaised[i] = dict.GetBool( key ); + } + //HUMANHEAD END + + // items + num = dict.GetInt( "items" ); + items.SetNum( num ); + for( i = 0; i < num; i++ ) { + item = new idDict(); + items[ i ] = item; + sprintf( itemname, "item_%i ", i ); + kv = dict.MatchPrefix( itemname ); + while( kv ) { + key = kv->GetKey(); + key.Strip( itemname ); + item->Set( key, kv->GetValue() ); + kv = dict.MatchPrefix( itemname, kv ); + } + } + + //HUMANHEAD aob: in addition to the persistent items, give hardcoded items from players.def + Give( owner, dict, "item", dict.GetString( "item" ), NULL, true ); + //HUMANHEAD END + + // weapons are stored as a number for persistant data, but as strings in the entityDef + weapons = dict.GetInt( "weapon_bits", "0" ); + Give( owner, dict, "weapon", dict.GetString( "weapon" ), NULL, false ); + + num = dict.GetInt( "levelTriggers" ); + for ( i = 0; i < num; i++ ) { + sprintf( itemname, "levelTrigger_Level_%i", i ); + idLevelTriggerInfo lti; + lti.levelName = dict.GetString( itemname ); + sprintf( itemname, "levelTrigger_Trigger_%i", i ); + lti.triggerName = dict.GetString( itemname ); + levelTriggers.Append( lti ); + } + + // Keep weapon switch HUD element from showing up at level load + weaponPulse = false; +} + +int hhInventory::MaxAmmoForAmmoClass( idPlayer *owner, const char *ammo_classname ) const { + int max = 0; + if (ammo_classname != NULL) { + if (!idStr::Icmp(ammo_classname, "ammo_spiritpower")) { + max = maxSpirit; + } + else { + max = idInventory::MaxAmmoForAmmoClass(owner, ammo_classname); + } + } + return max; +} + +// PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +void hhInventory::AddPickupName( const char *name, const char *icon, bool bIsWeapon) { + if ( idStr::Length(icon) > 0 ) { + idItemInfo &info = pickupItemNames.Alloc(); + + if ( !idStr::Icmpn( name, STRTABLE_ID, strlen( STRTABLE_ID ) ) ) { + info.name = common->GetLanguageDict()->GetString( name ); + } else { + info.name = name; + } + info.icon = icon; + info.time = 0; + info.slotZeroTime = 0; + info.matcolorAlpha = 0.0f; + info.bDoubleWide = bIsWeapon; + } +} + +/* +============== +hhInventory::Give + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +============== +*/ +bool hhInventory::Give( idPlayer *owner, const idDict &spawnArgs, const char *statname, const char *value, int *idealWeapon, bool updateHud ) { + int i; + const char *pos; + const char *end; + int len; + idStr weaponString; + int max; + const idDeclEntityDef *weaponDecl; + bool tookWeapon; + int amount; + idItemInfo info; + hhPlayer* playerOwner; + + + if( owner && owner->IsType( hhPlayer::Type ) ) { + playerOwner = static_cast(owner); + } + + if ( !idStr::Icmp( statname, "health" ) || !idStr::Icmp( statname, "healthspecial" ) ) { //healthspecial is for mp and indicates that this item can push a player's health up to the "real" maxhealth + int localMaxHealth = maxHealth; + if (gameLocal.isMultiplayer) { //rww - only pipes can put us above 100 in mp + if (idStr::Icmp( statname, "healthspecial" )) { + localMaxHealth = MAX_HEALTH_NORMAL_MP; + } + } + if ( playerOwner->health >= localMaxHealth ) { + return false; + } + int oldHealth = playerOwner->health; + playerOwner->health += atoi( value ); + if ( playerOwner->health > localMaxHealth ) { + playerOwner->health = localMaxHealth; + } + if (playerOwner) { + playerOwner->healthPulse = true; + } + } else if ( !idStr::Icmpn( statname, "ammo_", 5 ) ) { + i = AmmoIndexForAmmoClass( statname ); + max = MaxAmmoForAmmoClass( owner, statname ); + if ( ammo[ i ] >= max ) { + return false; + } + amount = atoi( value ); + if ( amount ) { + ammo[ i ] += amount; + if ( ( max > 0 ) && ( ammo[ i ] > max ) ) { + ammo[ i ] = max; + } + ammoPulse = true; + } + if (playerOwner && !idStr::Icmp(statname, "ammo_spiritpower")) { + playerOwner->spiritPulse = true; + } + } else if ( !idStr::Icmp( statname, "item" ) ) { + pos = value; + while( pos != NULL ) { + end = strchr( pos, ',' ); + if ( end ) { + len = end - pos; + end++; + } else { + len = strlen( pos ); + } + + idStr itemName( pos, 0, len ); + + GiveItem( spawnArgs, gameLocal.FindEntityDefDict(itemName.c_str(), false) ); + + pos = end; + } + } else if ( !idStr::Icmp( statname, "weapon" ) ) { + tookWeapon = false; + for( pos = value; pos != NULL; pos = end ) { + end = strchr( pos, ',' ); + if ( end ) { + len = end - pos; + end++; + } else { + len = strlen( pos ); + } + + idStr weaponName( pos, 0, len ); + + // find the number of the matching weapon name + for( i = 1; i < MAX_WEAPONS; i++ ) { + if ( weaponName == playerOwner->GetWeaponName(i) ) { + break; + } + } + + if ( i >= MAX_WEAPONS ) { + gameLocal.Error( "Unknown weapon '%s'", weaponName.c_str() ); + } + + // cache the media for this weapon + weaponDecl = gameLocal.FindEntityDef( weaponName, false ); + + // don't pickup "no ammo" weapon types twice + // not for D3 SP .. there is only one case in the game where you can get a no ammo + // weapon when you might already have it, in that case it is more conistent to pick it up + if ( gameLocal.isMultiplayer && weaponDecl && ( weapons & ( 1 << i ) ) && !weaponDecl->dict.GetInt( "ammoRequired" ) ) { + continue; + } + + if ( !gameLocal.world->spawnArgs.GetBool( "no_Weapons" ) || ( weaponName == "weaponobj_fists" ) ) { + playerOwner->UnlockWeapon( i ); //TODO add key for disabling this + if ( ( weapons & ( 1 << i ) ) == 0 || gameLocal.isMultiplayer ) { + if ( (owner->GetUserInfo()->GetBool( "ui_autoSwitch" ) || !gameLocal.isMultiplayer) && idealWeapon ) { + // HUMANHEAD pdm: added spirit check, so we don't autoswitch to weapons when picking them up in spriitwalk + // HUMANHEAD pdm: also added check for spiritweapon, don't autoswitch to it. + if (!static_cast(owner)->IsSpiritOrDeathwalking() && weaponName.Icmp("weaponobj_bow")) { + assert( !gameLocal.isClient ); + *idealWeapon = i; + } + } + + // Pulse if not picking up the spirit bow + if (weaponName.Icmp("weaponobj_bow") != 0) { + weaponPulse = true; + } + weapons |= ( 1 << i ); + tookWeapon = true; + } + } + } + return tookWeapon; + } + else if ( !idStr::Icmp( statname, "maxhealth" ) ) { + if (owner) { + if (gameLocal.GetLocalPlayer() == owner) { //sp, listen server + if (owner->hud) { + owner->hud->HandleNamedEvent( "maxHealthPulse" ); + } + } + else if (gameLocal.isMultiplayer && !gameLocal.isClient) { //otherwise, broadcast event to owner + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + msg.Init(msgBuf, sizeof(msgBuf)); + msg.WriteBits((1<ServerSendEvent(idPlayer::EVENT_MENUEVENT, &msg, false, -1, owner->entityNumber); + } + } + if (gameLocal.isMultiplayer) { //rww - different behaviour for mp + maxHealth = atoi(value); + } + else { + maxHealth += atoi(value); + } + } + else if ( !idStr::Icmp( statname, "maxspirit" ) ) { + owner->hud->HandleNamedEvent( "maxSpiritPulse" ); + maxSpirit += atoi(value); + } + else { + // unknown item + return false; + } + + return true; +} + +/* +============== +hhInventory::GiveItem +============== +*/ +bool hhInventory::GiveItem( const idDict& spawnArgs, const idDict* item ) { + idStr itemName; + + if( !item ) { + return false; + } + + idDict* dict = new idDict( *item ); + + if( !FindItem(item) ) { + items.Append( dict ); + return true; + } + + SAFE_DELETE_PTR( dict ); + return false; +} + +/* +=============== +hhInventory::HasAmmo +=============== +*/ +int hhInventory::HasAmmo( ammo_t type, int amount ) { + assert(type >= 0 && type < AMMO_NUMTYPES); + + if ( ( type == idWeapon::GetAmmoNumForName("ammo_none") ) || !amount ) { + // always allow weapons that don't use ammo to fire + return -1; + } + + // check if we have infinite ammo + if ( ammo[ type ] < 0 ) { + return -1; + } + + // return how many shots we can fire + return ammo[ type ] / amount; +} + +/* +=============== +hhInventory::HasAmmo +=============== +*/ +int hhInventory::HasAmmo( const char *weapon_classname ) { + int ammoRequired; + ammo_t ammo_i = AmmoIndexForWeaponClass( weapon_classname, &ammoRequired ); + return HasAmmo( ammo_i, ammoRequired ); +} + +/* +=============== +hhInventory::HasAltAmmo +HUMANHEAD bjk +=============== +*/ +int hhInventory::HasAltAmmo( const char *weapon_classname ) { + int ammoRequired; + ammo_t ammo_i = AltAmmoIndexForWeaponClass( weapon_classname, &ammoRequired ); + return HasAmmo( ammo_i, ammoRequired ); +} + +/* +=============== +hhInventory::UseAmmo +=============== +*/ +bool hhInventory::UseAmmo( ammo_t type, int amount ) { + if ( !HasAmmo( type, amount ) ) { + return false; + } + + // take an ammo away if not infinite + if ( ammo[ type ] >= 0 ) { + ammo[ type ] -= amount; + //rww - don't forget this, it's important! + ammoPredictTime = gameLocal.time; // mp client: we predict this. mark time so we're not confused by snapshots + } + + return true; +} + +float hhInventory::AmmoPercentage(idPlayer *player, ammo_t type) { + float amount = ammo[type]; + const char *ammoName = idWeapon::GetAmmoNameForNum( type ); + float max = MaxAmmoForAmmoClass( player, ammoName ); + max = max(1, max); + return amount / max; +} + +/* +=============== +hhInventory::FindInventoryItem +=============== +*/ +idDict* hhInventory::FindItem( const idDict* dict ) { + if( !dict ) { + return NULL; + } + + return FindItem( dict->GetString("inv_name") ); +} + +/* +=============== +hhInventory::FindInventoryItem +=============== +*/ +idDict* hhInventory::FindItem( const char *name ) { + const char* lname = NULL; + + for( int ix = 0; ix < items.Num(); ++ix ) { + if( !items[ix] ) { + continue; + } + + lname = items[ix]->GetString( "inv_name" ); + if ( lname && *lname ) { + if ( idStr::Icmp( name, lname ) == 0 ) { + return items[ix]; + } + } + } + return NULL; +} + +/* +=============== +hhInventory::EvaluateRequirements +=============== +*/ +void hhInventory::EvaluateRequirements(idPlayer *p) { + if (p) { + requirements.bCanDeathWalk = gameLocal.RequirementMet(p, p->spawnArgs.GetString("requirement_deathwalk"), 0); + requirements.bCanSpiritWalk = gameLocal.RequirementMet(p, p->spawnArgs.GetString("requirement_spiritwalk"), 0); + requirements.bCanSummonTalon = gameLocal.RequirementMet(p, p->spawnArgs.GetString("requirement_talon"), 0); + requirements.bCanUseBowVision = gameLocal.RequirementMet(p, p->spawnArgs.GetString("requirement_bowvision"), 0); + requirements.bCanUseLighter = gameLocal.RequirementMet(p, p->spawnArgs.GetString("requirement_lighter"), 0); + requirements.bCanWallwalk = gameLocal.RequirementMet(p, p->spawnArgs.GetString("requirement_wallwalk"), 0); + requirements.bHunterHand = gameLocal.RequirementMet(p, p->spawnArgs.GetString("requirement_hunterhand"), 0); + + // See if talon should be spawned + if (p->IsType(hhPlayer::Type)) { + static_cast(p)->TrySpawnTalon(); + } + } +} + diff --git a/src/Prey/game_inventory.h b/src/Prey/game_inventory.h new file mode 100644 index 0000000..18a0bcb --- /dev/null +++ b/src/Prey/game_inventory.h @@ -0,0 +1,54 @@ + +#ifndef __PREY_GAME_INVENTORY_H__ +#define __PREY_GAME_INVENTORY_H__ + + +class hhInventory : public idInventory { + +public: + virtual void GetPersistantData( idDict &dict ); + virtual void RestoreInventory( idPlayer *owner, const idDict &dict ); + virtual bool Give( idPlayer *owner, const idDict &spawnArgs, const char *statname, const char *value, int *idealWeapon, bool updateHud ); + virtual void Clear(); + virtual int MaxAmmoForAmmoClass( idPlayer *owner, const char *ammo_classname ) const; + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + bool GiveItem( const idDict& spawnArgs, const idDict* item ); + int HasAmmo( ammo_t type, int amount ); + bool UseAmmo( ammo_t type, int amount ); + int HasAmmo( const char *weapon_classname ); + int HasAltAmmo( const char *weapon_classname ); + float AmmoPercentage(idPlayer *player, ammo_t ammoType); + + idDict* FindItem( const idDict* dict ); + idDict* FindItem( const char *name ); + void EvaluateRequirements( idPlayer *p ); + void AddPickupName( const char *name, const char *icon, bool bIsWeapon ); + + // Requirements are precomputed after any inventory item change, for faster access + struct { + bool bCanWallwalk : 1; + bool bCanSpiritWalk : 1; + bool bCanSummonTalon : 1; + bool bCanUseBowVision : 1; + bool bCanUseLighter : 1; + bool bHunterHand : 1; + bool bCanDeathWalk : 1; + } requirements; + int maxSpirit; + bool bHasDeathwalked; + int storedHealth; + + //bjk: persistent weapons + idStr energyType; + bool altMode[ MAX_WEAPONS ]; + bool weaponRaised[ MAX_WEAPONS ]; + int lastShot[ MAX_WEAPONS ]; //HUMANHEAD bjk PATCH 7-27-06 + int zoomFov; +}; + + +#endif /* __PREY_INVENTORY_H__ */ + diff --git a/src/Prey/game_itemautomatic.cpp b/src/Prey/game_itemautomatic.cpp new file mode 100644 index 0000000..e665b82 --- /dev/null +++ b/src/Prey/game_itemautomatic.cpp @@ -0,0 +1,227 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + +hhItemAutomatic + +***********************************************************************/ + +CLASS_DECLARATION( idEntity, hhItemAutomatic ) +END_CLASS + + +//============================================================================= +// +// hhItemAutomatic::Spawn +// +//============================================================================= +void hhItemAutomatic::Spawn() { + GetPhysics()->SetContents( CONTENTS_TRIGGER ); + BecomeActive( TH_THINK ); +} + +//============================================================================= +// +// hhItemAutomatic::Think +// +//============================================================================= + +void hhItemAutomatic::Think() { + if ( thinkFlags & TH_THINK ) { // Guard against the first level-load think + // We only think when we are in the PVS. We think once and remove ourselves. + hhPlayer *player; + player = static_cast< hhPlayer *>( gameLocal.GetLocalPlayer() ); + if ( player && player->CheckFOV( this->GetOrigin() ) ) { + SpawnItem(); + } + } +} + +//============================================================================= +// +// hhItemAutomatic::SpawnItem +// +//============================================================================= + +void hhItemAutomatic::SpawnItem() { + hhPlayer *player; + idDict Args; + idStr itemName = NULL; + idEntity *spawned = NULL; + + // Remove this object and spawn in a new item in it's place + PostEventMS( &EV_Remove, 0 ); + + Args.SetBool( "spin", 0 ); + Args.SetFloat( "triggersize", 48.0f ); + Args.SetFloat( "respawn", 0.0f ); + Args.SetBool( "enablePickup", false ); + Args.SetFloat( "wander_radius", 12.0f ); // in case the item spawned is a crawler, don't let it wander very far + Args.Set( "target", spawnArgs.GetString( "target" ) ); // Pass the target on to the spawned item + + player = static_cast(gameLocal.GetLocalPlayer()); + + itemName = GetNewItem(); + + if ( itemName.IsEmpty() ) { + return; + } + + spawned = gameLocal.SpawnObject( itemName.c_str(), &Args ); + if ( spawned ) { + spawned->SetOrigin( this->GetOrigin() ); + spawned->SetAxis( this->GetAxis() ); + + if ( spawned->IsType( hhItem::Type ) ) { + hhItem *item = static_cast( spawned ); + item->EnablePickup(); + } + } +} + +//============================================================================= +// +// hhItemAutomatic::GetNewItem +// +//============================================================================= + +idStr hhItemAutomatic::GetNewItem() { + int i; + idStr defaultName; + float skipPercent; + int numAmmo; + + idList weaponNames; + idList weaponIndexes; + idList validWeapon; + idList ammoPercent; + idList ammoTypes; + idList itemNames; + idList skipPercentString; + + float total; + + bool bDontSkip = spawnArgs.GetBool( "bDontSkip", "0" ); + + // This is the heart of the DDA system: + // list all available weapons that can have ammo in the cabinet and how much ammo the player has + + hhPlayer *player = static_cast(gameLocal.GetLocalPlayer()); + + // Build lists of all valid weapon names, ammo types and ammo names + // Note that all these must line up -- so the first weapon corresponds with the first ammo type and the first item name + hhUtils::SplitString( idStr(spawnArgs.GetString( "weaponNames" )), weaponNames ); + hhUtils::SplitString( idStr(spawnArgs.GetString( "ammoTypes" )), ammoTypes ); + hhUtils::SplitString( idStr(spawnArgs.GetString( "itemNames" )), itemNames ); + hhUtils::SplitString( idStr(spawnArgs.GetString( "skipPercents" )), skipPercentString ); + + numAmmo = weaponNames.Num(); + + if ( skipPercentString.Num() != numAmmo ) { + gameLocal.Error( "hhItemAutomatic::GetNewItem: skipPercent.Num() != weaponNames.Num()\n" ); + } + + weaponIndexes.SetNum( numAmmo ); + validWeapon.SetNum( numAmmo ); + ammoPercent.SetNum( numAmmo ); + + for( i = 0; i < weaponNames.Num(); i++ ) { + weaponIndexes[i] = player->GetWeaponNum( weaponNames[i].c_str() ); + } + + // compute percentages of each ammo compared to the max allowed + for( i = 0; i < numAmmo; i++ ) { + validWeapon[i] = false; + if ( player->inventory.weapons & (1 << weaponIndexes[i] ) ) { + int ammoIndex = player->inventory.AmmoIndexForAmmoClass( ammoTypes[i].c_str() ); + ammoPercent[i] = player->inventory.AmmoPercentage( player, ammoIndex ); + + if ( !bDontSkip ) { // Facility to guarantee that ammo won't be skipped + float adjustFactor = FindAmmoNearby( itemNames[i].c_str() ); // If similar ammo is nearby, then reduce the chance of this spawning that ammo + ammoPercent[i] *= adjustFactor; + + skipPercent = idMath::ClampFloat( 0.0f, 1.0f, (float)atof( skipPercentString[i].c_str() ) ); + skipPercent *= (1.0f - (gameLocal.GetDDAValue() * 0.5f + 0.25f)); // CJR TEST: Scale based upon DDA + + if ( ammoPercent[i] > skipPercent ) { // Skip this weapon if the player weapon percentage is high enough + validWeapon[i] = false; + continue; + } + } + + ammoPercent[i] = 1.0f - ammoPercent[i]; // Reverse it to make the math below a bit simpler + + if ( ammoPercent[i] == 0.0f ) { + ammoPercent[i] = 0.1f; // Give full ammo at least a slight chance + } + + validWeapon[i] = true; + } + } + + // re-compute the total of all percentages + total = 0; + for( i = 0; i < numAmmo; i++ ) { + if ( validWeapon[i] ) { + total += ammoPercent[i]; + } + } + + // random number from 0 - total + float random = gameLocal.random.RandomFloat() * total; + + // calculate which ammo that number is associated with and add that item to the cabinet + for( i = 0; i < numAmmo; i++ ) { + if ( validWeapon[i] ) { + if ( random <= ammoPercent[i] ) { // This is the ammo we want + return itemNames[i]; + } + + random -= ammoPercent[i]; // Not the item, so remove this percent and check the next value + } + } + + return NULL; +} + +//============================================================================= +// +// hhItemAutomatic::FindAmmoNearby +// +//============================================================================= + +float hhItemAutomatic::FindAmmoNearby( const char *ammoName ) { + int i; + int e; + hhItem *ent; + idEntity *entityList[ MAX_GENTITIES ]; + int numListedEntities; + idBounds bounds; + idVec3 org; + float adjustFactor = 1.0f; + + float nearbySize = spawnArgs.GetFloat( "nearbySize", "512" ); + + org = GetPhysics()->GetOrigin(); + for ( i = 0 ; i < 3 ; i++ ) { + bounds[0][i] = org[i] - nearbySize; + bounds[1][i] = org[i] + nearbySize; + } + + // Find the closest ammo types that are the same class + numListedEntities = gameLocal.clip.EntitiesTouchingBounds( bounds, -1, entityList, MAX_GENTITIES ); + + for ( e = 0 ; e < numListedEntities ; e++ ) { + ent = static_cast< hhItem * >( entityList[e] ); + + const char *name = ent->spawnArgs.GetString( "classname" ); + if ( !idStr::Icmp( name, ammoName ) ) { + adjustFactor += spawnArgs.GetFloat( "nearbyReduction", "0.25" ); + } + } + + return adjustFactor; +} \ No newline at end of file diff --git a/src/Prey/game_itemautomatic.h b/src/Prey/game_itemautomatic.h new file mode 100644 index 0000000..5dab05b --- /dev/null +++ b/src/Prey/game_itemautomatic.h @@ -0,0 +1,24 @@ +#ifndef __HH_ITEM_AUTOMATIC_H +#define __HH_ITEM_AUTOMATIC_H + +/*********************************************************************** + +hhItemAutomatic + +***********************************************************************/ + +class hhItemAutomatic : public idEntity { + CLASS_PROTOTYPE( hhItemAutomatic ); + +public: + void Spawn(); + virtual void Think(); + + +protected: + void SpawnItem(); + idStr GetNewItem(); + float FindAmmoNearby( const char *ammoName ); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_itemcabinet.cpp b/src/Prey/game_itemcabinet.cpp new file mode 100644 index 0000000..da1fba6 --- /dev/null +++ b/src/Prey/game_itemcabinet.cpp @@ -0,0 +1,417 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_GiveItems("", "e"); + +/*********************************************************************** + +hhItemCabinet + +***********************************************************************/ + +const idEventDef EV_EnableItemClip( "" ); + +CLASS_DECLARATION( hhAnimatedEntity, hhItemCabinet ) + EVENT( EV_Activate, hhItemCabinet::Event_Activate ) + EVENT( EV_EnableItemClip, hhItemCabinet::Event_EnableItemClip ) + EVENT( EV_Broadcast_AppendFxToList, hhItemCabinet::Event_AppendFxToIdleList ) + EVENT( EV_PostSpawn, hhItemCabinet::Event_PostSpawn ) //rww +END_CLASS + +/* +================ +hhItemCabinet::hhItemCabinet +================ +*/ +hhItemCabinet::hhItemCabinet() { +} + +/* +================ +hhItemCabinet::Spawn +================ +*/ +void hhItemCabinet::Spawn() { + animDoneTime = 0; + + GetPhysics()->SetContents( CONTENTS_BODY ); + + ResetItemList(); + + InitBoneInfo(); + + if (!gameLocal.isClient) { + PostEventMS(&EV_PostSpawn, 0); + } + + StartSound( "snd_idle", SND_CHANNEL_IDLE ); +} + +/* +================ +hhItemCabinet::Event_PostSpawn +================ +*/ +void hhItemCabinet::Event_PostSpawn(void) { + SpawnIdleFX(); +} + +/* +================ +hhItemCabinet::~hhItemCabinet +================ +*/ +hhItemCabinet::~hhItemCabinet() { + + for( int i = 0; i < CABINET_MAX_ITEMS; i++) { + SAFE_REMOVE( itemList[i] ); + } + + hhUtils::RemoveContents< idEntityPtr >( idleFxList, true ); + + StopSound( SND_CHANNEL_IDLE ); +} + +/* +================ +hhItemCabinet::InitBoneInfo +================ +*/ +void hhItemCabinet::InitBoneInfo() { + boneList[0] = idStr( spawnArgs.GetString("bone_shelfTop") ); + boneList[1] = idStr(spawnArgs.GetString("bone_shelfMiddle") ); + boneList[2] = idStr(spawnArgs.GetString("bone_shelfBottom") ); +} + +void hhItemCabinet::Save(idSaveGame *savefile) const { + savefile->WriteInt( animDoneTime ); + + int num = idleFxList.Num(); + savefile->WriteInt( num ); + for( int i = 0; i < num; i++ ) { + idleFxList[i].Save( savefile ); + } + + for( int i = 0; i < CABINET_MAX_ITEMS; i++ ) { + itemList[i].Save( savefile ); + } + + for( int i = 0; i < CABINET_MAX_ITEMS; i++ ) { + savefile->WriteString( boneList[i] ); + } +} + +void hhItemCabinet::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( animDoneTime ); + + int num; + savefile->ReadInt( num ); + idleFxList.Clear(); + idleFxList.SetNum( num ); + for( int i = 0; i < num; i++ ) { + idleFxList[i].Restore( savefile ); + } + + for( int i = 0; i < CABINET_MAX_ITEMS; i++ ) { + itemList[i].Restore( savefile ); + } + + for( int i = 0; i < CABINET_MAX_ITEMS; i++ ) { + savefile->ReadString( boneList[i] ); + } +} + +/* +================ +hhItemCabinet::PlayAnim +================ +*/ +void hhItemCabinet::PlayAnim( const char* pAnimName, int iBlendTime ) { + PlayAnim( GetAnimator()->GetAnim(pAnimName), iBlendTime ); +} + +/* +================ +hhItemCabinet::PlayAnim +================ +*/ +void hhItemCabinet::PlayAnim( int pAnim, int iBlendTime ) { + int iAnimLength = 0; + + if( pAnim ) { + animator.ClearAllAnims( gameLocal.GetTime(), 0 ); + animator.PlayAnim( ANIMCHANNEL_ALL, pAnim, gameLocal.GetTime(), iBlendTime ); + iAnimLength = GetAnimator()->GetAnim( pAnim )->Length(); + } + + animDoneTime = gameLocal.GetTime() + iAnimLength; +} + +/* +================ +hhItemCabinet::SpawnIdleFX +================ +*/ +void hhItemCabinet::SpawnIdleFX() { + hhUtils::RemoveContents< idEntityPtr >( idleFxList, true ); + + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_idle"), spawnArgs.GetString("bone_idleRight"), NULL, &EV_Broadcast_AppendFxToList ); + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_idle"), spawnArgs.GetString("bone_idleLeft"), NULL, &EV_Broadcast_AppendFxToList ); +} + +//============================================================================= +// +// hhItemCabinet::SpawnDefaultItems +// +//============================================================================= + +bool hhItemCabinet::SpawnDefaultItems() { + idList items; + + if ( spawnArgs.GetString( "items", NULL ) ) { + hhUtils::SplitString( idStr(spawnArgs.GetString( "items" )), items ); + + int slot = 0; + for( int i = 0; i < items.Num(); i++ ) { + AddItem( items[i], slot ); + if ( ++slot >= CABINET_MAX_ITEMS ) { + return true; + } + } + + return true; + } + + return false; // No default items +} + +/* +================ +hhItemCabinet::SpawnItems +================ +*/ + +void hhItemCabinet::SpawnItems() { + int i; + idStr defaultName; + + int numAmmo; + + idList weaponNames; + idList weaponIndexes; + idList validWeapon; + idList ammoPercent; + idList ammoTypes; + idList itemNames; + + float total; + + bool bAutomatic = true; + + if ( !bAutomatic ) { // Items were placed by a designer, so don't automatically add any items + return; + } + + // This is the heart of the DDA system: + // list all available weapons that can have ammo in the cabinet and how much ammo the player has + + hhPlayer *player = static_cast(gameLocal.GetLocalPlayer()); + + // Build lists of all valid weapon names, ammo types and ammo names + // Note that all these must line up -- so the first weapon corresponds with the first ammo type and the first item name + hhUtils::SplitString( idStr(spawnArgs.GetString( "weaponNames" )), weaponNames ); + hhUtils::SplitString( idStr(spawnArgs.GetString( "ammoTypes" )), ammoTypes ); + hhUtils::SplitString( idStr(spawnArgs.GetString( "itemNames" )), itemNames ); + + numAmmo = weaponNames.Num(); + + weaponIndexes.SetNum( numAmmo ); + validWeapon.SetNum( numAmmo ); + ammoPercent.SetNum( numAmmo ); + + for( i = 0; i < weaponNames.Num(); i++ ) { + weaponIndexes[i] = player->GetWeaponNum( weaponNames[i].c_str() ); + } + + // compute percentages of each ammo compared to the max allowed + for( i = 0; i < numAmmo; i++ ) { + validWeapon[i] = false; + if ( player->inventory.weapons & (1 << weaponIndexes[i] ) ) { + int ammoIndex = player->inventory.AmmoIndexForAmmoClass( ammoTypes[i].c_str() ); + ammoPercent[i] = 1.0f - player->inventory.AmmoPercentage( player, ammoIndex ); + if ( ammoPercent[i] == 0.0f ) { // Give full ammo weapons a slight chance + ammoPercent[i] = 0.01f; + } + validWeapon[i] = true; + } + } + + // for each slot in the cabinet: + for( int slot = 0; slot < numAmmo; slot++ ) { + if ( gameLocal.random.RandomFloat() < spawnArgs.GetFloat( "emptyChance", "0.15" ) ) { // Chance the slot is empty + continue; + } + + // re-compute the total of all percentages + total = 0; + for( i = 0; i < numAmmo; i++ ) { + if ( validWeapon[i] ) { + total += ammoPercent[i]; + } + } + + // random number from 0 - total + float random = gameLocal.random.RandomFloat() * total; + + // calculate which ammo that number is associated with and add that item to the cabinet + for( i = 0; i < numAmmo; i++ ) { + if ( validWeapon[i] ) { + if ( random <= ammoPercent[i] ) { // This is the ammo we want + AddItem( itemNames[i], slot ); // Add the item + ammoPercent[i] *= spawnArgs.GetFloat( "repeatReduce", "0.5" ); // reduce this item's chance of being chosen for the next slot + break; // No need to check further, go to the next slot + } + + random -= ammoPercent[i]; // Not the item, so remove this percent and check the next value + } + } + } +} + +/* +================ +hhItemCabinet::ResetItemList +================ +*/ +void hhItemCabinet::ResetItemList() { + for( int i = 0; i < CABINET_MAX_ITEMS; i++ ) { + SAFE_REMOVE( itemList[i] ); + } +} + +/* +================ +hhItemCabinet::AddItem +================ +*/ +void hhItemCabinet::AddItem( idStr itemName, int slot ) { + idDict Args; + hhItem* item = NULL; + + if ( slot < 0 || slot >= CABINET_MAX_ITEMS ) { + return; + } + + Args.SetBool( "spin", 0 ); + Args.SetFloat( "triggersize", 48.0f ); + Args.SetFloat( "respawn", 0.0f ); + Args.SetBool( "enablePickup", false ); + + item = static_cast( gameLocal.SpawnObject( itemName.c_str(), &Args ) ); + + BroadcastFxInfoAlongBone( spawnArgs.GetString("fx_shelf"), boneList[slot] ); + + // Check if the item should be rotated + bool bRotated = false; + for ( int i = 0; i < spawnArgs.GetInt( "numRotated" ); i++ ) { + if ( !idStr::Icmp( spawnArgs.GetString( va("rotate%d", i) ), itemName.c_str() ) ) { + bRotated = true; + } + } + + if( item ) { + const char *boneName = boneList[ slot ]; + + idVec3 boneOffset; + idMat3 boneAxis; + this->GetJointWorldTransform( boneName, boneOffset, boneAxis ); + + if ( bRotated ) { + item->SetOrigin( boneOffset + GetAxis()[1] * -14 + GetAxis()[2] * 8 ); + item->SetAxis( idMat3( idVec3( 1, 0, 0 ), idVec3( 0, 0, -1), idVec3( 0, 1, 0 ) ) * GetAxis() ); + } else { + item->MoveToJoint( this, boneName ); + item->SetAxis( GetAxis() ); + } + item->BindToJoint( this, boneName, false ); + + itemList[ slot ] = item; + return; + } +} + +/* +=============== +hhItemCabinet::HandleSingleGuiCommand +=============== +*/ +bool hhItemCabinet::HandleSingleGuiCommand( idEntity *entityGui, idLexer *src ) { + + idToken token; + + if( !src->ReadToken(&token) ) { + return false; + } + + if( token == ";" ) { + return false; + } + + if( token.Icmp("openCabinet") == 0 ) { + Event_Activate( NULL ); + return true; + } + + src->UnreadToken( &token ); + return false; +} + +/* +================ +hhItemCabinet::Event_AppendFxToIdleList +================ +*/ +void hhItemCabinet::Event_AppendFxToIdleList( hhEntityFx* fx ) { + idleFxList.Append( fx ); +} + +/* +================ +hhItemCabinet::Event_Activate +================ +*/ +void hhItemCabinet::Event_Activate( idEntity* pActivator ) { + if( gameLocal.GetTime() < animDoneTime ) { + return; + } + + if ( !SpawnDefaultItems() ) { + SpawnItems(); + } + + PlayAnim( "open", 0 ); + StartSound( "snd_open", SND_CHANNEL_ANY ); + PostEventMS( &EV_EnableItemClip, animDoneTime - gameLocal.GetTime() ); + + ActivateTargets(pActivator); + + StartSound( "snd_idle_open", SND_CHANNEL_IDLE ); +} + +/* +================ +hhItemCabinet::Event_EnableItemClip +================ +*/ +void hhItemCabinet::Event_EnableItemClip() { + for( int i = 0; i < CABINET_MAX_ITEMS; i++ ) { + if( !itemList[i].IsValid() ) { + continue; + } + + itemList[i]->EnablePickup(); + itemList[i] = NULL; + } +} diff --git a/src/Prey/game_itemcabinet.h b/src/Prey/game_itemcabinet.h new file mode 100644 index 0000000..1591eb2 --- /dev/null +++ b/src/Prey/game_itemcabinet.h @@ -0,0 +1,51 @@ +#ifndef __HH_ITEM_CABINET_H +#define __HH_ITEM_CABINET_H + +/*********************************************************************** + +hhItemCabinet + +***********************************************************************/ +#define CABINET_MAX_ITEMS 3 + +class hhItemCabinet: public hhAnimatedEntity { + CLASS_PROTOTYPE( hhItemCabinet ); + +public: + hhItemCabinet(); + virtual ~hhItemCabinet(); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void InitBoneInfo(); + + void PlayAnim( const char* pAnimName, int iBlendTime ); + void PlayAnim( int pAnim, int iBlendTime ); + + void ResetItemList(); + void AddItem( idStr itemName, int slot ); + bool SpawnDefaultItems(); + void SpawnItems(); + void SpawnIdleFX(); + + bool HandleSingleGuiCommand( idEntity *entityGui, idLexer *src ); + +protected: + void Event_AppendFxToIdleList( hhEntityFx* fx ); + void Event_Activate( idEntity* pActivator ); + void Event_EnableItemClip(); + virtual void Event_PostSpawn(void); + +protected: + int animDoneTime; + + idList< idEntityPtr > idleFxList; + + idEntityPtr itemList[ CABINET_MAX_ITEMS ]; + idStr boneList[ CABINET_MAX_ITEMS ]; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_jukebox.cpp b/src/Prey/game_jukebox.cpp new file mode 100644 index 0000000..22a5e36 --- /dev/null +++ b/src/Prey/game_jukebox.cpp @@ -0,0 +1,265 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +CLASS_DECLARATION(hhSound, hhJukeBoxSpeaker) +END_CLASS + + +const idEventDef EV_SetNumTracks("setNumTracks", "d"); +const idEventDef EV_SetTrack("setTrack", "d"); +const idEventDef EV_PlaySelected("playSelected", NULL); +const idEventDef EV_TrackOver("", NULL); +const idEventDef EV_SetJukeboxVolume("setvolume", "f"); + +CLASS_DECLARATION(hhConsole, hhJukeBox) + EVENT( EV_SetNumTracks, hhJukeBox::Event_SetNumTracks) + EVENT( EV_SetTrack, hhJukeBox::Event_SetTrack) + EVENT( EV_PlaySelected, hhJukeBox::Event_PlaySelected) + EVENT( EV_TrackOver, hhJukeBox::Event_TrackOver) + EVENT( EV_SetJukeboxVolume, hhJukeBox::Event_SetVolume) +END_CLASS + + +void hhJukeBox::Spawn() { + track = 1; + currentHistorySample = 0; + numTracks = spawnArgs.GetInt("numtracks", "1"); + volume = spawnArgs.GetFloat("volume"); + UpdateView(); +} + +void hhJukeBox::Save(idSaveGame *savefile) const { + int i; + + savefile->WriteFloat( volume ); + savefile->WriteInt( track ); + savefile->WriteInt( numTracks ); + savefile->WriteInt( currentHistorySample ); + + savefile->WriteInt( speakers.Num() ); // Saving of idList + for( i = 0; i < speakers.Num(); i++ ) { + savefile->WriteObject(speakers[i]); + } +} + +void hhJukeBox::Restore( idRestoreGame *savefile ) { + int i, num; + + savefile->ReadFloat( volume ); + savefile->ReadInt( track ); + savefile->ReadInt( numTracks ); + savefile->ReadInt( currentHistorySample ); + + speakers.Clear(); + savefile->ReadInt( num ); + speakers.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->ReadObject( reinterpret_cast(speakers[i]) ); + } + + UpdateVolume(); +} + +void hhJukeBox::ConsoleActivated() { + BecomeActive(TH_MISC3); +} + +void hhJukeBox::UpdateView() { + idUserInterface *gui = renderEntity.gui[0]; + + if (gui) { + gui->SetStateInt("track", track); + gui->SetStateFloat("volume", volume); + } +} + +void hhJukeBox::ClearSpectrum() { + idUserInterface *gui = renderEntity.gui[0]; + if (gui) { + gui->SetStateFloat("amplitude", 0.0f); + + for (int ix=0; ix<10; ix++) { + gui->SetStateFloat(va("amplitude%d", ix), 0.0f); + } + gui->StateChanged(gameLocal.time); + } +} + +void hhJukeBox::SetTrack(int newTrack) { + track = idMath::ClampInt(1, numTracks, newTrack); +} + +void hhJukeBox::PlayCurrentTrack() { + const char *shaderName = spawnArgs.GetString(va("snd_song%d", track), NULL); + const idSoundShader *shader = declManager->FindSound(shaderName); + int time = 0; + + // Clear up any previous state, even if using another mixer at the time + StopCurrentTrack(); + + // In OpenAL, samples that are out of range pause instead of mute so the targetted speakers can get out of sync. + if (shader && targets.Num() && !cvarSystem->GetCVarBool("s_useOpenAL")) { + for (int ix=0; ixStartSoundShader(shader, SND_CHANNEL_VOICE, 0, true, &time); + } + } + } + else { + StartSound(va("snd_song%d", track), SND_CHANNEL_VOICE, 0, true, &time); + } + UpdateVolume(); + CancelEvents(&EV_TrackOver); + PostEventMS(&EV_TrackOver, time + 500); +} + +void hhJukeBox::StopCurrentTrack() { + CancelEvents(&EV_TrackOver); + ClearSpectrum(); + + // Stop speakers and jukebox so no mixer switches can screw us up + StopSound(SND_CHANNEL_VOICE, true); + for (int ix=0; ixStopSound(SND_CHANNEL_VOICE, true); + } + } +} + +bool hhJukeBox::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + + idToken token; + + if (!src->ReadToken(&token)) { + return false; + } + + if (token == ";") { + return false; + } + + if (token.Icmp("prevtrack") == 0) { + SetTrack(track-1); + UpdateView(); + } + else if (token.Icmp("nexttrack") == 0) { + SetTrack(track+1); + UpdateView(); + } + else if (token.Icmp("selecttrack") == 0) { + BecomeActive(TH_MISC3); + PlayCurrentTrack(); + UpdateView(); + } + else if (token.Icmp("turnoff") == 0) { + StopCurrentTrack(); + BecomeInactive(TH_MISC3); + UpdateView(); + } + else if (token.Icmp("turnon") == 0) { + BecomeActive(TH_MISC3); + UpdateView(); + } + else if (token.Icmp("volumeup") == 0) { + volume = idMath::ClampFloat(0.0f, 1.0f, volume + 0.02f); + UpdateVolume(); + UpdateView(); + } + else if (token.Icmp("volumedown") == 0) { + volume = idMath::ClampFloat(0.0f, 1.0f, volume - 0.02f); + UpdateVolume(); + UpdateView(); + } + else { + src->UnreadToken(&token); + return false; + } + + return true; +} + +void hhJukeBox::UpdateEntityVolume(idEntity *ent) { + ent->HH_SetSoundVolume(volume, SND_CHANNEL_VOICE); +} + +void hhJukeBox::UpdateVolume() { + if (targets.Num() && !cvarSystem->GetCVarBool("s_useOpenAL")) { + for (int ix=0; ixCurrentlyPlaying()) { + amplitude = refSound.referenceSound->CurrentAmplitude(); + } + else if (targets.Num() && targets[0].IsValid() && targets[0].GetEntity()->IsType(hhSound::Type) ) { + amplitude = static_cast(targets[0].GetEntity())->GetCurrentAmplitude(SND_CHANNEL_VOICE); + } + + amplitude = idMath::ClampFloat(0.0f, 1.0f, amplitude); + idUserInterface *gui = renderEntity.gui[0]; // Interface area + if (gui) { + gui->SetStateFloat("amplitude", amplitude); + + currentHistorySample = (currentHistorySample+1)%10; + gui->SetStateFloat(va("amplitude%d", currentHistorySample), amplitude); + gui->StateChanged(gameLocal.time); + } + gui = renderEntity.gui[1]; // Outer jukebox + if (gui) { + gui->SetStateFloat("amplitude", amplitude); + } + } +} + +void hhJukeBox::Event_SetNumTracks(int newNumTracks) { + numTracks = newNumTracks; +} + +void hhJukeBox::Event_SetTrack(int newTrack) { + SetTrack(newTrack); + UpdateView(); +} + +void hhJukeBox::Event_PlaySelected() { + PlayCurrentTrack(); + BecomeActive(TH_MISC3); + UpdateView(); +} + +void hhJukeBox::Event_TrackOver() { + ClearSpectrum(); + //loop to beginning + if ( track+1 > numTracks ) { + SetTrack(1); + } else { + SetTrack(track+1); + } + PlayCurrentTrack(); + UpdateView(); +} + +void hhJukeBox::Event_SetVolume(float vol) { + volume = idMath::ClampFloat(0.0f, 1.0f, vol); + UpdateVolume(); + UpdateView(); +} + + diff --git a/src/Prey/game_jukebox.h b/src/Prey/game_jukebox.h new file mode 100644 index 0000000..1eb210c --- /dev/null +++ b/src/Prey/game_jukebox.h @@ -0,0 +1,47 @@ + +#ifndef __GAME_JUKEBOX_H__ +#define __GAME_JUKEBOX_H__ + + +class hhJukeBox : public hhConsole { +public: + CLASS_PROTOTYPE( hhJukeBox ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + virtual void ConsoleActivated(); + virtual void Think(); + + void ClearSpectrum(); + void SetTrack(int track); + void PlayCurrentTrack(); + void StopCurrentTrack(); + void UpdateView(); + void UpdateVolume(); + void UpdateEntityVolume(idEntity *ent); + + void Event_SetNumTracks(int newNumTracks); + void Event_SetTrack(int newTrack); + void Event_PlaySelected(); + void Event_TrackOver(); + void Event_SetVolume(float vol); + +protected: + float volume; + int track; + int numTracks; + int currentHistorySample; + idList speakers; +}; + + +class hhJukeBoxSpeaker : public hhSound { +public: + CLASS_PROTOTYPE( hhJukeBoxSpeaker ); +}; + + +#endif diff --git a/src/Prey/game_jumpzone.cpp b/src/Prey/game_jumpzone.cpp new file mode 100644 index 0000000..3897b0a --- /dev/null +++ b/src/Prey/game_jumpzone.cpp @@ -0,0 +1,129 @@ +// Game_JumpZone.cpp +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +const idEventDef EV_ResetSlopeCheck("", "d"); + +CLASS_DECLARATION(hhTrigger, hhJumpZone) + EVENT( EV_Touch, hhJumpZone::Event_Touch ) + EVENT( EV_Enable, hhJumpZone::Event_Enable ) + EVENT( EV_Disable, hhJumpZone::Event_Disable ) + EVENT( EV_ResetSlopeCheck, hhJumpZone::Event_ResetSlopeCheck ) +END_CLASS + +void hhJumpZone::Spawn(void) { + + velocity = spawnArgs.GetVector("velocity"); + pitchDegrees = spawnArgs.GetFloat("jumpPitch"); + GetPhysics()->SetContents( CONTENTS_TRIGGER ); +} + +void hhJumpZone::Save(idSaveGame *savefile) const { + savefile->WriteVec3( velocity ); + savefile->WriteFloat( pitchDegrees ); +} + +void hhJumpZone::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3( velocity ); + savefile->ReadFloat( pitchDegrees ); +} + + +// Given a pitch angle, calculate a speed to get us to destination +static float JumpBallistics( const idVec3 &start, const idVec3 &end, float pitch, float gravity ) { +/* + speed = sqrt( + -0.5f * gravity * ( x / cos(pitch) )^2 + ----------------------------------------- + y - x * tan(pitch) + ); +*/ + float pitchRadians = DEG2RAD(pitch); + float speed = 0.0f; + idVec3 toTarget = end - start; + float dist = toTarget.Length(); + float a = dist / idMath::Cos(pitchRadians); + float num = -0.5f * gravity * a*a; + float den = toTarget.z - dist * idMath::Tan(pitchRadians); + if (den != 0.0f) { + speed = idMath::Sqrt( num / den ); + } + return speed; +} + +idVec3 hhJumpZone::CalculateJumpVelocity() { + idVec3 destination = GetOrigin() + idVec3(0,0,200); + if (targets.Num() == 0 || !targets[0].IsValid()) { + // Use explicit velocity + return velocity; + } + + destination = targets[0]->GetOrigin(); + + idVec3 toTarget = destination - GetOrigin(); + + // Given the angle, calculate a speed to get us to destination + float speed = JumpBallistics(GetOrigin(), destination, pitchDegrees, DEFAULT_GRAVITY); + + idAngles ang; + ang.Set(-pitchDegrees, toTarget.ToYaw(), 0.0f); + return ang.ToForward() * speed; +} + +/* +================ +hhJumpZone::Event_Enable +================ +*/ +void hhJumpZone::Event_Enable( void ) { + GetPhysics()->SetContents( CONTENTS_TRIGGER ); +} + +/* +================ +hhJumpZone::Event_Disable +================ +*/ +void hhJumpZone::Event_Disable( void ) { + GetPhysics()->SetContents( 0 ); +} + + +/* +================ +hhJumpZone::Event_Touch +================ +*/ +void hhJumpZone::Event_Touch( idEntity *other, trace_t *trace ) { + if (other) { + // Enable slope checks on player in case it was turned off by gravity zone. Need it on to + // recognize getting thrown off the ground + if (other->IsType( hhPlayer::Type ) && other->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + static_cast(other->GetPhysics())->SetSlopeCheck(true); + // Post an event to turn it back off? + other->PostEventMS(&EV_ResetSlopeCheck, 200, other->entityNumber); + } + other->GetPhysics()->SetLinearVelocity( CalculateJumpVelocity() ); + } +} + +void hhJumpZone::Event_ResetSlopeCheck(int entNum) { + idEntity *entityList[100]; + + // If the player is still encroaching on an inward gravity zone, reset slope check (off) + idEntity *player = gameLocal.entities[entNum]; + if (player && player->IsType(hhPlayer::Type) && player->GetPhysics()->IsType(hhPhysics_Player::Type)) { + int num = gameLocal.clip.EntitiesTouchingBounds(player->GetPhysics()->GetAbsBounds(), CONTENTS_TRIGGER, entityList, 100); + for (int ix=0; ixIsType(hhGravityZoneInward::Type)) { + static_cast(player->GetPhysics())->SetSlopeCheck(false); + break; + } + } + } +} \ No newline at end of file diff --git a/src/Prey/game_jumpzone.h b/src/Prey/game_jumpzone.h new file mode 100644 index 0000000..d593dc5 --- /dev/null +++ b/src/Prey/game_jumpzone.h @@ -0,0 +1,28 @@ +// Game_JumpZone.h +// + +#ifndef __GAME_JUMPZONE_H__ +#define __GAME_JUMPZONE_H__ + +class hhJumpZone : public hhTrigger { +public: + CLASS_PROTOTYPE( hhJumpZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + idVec3 CalculateJumpVelocity(); + void Event_Touch( idEntity *other, trace_t *trace ); + void Event_Enable( void ); + void Event_Disable( void ); + void Event_ResetSlopeCheck(int entNum); + +public: + idVec3 velocity; + float pitchDegrees; +}; + + +#endif /* __GAME_JUMPZONE_H__ */ diff --git a/src/Prey/game_light.cpp b/src/Prey/game_light.cpp new file mode 100644 index 0000000..4c6c68b --- /dev/null +++ b/src/Prey/game_light.cpp @@ -0,0 +1,103 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhLight + +***********************************************************************/ + +//HUMANHEAD: aob +const idEventDef EV_Light_StartAltMode( "startAltMode" ); +//HUMANHEAD END + +CLASS_DECLARATION( idLight, hhLight ) + EVENT( EV_PostSpawn, hhLight::Event_SetTargetHandles ) + EVENT( EV_ResetTargetHandles, hhLight::Event_SetTargetHandles ) + EVENT( EV_Light_StartAltMode, hhLight::Event_StartAltMode ) +END_CLASS + +/* +================ +hhLight::SetLightCenter + +HUMANHEAD cjr +================ +*/ +void hhLight::SetLightCenter( idVec3 center ) { + renderLight.lightCenter = center; + PresentLightDefChange(); +} + +/* +================ +hhLight::StartAltSound +================ +*/ +void hhLight::StartAltSound() { + if ( refSound.shader ) { + StopSound( SND_CHANNEL_ANY ); + const idSoundShader *alternate = refSound.shader->GetAltSound(); + if ( alternate ) { + StartSoundShader( alternate, SND_CHANNEL_ANY ); + } + } +} + +/* +================ +hhLight::Event_SetTargetHandles + + set the same sound def handle on all targeted entities + +HUMANHEAD: aob +================ +*/ +void hhLight::Event_SetTargetHandles( void ) { + int i; + idEntity *targetEnt = NULL; + + if ( !refSound.referenceSound ) { + return; + } + + for( i = 0; i < targets.Num(); i++ ) { + targetEnt = targets[ i ].GetEntity(); + if ( targetEnt ) { + if( targetEnt->IsType(idLight::Type) ) { + static_cast(targetEnt)->SetLightParent( this ); + } + + targetEnt->FreeSoundEmitter( true ); + + // manually set the refSound to this light's refSound + targetEnt->GetRenderEntity()->referenceSound = renderEntity.referenceSound; + + // update the renderEntity to the renderer + targetEnt->UpdateVisuals(); + } + } +} + +/* +================ +hhLight::Event_StartAltMode +================ +*/ +void hhLight::Event_StartAltMode() { + //Copied fron idLight::BecomeBroken + + // offset the start time of the shader to sync it to the game time + renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.time ); + renderLight.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.time ); + + // set the state parm + renderEntity.shaderParms[ SHADERPARM_MODE ] = 1; + renderLight.shaderParms[ SHADERPARM_MODE ] = 1; + + StartAltSound(); + + UpdateVisuals(); +} \ No newline at end of file diff --git a/src/Prey/game_light.h b/src/Prey/game_light.h new file mode 100644 index 0000000..fde0488 --- /dev/null +++ b/src/Prey/game_light.h @@ -0,0 +1,25 @@ +#ifndef __HH_GAME_LIGHT_H +#define __HH_GAME_LIGHT_H + +/* +=============================================================================== + +hhLight + +=============================================================================== +*/ + +class hhLight : public idLight { + CLASS_PROTOTYPE( hhLight ); + +public: + void SetLightCenter( idVec3 center ); + + void StartAltSound(); + +protected: + void Event_SetTargetHandles( void ); + void Event_StartAltMode(); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_lightfixture.cpp b/src/Prey/game_lightfixture.cpp new file mode 100644 index 0000000..3449e86 --- /dev/null +++ b/src/Prey/game_lightfixture.cpp @@ -0,0 +1,210 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhAFEntity, hhLightFixture ) + EVENT( EV_PostSpawn, hhLightFixture::Event_PostSpawn ) + EVENT( EV_Hide, hhLightFixture::Event_Hide ) + EVENT( EV_Show, hhLightFixture::Event_Show ) +END_CLASS + +/* +=============== +hhLightFixture::Spawn +=============== +*/ +void hhLightFixture::Spawn() { + collisionBone = INVALID_JOINT; + boundLight = NULL; + + PostEventMS( &EV_PostSpawn, 10 ); +} + +void hhLightFixture::Save(idSaveGame *savefile) const { + savefile->WriteInt( collisionBone ); + boundLight.Save(savefile); +} + +void hhLightFixture::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( (int &)collisionBone ); + boundLight.Restore(savefile); +} + +/* +=============== +hhLightFixture::~hhLightFixture +=============== +*/ +hhLightFixture::~hhLightFixture() { + RemoveLight(); +} + +/* +=============== +hhLightFixture::Damage +=============== +*/ +void hhLightFixture::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if( collisionBone != INVALID_JOINT && (location == INVALID_JOINT || collisionBone == location) ) { + hhAFEntity::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + } +} + +/* +=============== +hhLightFixture::Killed +=============== +*/ +void hhLightFixture::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + hhAFEntity::Killed( inflictor, attacker, damage, dir, location ); + + if( StillBound(boundLight.GetEntity()) ) { + //boundLight->SetShader( spawnArgs.GetString("mtr_lightDestroyed") ); + boundLight->BecomeBroken( attacker ); + } + + const char* skinName = spawnArgs.GetString( "skin_destroyed" ); + if( skinName && skinName[0] ) { + SetSkinByName( skinName ); + } + + // offset the start time of the shader to sync it to the game time + renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.time ); + + // set the state parm + renderEntity.shaderParms[ SHADERPARM_MODE ] = 1; + + UpdateVisuals(); +} + +/* +=============== +hhLightFixture::GetBoundLight +=============== +*/ +void hhLightFixture::GetBoundLight() { + idVec3 color; + + boundLight = SearchForBoundLight(); + if( boundLight.IsValid() ) { + boundLight->fl.takedamage = false; + + collisionBone = GetAnimator()->GetJointHandle( boundLight->spawnArgs.GetString("bindToJoint") ); + + SetColor( boundLight->spawnArgs.GetVector("_color", "1 1 1") ); + } +} + + +/* +=============== +hhLightFixture::SearchForBoundLight +=============== +*/ +idLight* hhLightFixture::SearchForBoundLight() { + for( idEntity* entity = GetTeamChain(); entity; entity = entity->GetTeamChain() ) { + if( entity && entity->IsType(idLight::Type) ) { + return static_cast( entity ); + } + } + + return NULL; +} + +/* +=============== +hhLightFixture::StillBound +=============== +*/ +bool hhLightFixture::StillBound( const idLight* light ) { + return (light) ? light->IsBoundTo(this) : false; +} + +/* +=============== +hhLightFixture::RemoveLight +=============== +*/ +void hhLightFixture::RemoveLight() { + SAFE_REMOVE( boundLight ); +} + +/* +================ +hhLightFixture::Present +================ +*/ +void hhLightFixture::Present( void ) { + // don't present to the renderer if the entity hasn't changed + if ( !( thinkFlags & TH_UPDATEVISUALS ) ) { + return; + } + + // add the model + hhAFEntity::Present(); + + // reference the sound for shader synced effects + if ( boundLight.IsValid() && StillBound(boundLight.GetEntity()) ) { + renderEntity.referenceSound = boundLight->GetSoundEmitter(); + } + else { + renderEntity.referenceSound = refSound.referenceSound; + } + + PresentModelDefChange(); +} + +/* +================ +hhLightFixture::PresentModelDefChange +================ +*/ +void hhLightFixture::PresentModelDefChange( void ) { + + if ( !renderEntity.hModel || IsHidden() ) { + return; + } + + // add to refresh list + if ( modelDefHandle == -1 ) { + modelDefHandle = gameRenderWorld->AddEntityDef( &renderEntity ); + } else { + gameRenderWorld->UpdateEntityDef( modelDefHandle, &renderEntity ); + } +} + +/* +=============== +hhLightFixture::Event_PostSpawn +=============== +*/ +void hhLightFixture::Event_PostSpawn() { + GetBoundLight(); +} + +/* +=============== +hhLightFixture::Event_Hide +=============== +*/ +void hhLightFixture::Event_Hide() { + idEntity::Event_Hide(); + + if( boundLight.IsValid() ) { + boundLight->Off(); + } +} + +/* +=============== +hhLightFixture::Event_Show +=============== +*/ +void hhLightFixture::Event_Show() { + idEntity::Event_Show(); + + if( boundLight.IsValid() ) { + boundLight->On(); + } +} \ No newline at end of file diff --git a/src/Prey/game_lightfixture.h b/src/Prey/game_lightfixture.h new file mode 100644 index 0000000..5db3229 --- /dev/null +++ b/src/Prey/game_lightfixture.h @@ -0,0 +1,37 @@ +#ifndef __HH_LIGHT_FIXTURE_H +#define __HH_LIGHT_FIXTURE_H + +class hhLightFixture : public hhAFEntity { + CLASS_PROTOTYPE( hhLightFixture ); + +public: + ~hhLightFixture(); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void Present( void ); + +protected: + void GetBoundLight(); + idLight* SearchForBoundLight(); + bool StillBound( const idLight* light ); + void RemoveLight(); + + void PresentModelDefChange( void ); + +protected: + void Event_PostSpawn(); + void Event_Hide(); + void Event_Show(); + +protected: + jointHandle_t collisionBone; + + idEntityPtr boundLight; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_mine.cpp b/src/Prey/game_mine.cpp new file mode 100644 index 0000000..93d8874 --- /dev/null +++ b/src/Prey/game_mine.cpp @@ -0,0 +1,432 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//========================================================================== +// +// hhMine +// +//========================================================================== + +const idEventDef EV_ExplodeDamage( "explodeDamage", "e" ); +const idEventDef EV_MineHover( "" ); + +CLASS_DECLARATION(hhMoveable, hhMine) + EVENT( EV_Activate, hhMine::Event_Trigger) + EVENT( EV_ExplodeDamage, hhMine::Event_ExplodeDamage) + EVENT( EV_Remove, hhMine::Event_Remove) + EVENT( EV_ExplodedBy, hhMine::Event_ExplodedBy ) + EVENT( EV_MineHover, hhMine::Event_MineHover ) +END_CLASS + +void hhMine::Spawn(void) { + spawner = NULL; + fl.takedamage = spawnArgs.GetBool("takedamage", "1"); + + idVec3 worldGravityDir( gameLocal.GetGravity() ); + float gravityMagnitude = spawnArgs.GetFloat( "gravity", va("%.2f", worldGravityDir.Normalize()) ); + GetPhysics()->SetGravity( gravityMagnitude * worldGravityDir ); + + bScaleIn = spawnArgs.GetBool("scalein"); + if (bScaleIn) { + fadeAlpha.Init(gameLocal.time, 2000, 0.01f, 1.0f); + BecomeActive(TH_MISC1); + } + + if (spawnArgs.FindKey("snd_spawn")) { + StartSound( "snd_spawn", SND_CHANNEL_ANY ); + } + + if (spawnArgs.FindKey("snd_idle")) { + StartSound( "snd_idle", SND_CHANNEL_IDLE ); + } + + bDetonateOnCollision = spawnArgs.GetBool("DetonateOnCollision"); + bDamageOnCollision = spawnArgs.GetBool("DamageOnCollision"); + bExploded = false; + + if (!spawnArgs.GetBool("nodrop") && GetPhysics()->GetGravity() == vec3_origin) { + gameLocal.Warning("zero grav object without nodrop will not move: %s", name.c_str()); + } + + if (bDetonateOnCollision) { + SpawnTrigger(); + } +} + +void hhMine::Save(idSaveGame *savefile) const { + savefile->WriteObject( spawner ); + savefile->WriteBool( bDetonateOnCollision ); + savefile->WriteBool( bDamageOnCollision ); + + savefile->WriteFloat( fadeAlpha.GetStartTime() ); // idInterpolate + savefile->WriteFloat( fadeAlpha.GetDuration() ); + savefile->WriteFloat( fadeAlpha.GetStartValue() ); + savefile->WriteFloat( fadeAlpha.GetEndValue() ); + + savefile->WriteBool( bScaleIn ); + savefile->WriteBool( bExploded ); +} + +void hhMine::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadObject( reinterpret_cast(spawner) ); + savefile->ReadBool( bDetonateOnCollision ); + savefile->ReadBool( bDamageOnCollision ); + + savefile->ReadFloat( set ); // idInterpolate + fadeAlpha.SetStartTime( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetDuration( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetStartValue(set); + savefile->ReadFloat( set ); + fadeAlpha.SetEndValue( set ); + + savefile->ReadBool( bScaleIn ); + savefile->ReadBool( bExploded ); +} + +void hhMine::SpawnTrigger() { + idEntity *trigger; + idDict Args; + + Args.Set( "target", name.c_str() ); + Args.Set( "mins", spawnArgs.GetString("triggerMins") ); + Args.Set( "maxs", spawnArgs.GetString("triggerMaxs") ); + Args.Set( "bind", name.c_str() ); + Args.SetVector( "origin", GetOrigin() ); + Args.SetMatrix( "rotation", GetAxis() ); + trigger = gameLocal.SpawnObject( spawnArgs.GetString("def_trigger"), &Args ); +} + +void hhMine::Think() { + if (thinkFlags & TH_MISC1) { + if (bScaleIn) { + if (fadeAlpha.IsDone(gameLocal.time)) { + SetDeformation(DEFORMTYPE_SCALE, 0.0f); // Turn scaling off + bScaleIn = false; + BecomeInactive(TH_MISC1); + } + else { + SetDeformation(DEFORMTYPE_SCALE, fadeAlpha.GetCurrentValue(gameLocal.time)); + } + } + } + //TODO: Could make these come to rest when gravity is zero and not moving + RunPhysics(); + Present(); +} + +void hhMine::Launch(idVec3 &velocity, idVec3 &avelocity) { + GetPhysics()->SetLinearVelocity(velocity); + GetPhysics()->SetAngularVelocity(avelocity); + BecomeActive(TH_MISC1); +} + +void hhMine::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + // skip idMoveable::Damage which handles damage differently + idEntity::Damage(inflictor, attacker, dir, damageDefName, damageScale, location); +} + +void hhMine::ApplyImpulse(idEntity * ent, int id, const idVec3 &point, const idVec3 &impulse) { + if (impulse == vec3_origin && ent->IsType(idActor::Type)) { // Just pushed + // Apply player's velocity to the pod so it moves at same rate as player and keeps going + idVec3 newimpulse = ent->GetPhysics()->GetLinearVelocity() * GetPhysics()->GetMass(); + hhMoveable::ApplyImpulse(ent, id, point, newimpulse); +// GetPhysics()->SetLinearVelocity(ent->GetPhysics()->GetLinearVelocity()*4); + } + if (bDetonateOnCollision) { + // Need to post an event because already in physics code now, can't nest a projectile spawn from within physics code + // currently, because rigid body physics ::Evaluate is not reentrant friendly (has a static timer) + PostEventMS(&EV_ExplodedBy, 0, this); + } + hhMoveable::ApplyImpulse(ent, id, point, impulse); +} + +bool hhMine::AllowCollision( const trace_t &collision ) { + idEntity *ent = gameLocal.entities[collision.c.entityNum]; + if ( ent && ent->IsType(hhShuttleForceField::Type) ) { + return false; // Allow asteroids to go through shuttle forcefields + } + return true; +} + +bool hhMine::Collide( const trace_t &collision, const idVec3 &velocity ) { + const char *decal; + idEntity *ent = gameLocal.entities[collision.c.entityNum]; + + // project decal + decal = spawnArgs.RandomPrefix( "mtr_decal", gameLocal.random ); + if ( decal && *decal ) { + gameLocal.ProjectDecal( collision.c.point, -collision.c.normal, spawnArgs.GetFloat( "decal_trace", "128.0" ), true, spawnArgs.GetFloat( "decal_size", "6.0" ), decal ); + } + + if (bDamageOnCollision) { + const idKeyValue *kv = spawnArgs.FindKey("def_damage"); + if (ent && kv != NULL) { + ent->Damage(this, gameLocal.world, velocity, kv->GetValue().c_str(), 1.0f, 0); + } + } + if (bDetonateOnCollision) { + // Need to post an event because already in physics code now, can't nest a projectile spawn from within physics code + // currently, because rigid body physics ::Evaluate is not reentrant friendly (has a static timer) + PostEventMS(&EV_ExplodedBy, 0, this); + + return true; + } + return false; +} + +void hhMine::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + fl.takedamage = false; // nla - Prevent killed from being called too many times. + + // Need to post an event because already in physics code now, can't nest a projectile spawn from within physics code + // currently, because rigid body physics ::Evaluate is not reentrant friendly (has a static timer) + PostEventMS(&EV_ExplodedBy, 0, attacker); +} + +void hhMine::Explode( idEntity *attacker ) { + hhFxInfo fxInfo; + int splash_damage_delay = SEC2MS( spawnArgs.GetFloat( "splash_damage_delay", "0.5" ) ); + + if (bExploded || IsHidden() ) { + return; + } + + bExploded = true; + + // Activate targets + ActivateTargets( attacker ); + + // Set for removal + if ( spawnArgs.GetBool( "respawn" ) ) { + fl.takedamage = true; + PostEventSec( &EV_Show, spawnArgs.GetFloat( "respawn_delay", "2" ) ); + bExploded = false; + Hide(); + } else { + PostEventMS( &EV_Remove, 1500 + splash_damage_delay ); + GetPhysics()->SetContents( 0 ); + fl.takedamage = false; + Hide(); + } + + RemoveBinds(); + + // Spawn explosion + StopSound( SND_CHANNEL_IDLE ); + StartSound( "snd_explode", SND_CHANNEL_ANY ); + + //fixme: if we stay with moveables, this can be replaced by key "gib" + if ( spawnArgs.GetString("def_debrisspawner")[0] ) { + hhUtils::SpawnDebrisMass(spawnArgs.GetString("def_debrisspawner"), GetOrigin()); + } + + //fixme: fx are spawned by debris system? + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_detonate", GetOrigin(), GetAxis(), &fxInfo ); + + if (spawnArgs.FindKey("def_splash_damage") != NULL) { + PostEventMS(&EV_ExplodeDamage, splash_damage_delay, attacker); + } +} + +bool hhMine::WasSpawnedBy(idEntity *theSpawner) { + return (spawner == theSpawner); +} + +void hhMine::Event_Trigger( idEntity *activator ) { + Explode(activator); +} + +void hhMine::Event_ExplodedBy( idEntity *activator) { + Explode(activator); +} + +void hhMine::Event_MineHover() { + GetPhysics()->SetLinearVelocity( vec3_zero ); +} + +void hhMine::Event_Remove() { + if (spawner) { // notify spawner + spawner->MineRemoved(this); + spawner = NULL; + } + hhMoveable::Event_Remove(); +} + +void hhMine::Event_ExplodeDamage( idEntity *attacker ) { + gameLocal.RadiusDamage( GetPhysics()->GetOrigin(), this, attacker, this, this, spawnArgs.GetString("def_splash_damage") ); +} + + +//========================================================================== +// +// hhMineSpawner +// +//========================================================================== +const idEventDef EV_SpawnMine("SpawnMine", NULL); + +CLASS_DECLARATION(hhAnimatedEntity, hhMineSpawner) + EVENT( EV_Activate, hhMineSpawner::Event_Activate) + EVENT( EV_SpawnMine, hhMineSpawner::Event_SpawnMine) + EVENT( EV_Remove, hhMineSpawner::Event_Remove ) +END_CLASS + +void hhMineSpawner::Spawn(void) { + population = 0; + targetPopulation = spawnArgs.GetInt("population"); + mineVelocity = spawnArgs.GetVector("velocity"); + mineAVelocity = spawnArgs.GetVector("avelocity"); + bRandomDirection = spawnArgs.GetBool("randdir"); + bRandomRotation = spawnArgs.GetBool("randrot"); + spawnDelay = SEC2MS(spawnArgs.GetFloat("spawndelay")); + speed = mineVelocity.Length(); + aspeed = mineAVelocity.Length(); + active = spawnArgs.GetBool("start_on"); + + GetPhysics()->SetContents(0); + if (targetPopulation > 0 && active) { + PostEventMS(&EV_SpawnMine, 2000); + } + BecomeActive(TH_THINK); // Need to be active in order to get dormant messages +} + +void hhMineSpawner::Save(idSaveGame *savefile) const { + savefile->WriteInt( population ); + savefile->WriteInt( targetPopulation ); + savefile->WriteVec3( mineVelocity ); + savefile->WriteVec3( mineAVelocity ); + savefile->WriteBool( bRandomDirection ); + savefile->WriteBool( bRandomRotation ); + savefile->WriteFloat( speed ); + savefile->WriteFloat( aspeed ); + savefile->WriteInt( spawnDelay ); + savefile->WriteBool( active ); +} + +void hhMineSpawner::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( population ); + savefile->ReadInt( targetPopulation ); + savefile->ReadVec3( mineVelocity ); + savefile->ReadVec3( mineAVelocity ); + savefile->ReadBool( bRandomDirection ); + savefile->ReadBool( bRandomRotation ); + savefile->ReadFloat( speed ); + savefile->ReadFloat( aspeed ); + savefile->ReadInt( spawnDelay ); + savefile->ReadBool( active ); +} + +void hhMineSpawner::DormantBegin() { + // Remove all pending spawn events + CancelEvents(&EV_SpawnMine); +} + +void hhMineSpawner::DormantEnd() { + // restart if active + CheckPopulation(); +} + +void hhMineSpawner::CheckPopulation() { + if (active && population < targetPopulation) { + CancelEvents(&EV_SpawnMine); + PostEventMS(&EV_SpawnMine, spawnDelay); + } +} + +void hhMineSpawner::MineRemoved(hhMine *mine) { + --population; + CheckPopulation(); +} + +void hhMineSpawner::SpawnMine() { + if (fl.isDormant) { + return; + } + + const char *mineDefName = spawnArgs.GetString("def_mine"); + idBounds bounds = GetPhysics()->GetAbsBounds(); + idVec3 location = hhUtils::RandomPointInBounds(bounds.Expand(-33)); + + // If won't fit, wait and spawn later + if ( spawnArgs.GetBool( "force_spawn", "0" ) || hhUtils::EntityDefWillFit(mineDefName, location, mat3_identity, CONTENTS_SOLID, NULL)) { + if (bRandomDirection) { + mineVelocity = hhUtils::RandomVector() * speed; + } + if (bRandomRotation) { + mineAVelocity = hhUtils::RandomVector() * aspeed; + } + idDict args; + args.SetVector("origin", location); + + hhMine *mine = static_cast(gameLocal.SpawnObject(mineDefName, &args)); + if (mine) { + ActivateTargets( this ); + mine->SetSpawner(this); + mine->Launch(mineVelocity, mineAVelocity); + float stopDelay = spawnArgs.GetFloat( "stop_delay", "0" ); + if ( stopDelay > 0.0f ) { + mine->PostEventSec( &EV_MineHover, stopDelay ); + } + } + ++population; + CheckPopulation(); + } + else { + // Check population with low delay, since it's already time to spawn + if (active && population < targetPopulation) { + CancelEvents(&EV_SpawnMine); + PostEventMS(&EV_SpawnMine, 500); + } + } +} + +void hhMineSpawner::Event_Activate(idEntity *activator) { + if ( spawnArgs.GetBool( "limit_triggers", "0" ) ) { + active = false; + CancelEvents(&EV_SpawnMine); // Turn off + if ( population < targetPopulation ) { + SpawnMine(); // This also will continue to check population + } + return; + } + + // Use population=0 to spawn purely based on triggers + if (active && targetPopulation != 0) { + active = false; + CancelEvents(&EV_SpawnMine); // Turn off + } + else { + active = true; + SpawnMine(); // This also will continue to check population + } +} + +void hhMineSpawner::Event_SpawnMine() { + SpawnMine(); +} + +void hhMineSpawner::Event_Remove() { + // Handle removal gracefully + idEntity *ent = NULL; + hhMine *mine = NULL; + for (int ix=0; ixIsType(hhMine::Type)) { + mine = static_cast(ent); + if (mine->WasSpawnedBy(this)) { + mine->SetSpawner(NULL); + if ( spawnArgs.GetBool( "explode_on_remove" ) ) { + mine->Explode( NULL ); + } + } + } + } + idEntity::Event_Remove(); +} diff --git a/src/Prey/game_mine.h b/src/Prey/game_mine.h new file mode 100644 index 0000000..2a23b8d --- /dev/null +++ b/src/Prey/game_mine.h @@ -0,0 +1,82 @@ + +#ifndef __GAME_MINE_H__ +#define __GAME_MINE_H__ + +extern const idEventDef EV_ExplodeDamage; + +class hhMineSpawner; + +class hhMine : public hhMoveable { +public: + CLASS_PROTOTYPE( hhMine ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void Launch(idVec3 &velocity, idVec3 &avelocity); + virtual bool AllowCollision( const trace_t &collision ); + virtual void ApplyImpulse(idEntity * ent, int id, const idVec3 &point, const idVec3 &impulse); + virtual bool Collide( const trace_t &collision, const idVec3 &velocity ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + void Explode( idEntity *attacker ); + void SetSpawner(hhMineSpawner *s) { spawner = s; } + bool WasSpawnedBy(idEntity *theSpawner); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Think(); + +protected: + void SpawnTrigger(); + void Event_ExplodeDamage( idEntity *attacker ); + void Event_Trigger( idEntity *activator ); + virtual void Event_Remove(); + void Event_ExplodedBy( idEntity *activator); + void Event_MineHover(); + +protected: + hhMineSpawner * spawner; // Entity that spawned this object + bool bDetonateOnCollision; + bool bDamageOnCollision; + idInterpolate fadeAlpha; + bool bScaleIn; + bool bExploded; +}; + + +extern const idEventDef EV_SpawnMine; + +class hhMineSpawner : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhMineSpawner ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void MineRemoved(hhMine *mine); + void DormantBegin(); + void DormantEnd(); + +protected: + virtual void SpawnMine(); + void CheckPopulation(); + + void Event_Activate(idEntity *activator); + void Event_SpawnMine(); + virtual void Event_Remove(); + +protected: + int population; + int targetPopulation; + idVec3 mineVelocity; + idVec3 mineAVelocity; + bool bRandomDirection; + bool bRandomRotation; + float speed; // Precomputed speed based on mineVelocity + float aspeed; // Precomputed speed based on mineAVelocity + int spawnDelay; + bool active; +}; + + +#endif diff --git a/src/Prey/game_misc.cpp b/src/Prey/game_misc.cpp new file mode 100644 index 0000000..6a43544 --- /dev/null +++ b/src/Prey/game_misc.cpp @@ -0,0 +1,404 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//========================================================================== +// +// hhWallWalkable +// +//========================================================================== + +CLASS_DECLARATION( idStaticEntity, hhWallWalkable ) + EVENT(EV_Activate, hhWallWalkable::Event_Activate) +END_CLASS + +void hhWallWalkable::Spawn( void ) { + wallwalkOn = spawnArgs.GetBool("active"); + flicker = spawnArgs.GetBool("flicker"); + + // Get skin references (already precached) + onSkin = declManager->FindSkin( spawnArgs.GetString("skinOn") ); + offSkin = declManager->FindSkin( spawnArgs.GetString("skinOff") ); + + if (wallwalkOn) { + SetSkin( onSkin ); + alphaOn.Init(gameLocal.time, 0, 1.0f, 1.0f); + } + else { + SetSkin( offSkin ); + alphaOn.Init(gameLocal.time, 0, 0.0f, 0.0f); + } + alphaOn.SetHermiteParms(WALLWALK_HERM_S1, WALLWALK_HERM_S2); + SetShaderParm(4, alphaOn.GetCurrentValue(gameLocal.time)); + UpdateVisuals(); + + fl.networkSync = true; +} + +void hhWallWalkable::Save(idSaveGame *savefile) const { + savefile->WriteFloat( alphaOn.GetStartTime() ); // hhHermiteInterpolate + savefile->WriteFloat( alphaOn.GetDuration() ); + savefile->WriteFloat( alphaOn.GetStartValue() ); + savefile->WriteFloat( alphaOn.GetEndValue() ); + savefile->WriteFloat( alphaOn.GetS1() ); + savefile->WriteFloat( alphaOn.GetS2() ); + + savefile->WriteBool( wallwalkOn ); + savefile->WriteBool( flicker ); + savefile->WriteSkin( onSkin ); + savefile->WriteSkin( offSkin ); +} + +void hhWallWalkable::Restore( idRestoreGame *savefile ) { + float set, set2; + + savefile->ReadFloat( set ); // hhHermiteInterpolate + alphaOn.SetStartTime( set ); + savefile->ReadFloat( set ); + alphaOn.SetDuration( set ); + savefile->ReadFloat( set ); + alphaOn.SetStartValue(set); + savefile->ReadFloat( set ); + alphaOn.SetEndValue( set ); + savefile->ReadFloat( set ); + savefile->ReadFloat( set2 ); + alphaOn.SetHermiteParms(set, set2); + + savefile->ReadBool( wallwalkOn ); + savefile->ReadBool( flicker ); + savefile->ReadSkin( onSkin ); + savefile->ReadSkin( offSkin ); +} + +void hhWallWalkable::Think() { + idEntity::Think(); + if (thinkFlags & TH_THINK) { + SetShaderParm(4, alphaOn.GetCurrentValue(gameLocal.time)); + if (alphaOn.IsDone(gameLocal.time)) { + BecomeInactive(TH_THINK); + if (wallwalkOn) { + } + else { + SetSkin( offSkin ); + } + } + } +} + +void hhWallWalkable::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(wallwalkOn, 1); + msg.WriteBits(IsActive(TH_THINK), 1); +} + +void hhWallWalkable::ReadFromSnapshot( const idBitMsgDelta &msg ) { + bool enabled = !!msg.ReadBits(1); + if (wallwalkOn != enabled) { + SetWallWalkable(enabled); + } + + bool thinking = !!msg.ReadBits(1); + if (thinking != IsActive(TH_THINK)) { + if (thinking) { + BecomeActive(TH_THINK); + } + else { + BecomeInactive(TH_THINK); + } + } +} + +void hhWallWalkable::ClientPredictionThink( void ) { + Think(); +} + +void hhWallWalkable::SetWallWalkable(bool on) { + wallwalkOn = on; + + float curAlpha = alphaOn.GetCurrentValue(gameLocal.time); + + if (wallwalkOn) { // Turning on + BecomeActive(TH_THINK); + SetSkin( onSkin ); + StartSound( "snd_powerup", SND_CHANNEL_ANY ); + alphaOn.Init(gameLocal.time, WALLWALK_TRANSITION_TIME, curAlpha, 1.0f ); + alphaOn.SetHermiteParms(WALLWALK_HERM_S1, WALLWALK_HERM_S2); + } + else { // Turning off + BecomeActive(TH_THINK); + StartSound( "snd_powerdown", SND_CHANNEL_ANY ); + alphaOn.Init(gameLocal.time, WALLWALK_TRANSITION_TIME, curAlpha, 0.0f); + alphaOn.SetHermiteParms(WALLWALK_HERM_S1, 1.0f); // no overshoot + } +} + +void hhWallWalkable::Event_Activate(idEntity *activator) { + SetWallWalkable(!wallwalkOn); +} + +//========================================================================== +// +// hhFuncEmitter +// +//========================================================================== +CLASS_DECLARATION( idStaticEntity, hhFuncEmitter ) + EVENT( EV_Activate, hhFuncEmitter::Event_Activate ) +END_CLASS + +void hhFuncEmitter::Spawn( void ) { + particle = static_cast( declManager->FindType(DECL_PARTICLE, spawnArgs.GetString("smoke_particle"), false) ); + particleStartTime = -1; + + (spawnArgs.GetBool("start_off")) ? Hide() : Show(); +} + +void hhFuncEmitter::Hide() { + idStaticEntity::Hide(); + + renderEntity.shaderParms[SHADERPARM_PARTICLE_STOPTIME] = MS2SEC( gameLocal.time ); + + particleStartTime = -1; + + BecomeInactive( TH_TICKER ); +} + +void hhFuncEmitter::Show() { + idStaticEntity::Show(); + + renderEntity.shaderParms[SHADERPARM_PARTICLE_STOPTIME] = 0; + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] = -MS2SEC( gameLocal.GetTime() ); + + particleStartTime = gameLocal.GetTime(); + + BecomeActive( TH_TICKER ); +} + +void hhFuncEmitter::Save( idSaveGame *savefile ) const { + savefile->WriteParticle( particle ); + savefile->WriteInt( particleStartTime ); +} + +void hhFuncEmitter::Restore( idRestoreGame *savefile ) { + savefile->ReadParticle( particle ); + savefile->ReadInt( particleStartTime ); +} + +void hhFuncEmitter::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteFloat( renderEntity.shaderParms[ SHADERPARM_PARTICLE_STOPTIME ] ); + msg.WriteFloat( renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] ); +} + +void hhFuncEmitter::ReadFromSnapshot( const idBitMsgDelta &msg ) { + renderEntity.shaderParms[ SHADERPARM_PARTICLE_STOPTIME ] = msg.ReadFloat(); + renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] = msg.ReadFloat(); + if ( msg.HasChanged() ) { + UpdateVisuals(); + } +} + +void hhFuncEmitter::Ticker() { + if( IsHidden() ) { + return; + } + + if( particle && particleStartTime != -1 ) { + if( !gameLocal.smokeParticles->EmitSmoke(particle, particleStartTime, gameLocal.random.RandomFloat(), GetOrigin(), GetAxis()) ) { + particleStartTime = -1; + } + } + + if( modelDefHandle != -1 ) { + renderEntity.origin = GetOrigin(); + renderEntity.axis = GetAxis(); + UpdateVisuals(); + } +} + +void hhFuncEmitter::Event_Activate( idEntity *activator ) { + (IsHidden() || spawnArgs.GetBool("cycleTrigger")) ? Show() : Hide(); + + UpdateVisuals(); +} + + +//========================================================================== +// +// hhPathEmitter +// +//========================================================================== + +CLASS_DECLARATION( hhFuncEmitter, hhPathEmitter ) +END_CLASS + +void hhPathEmitter::Spawn() { +} + +//========================================================================== +// +// hhDeathWraithEnergy +// +//========================================================================== + +CLASS_DECLARATION( hhPathEmitter, hhDeathWraithEnergy ) +END_CLASS + +void hhDeathWraithEnergy::Spawn() { + startTime = MS2SEC(gameLocal.time); + duration = spawnArgs.GetFloat("duration"); + + startRadius = spawnArgs.GetFloat("startRadius"); + endRadius = spawnArgs.GetFloat("endRadius"); + startTheta = DEG2RAD(spawnArgs.GetFloat("startTheta")); + endTheta = DEG2RAD(spawnArgs.GetFloat("endTheta")); + startZ = spawnArgs.GetFloat("startZ"); + endZ = spawnArgs.GetFloat("endZ"); + + // For testing + SetDestination(vec3_origin); + SetPlayer(static_cast(gameLocal.GetLocalPlayer())); + + StartSound("snd_idle", SND_CHANNEL_BODY); +} + +void hhDeathWraithEnergy::Save(idSaveGame *savefile) const { + savefile->WriteFloat( startTime ); + savefile->WriteFloat( duration ); + savefile->WriteFloat( startRadius ); + savefile->WriteFloat( endRadius ); + savefile->WriteFloat( startTheta ); + savefile->WriteFloat( endTheta ); + savefile->WriteFloat( startZ ); + savefile->WriteFloat( endZ ); + savefile->WriteVec3( centerPosition ); + thePlayer.Save(savefile); + + savefile->WriteFloat(spline.tension); + savefile->WriteFloat(spline.continuity); + savefile->WriteFloat(spline.bias); + savefile->WriteInt(spline.nodes.Num()); // idList + for (int i=0; iWriteVec3(spline.nodes[i]); + } +} + +void hhDeathWraithEnergy::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( startTime ); + savefile->ReadFloat( duration ); + savefile->ReadFloat( startRadius ); + savefile->ReadFloat( endRadius ); + savefile->ReadFloat( startTheta ); + savefile->ReadFloat( endTheta ); + savefile->ReadFloat( startZ ); + savefile->ReadFloat( endZ ); + savefile->ReadVec3( centerPosition ); + thePlayer.Restore(savefile); + + savefile->ReadFloat(spline.tension); + savefile->ReadFloat(spline.continuity); + savefile->ReadFloat(spline.bias); + + int num; + spline.nodes.Clear(); // idList + savefile->ReadInt(num); + spline.nodes.SetNum(num); + for (int i=0; iReadVec3(spline.nodes[i]); + } +} + +void hhDeathWraithEnergy::SetPlayer(hhPlayer *player) { + thePlayer = player; +} + +void hhDeathWraithEnergy::SetDestination(const idVec3 &destination) { + // Cylindrical support + this->centerPosition = destination; + idVec3 toWraith = GetOrigin() - centerPosition; + CartesianToCylindrical(toWraith, startRadius, startTheta, startZ); + endTheta += startTheta; + + // Spline support + spline.Clear(); + spline.SetControls(spawnArgs.GetFloat("tension"), spawnArgs.GetFloat("continuity"), spawnArgs.GetFloat("bias")); + spline.AddPoint(GetOrigin()); + + if (thePlayer.IsValid() && thePlayer->DeathWalkStage2()) { + idEntity *holeMarker = gameLocal.FindEntity( "dw_floatingBodyMarker" ); + if (holeMarker) { + spline.AddPoint(holeMarker->GetOrigin()); + } + } + + spline.AddPoint(destination); +} + +void hhDeathWraithEnergy::CartesianToCylindrical(idVec3 &cartesian, float &radius, float &theta, float &z) { + radius = cartesian.ToVec2().Length(); + theta = idMath::ATan(cartesian.y, cartesian.x); + z = cartesian.z; +} + +idVec3 hhDeathWraithEnergy::CylindricalToCartesian(float radius, float theta, float z) { + idVec3 cartesian; + cartesian.x = radius * idMath::Cos(theta); + cartesian.y = radius * idMath::Sin(theta); + cartesian.z = z; + return cartesian; +} + +void hhDeathWraithEnergy::Ticker() { + float theta; + float radius; + float z; + + if (!thePlayer.IsValid()) { + return; + } + + float alpha = (MS2SEC(gameLocal.time) - startTime) / duration; + + if (alpha < 1.0f) { + + if (thePlayer->DeathWalkStage2()) { + SetOrigin( spline.GetValue(alpha) ); + } + else { + radius = startRadius + alpha*(endRadius-startRadius); + theta = startTheta + alpha*(endTheta-startTheta); + z = startZ + alpha * (endZ - startZ); + + idVec3 locationRelativeToCenter = CylindricalToCartesian(radius, theta, z); + idEntity *destEntity = thePlayer->GetDeathwalkEnergyDestination(); + if (destEntity) { + centerPosition = destEntity->GetOrigin(); + } + + SetOrigin(centerPosition + locationRelativeToCenter); + } + } + else if (!IsHidden()) { + Hide(); + StopSound(SND_CHANNEL_BODY); + + bool energyHealth = spawnArgs.GetBool("healthEnergy"); + + idEntity *dwProxy = thePlayer->GetDeathwalkEnergyDestination(); + if (dwProxy) { + // Spawn arrival effect + StartSound("snd_arrival", SND_CHANNEL_ANY); + + dwProxy->SetShaderParm(SHADERPARM_TIMEOFFSET, -MS2SEC(gameLocal.time) ); + dwProxy->SetShaderParm(SHADERPARM_MODE, energyHealth ? 2 : 1 ); + } + + // Notify the player + thePlayer->DeathWraithEnergyArived(energyHealth); + + PostEventMS(&EV_Remove, 5000); + } + + hhPathEmitter::Ticker(); +} + + diff --git a/src/Prey/game_misc.h b/src/Prey/game_misc.h new file mode 100644 index 0000000..3123f0f --- /dev/null +++ b/src/Prey/game_misc.h @@ -0,0 +1,100 @@ +#ifndef __HH_MISC_H +#define __HH_MISC_H + +#define WALLWALK_TRANSITION_TIME 2510 // Time over which to fade +#define WALLWALK_HERM_S1 1.0f // Slope at start +#define WALLWALK_HERM_S2 -2.0f // Slope at end + +class hhWallWalkable : public idStaticEntity { + CLASS_PROTOTYPE(hhWallWalkable); + +public: + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - netcode + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + void SetWallWalkable(bool on); + virtual void Think(); + +protected: + void Event_Activate(idEntity *activator); + +protected: + hhHermiteInterpolate alphaOn; // Degree to which wallwalk is on + bool wallwalkOn; + bool flicker; + const idDeclSkin *onSkin; + const idDeclSkin *offSkin; +}; + +class hhFuncEmitter : public idStaticEntity { + CLASS_PROTOTYPE( hhFuncEmitter ); + +public: + void Spawn( void ); + + virtual void Hide(); + virtual void Show(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + +protected: + virtual void Ticker(); + +protected: + void Event_Activate( idEntity *activator ); + +protected: + const idDeclParticle* particle; + int particleStartTime; +}; + + +class hhPathEmitter : public hhFuncEmitter { + CLASS_PROTOTYPE( hhPathEmitter ); + +public: + void Spawn( void ); +}; + + +class hhDeathWraithEnergy : public hhPathEmitter { + CLASS_PROTOTYPE( hhDeathWraithEnergy ); + +public: + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void SetPlayer(hhPlayer *player); + void SetDestination(const idVec3 &destination); + +protected: + virtual void Ticker(); + idVec3 CylindricalToCartesian(float radius, float theta, float z); + void CartesianToCylindrical(idVec3 &cartesian, float &radius, float &theta, float &z); + +protected: + hhTCBSpline spline; + float startTime; + float duration; + float startRadius; + float endRadius; + float startTheta; + float endTheta; + float startZ; + float endZ; + idVec3 centerPosition; + idEntityPtr thePlayer; +}; + +#endif diff --git a/src/Prey/game_modeldoor.cpp b/src/Prey/game_modeldoor.cpp new file mode 100644 index 0000000..e9b1a16 --- /dev/null +++ b/src/Prey/game_modeldoor.cpp @@ -0,0 +1,1076 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// Events +const idEventDef EV_ModelDoorSpawnTrigger( "" ); +const idEventDef EV_ModelDoorOpen( "open" ); +const idEventDef EV_ModelDoorClose( "close" ); + +const idEventDef EV_ModelDoorClosedBegin( "" ); +const idEventDef EV_ModelDoorOpeningBegin( "" ); +const idEventDef EV_ModelDoorOpenBegin( "" ); +const idEventDef EV_ModelDoorClosingBegin( "" ); + +const idEventDef EV_SetBuddiesShaderParm( "setBuddiesShaderParm", "df" ); + +//-------------------------------- +// hhDoorTrigger +//-------------------------------- +CLASS_DECLARATION( idEntity, hhDoorTrigger ) + EVENT( EV_Touch, hhDoorTrigger::Event_TriggerDoor ) +END_CLASS + +//-------------------------------- +// hhDoorTrigger::hhDoorTrigger() +//-------------------------------- +hhDoorTrigger::hhDoorTrigger() { + enabled = true; // Need to have the ability to disable a trigger +} + +//-------------------------------- +// hhDoorTrigger::Event_TriggerDoor +//-------------------------------- +void hhDoorTrigger::Event_TriggerDoor( idEntity *other, trace_t *trace ) { + if( door && IsEnabled() ) { + door->ProcessEvent( &EV_Touch, other, trace ); + } +} + +//--------------------- +// hhDoorTrigger::GetEntitiesWithin +// ents should be an array of idEntity of length entLength +// +// If not enabled, always returns 0 +//--------------------- +int hhDoorTrigger::GetEntitiesWithin( idEntity **ents, int entsLength ) { + int num; + + if ( !enabled ) { + return( 0 ); + } + + num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), MASK_SHOT_BOUNDINGBOX, ents, entsLength ); + + return( num ); + +} + +void hhDoorTrigger::Save( idSaveGame *savefile ) const { + savefile->WriteObject( door ); + savefile->WriteBool( enabled ); +} + +void hhDoorTrigger::Restore( idRestoreGame *savefile ) { + savefile->ReadObject( reinterpret_cast ( door ) ); + savefile->ReadBool( enabled ); +} + +//-------------------------------- +// hhModelDoor +//-------------------------------- +CLASS_DECLARATION( hhAnimatedEntity, hhModelDoor ) + EVENT( EV_TeamBlocked, hhModelDoor::Event_TeamBlocked ) + EVENT( EV_PartBlocked, hhModelDoor::Event_PartBlocked ) + EVENT( EV_Activate, hhModelDoor::Event_Activate ) + EVENT( EV_Touch, hhModelDoor::Event_Touch ) + EVENT( EV_ModelDoorOpen, hhModelDoor::Event_OpenDoor ) + EVENT( EV_ModelDoorClose, hhModelDoor::Event_CloseDoor ) + EVENT( EV_Thread_SetCallback, hhModelDoor::Event_SetCallback ) + + EVENT( EV_SetBuddiesShaderParm, hhModelDoor::Event_SetBuddiesShaderParm ) + + // Internal events + EVENT( EV_ModelDoorSpawnTrigger, hhModelDoor::Event_SpawnNewDoorTrigger ) + EVENT( EV_ModelDoorClosedBegin, hhModelDoor::Event_STATE_ClosedBegin ) + EVENT( EV_ModelDoorOpeningBegin, hhModelDoor::Event_STATE_OpeningBegin ) + EVENT( EV_ModelDoorOpenBegin, hhModelDoor::Event_STATE_OpenBegin ) + EVENT( EV_ModelDoorClosingBegin, hhModelDoor::Event_STATE_ClosingBegin ) +END_CLASS + +//-------------------------------- +// hhModelDoor::Spawn +//-------------------------------- +void hhModelDoor::Spawn( void ) { + idEntity* master = NULL; + + doorTrigger = NULL; + fl.takedamage = true; + SetBlocking(true); + +// spawnArgs.GetFloat( "dmg", "2", &damage ); + spawnArgs.GetFloat( "triggersize", "50", triggersize ); + spawnArgs.GetBool( "no_touch", "0", noTouch ); + spawnArgs.GetInt( "locked", "0", locked ); + spawnArgs.GetFloat( "wait", "1.5", wait ); + if ( wait <= 0.1f ) { + //sanity check for waittime + wait = 0.1f; + } + spawnArgs.GetFloat( "damage", "1", damage ); + spawnArgs.GetFloat( "airlockwait", "3.0", airLockSndWait ); + airLockSndWait *= 1000.0f; // Convert from seconds to ms + + hhUtils::GetValues( spawnArgs, "buddy", buddyNames, true ); + + // If "health" is supplied, door will open when killed + // So this key determines if we should really take damage or not + fl.takedamage = (health > 0); + forcedOpen = false; + bOpenForMonsters = spawnArgs.GetBool("OpenForMonsters", "1"); + + SetShaderParm( SHADERPARM_MODE, GetDoorShaderParm( locked != 0, true ) ); // 2=locked, 1=unlocked, 0=never locked + + //HUMANHEAD: aob - airlock stuff + airlockMaster = NULL; + + airlockTeam.SetOwner( this ); + airlockTeamName = spawnArgs.GetString( "airlockTeam" ); + master = DetermineTeamMaster( GetAirLockTeamName() ); + if( master ) { + airlockMaster = static_cast( master ); + if( airlockMaster != this ) { + JoinAirLockTeam( airlockMaster ); + } + } + //HUMANHEAD END + + openAnim = GetAnimator()->GetAnim( "open" ); + closeAnim = GetAnimator()->GetAnim( "close" ); + idleAnim = GetAnimator()->GetAnim( "idle" ); + painAnim = GetAnimator()->GetAnim( "pain" ); + + // Spawn the trigger + if (!gameLocal.isClient) { + PostEventMS( &EV_ModelDoorSpawnTrigger, 0 ); + } + + // see if we are on an areaportal + areaPortal = gameRenderWorld->FindPortal( GetPhysics()->GetAbsBounds() ); + + StartSound( "snd_idle", SND_CHANNEL_ANY ); + + if( spawnArgs.GetBool("start_open") ) { + StartOpen(); + } else { + StartClosed(); + } + + threadNum = 0; + nextAirLockSnd = 0; + + finishedSpawn = true; + + fl.networkSync = true; + + bShuttleDoors = spawnArgs.GetBool( "shuttle_doors" ); +} + +//-------------------------------- +// hhModelDoor::hhModelDoor +//-------------------------------- +hhModelDoor::hhModelDoor() { + bOpen = false; + bTransition = false; + finishedSpawn = false; + sndTrigger = NULL; + nextSndTriggerTime = 0; +} + +//-------------------------------- +// hhModelDoor::~hhModelDoor +//-------------------------------- +hhModelDoor::~hhModelDoor() { + airlockTeam.Remove(); + if ( sndTrigger ) { + delete sndTrigger; + sndTrigger = NULL; + } +} + +void hhModelDoor::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( damage ); + savefile->WriteFloat( wait ); + savefile->WriteFloat( triggersize ); + savefile->WriteInt( areaPortal ); + if ( areaPortal ) { + savefile->WriteInt( gameRenderWorld->GetPortalState( areaPortal ) ); + } + savefile->WriteStringList( buddyNames ); + savefile->WriteInt( locked ); + savefile->WriteInt( openAnim ); + savefile->WriteInt( closeAnim ); + savefile->WriteInt( idleAnim ); + savefile->WriteInt( painAnim ); + savefile->WriteBool( forcedOpen ); + savefile->WriteBool( bOpenForMonsters ); + savefile->WriteBool( noTouch ); + savefile->WriteInt( threadNum ); + savefile->WriteObject( doorTrigger ); + savefile->WriteString( airlockTeamName ); + savefile->WriteObject( airlockMaster ); + savefile->WriteBounds( crusherBounds ); + + activatedBy.Save( savefile ); + + savefile->WriteBool( bOpen ); + savefile->WriteBool( bTransition ); + savefile->WriteFloat( airLockSndWait ); + + savefile->WriteClipModel( sndTrigger ); + savefile->WriteInt( nextSndTriggerTime ); +} + +void hhModelDoor::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( damage ); + savefile->ReadFloat( wait ); + savefile->ReadFloat( triggersize ); + savefile->ReadInt( areaPortal ); + if ( areaPortal ) { + int portalState; + savefile->ReadInt( portalState ); + gameLocal.SetPortalState( areaPortal, portalState ); + } + savefile->ReadStringList( buddyNames ); + savefile->ReadInt( locked ); + savefile->ReadInt( openAnim ); + savefile->ReadInt( closeAnim ); + savefile->ReadInt( idleAnim ); + savefile->ReadInt( painAnim ); + savefile->ReadBool( forcedOpen ); + savefile->ReadBool( bOpenForMonsters ); + savefile->ReadBool( noTouch ); + savefile->ReadInt( threadNum ); + savefile->ReadObject( reinterpret_cast ( doorTrigger ) ); + savefile->ReadString( airlockTeamName ); + savefile->ReadObject( reinterpret_cast ( airlockMaster ) ); + savefile->ReadBounds( crusherBounds ); + + activatedBy.Restore( savefile ); + + savefile->ReadBool( bOpen ); + savefile->ReadBool( bTransition ); + savefile->ReadFloat( airLockSndWait ); + + airlockTeam.SetOwner( this ); + if( airlockMaster && airlockMaster != this ) { + JoinAirLockTeam( airlockMaster ); + } + + nextAirLockSnd = 0; + finishedSpawn = true; + bShuttleDoors = spawnArgs.GetBool( "shuttle_doors" ); + + savefile->ReadClipModel( sndTrigger ); + savefile->ReadInt( nextSndTriggerTime ); +} + +//-------------------------------- +// hhModelDoor::Lock +//-------------------------------- +void hhModelDoor::Lock( int f ) { + locked = f; + float parmValue = GetDoorShaderParm( locked != 0, false ); + SetShaderParm( SHADERPARM_MODE, parmValue ); + SetBuddiesShaderParm( SHADERPARM_MODE, parmValue ); + + if (locked) { + CloseDoor(); + } +} + +//-------------------------------- +// hhModelDoor::CanOpen +//-------------------------------- +bool hhModelDoor::CanOpen() const { + if( IsLocked() ) { + return false; + } + + if( !GetAirLockMaster() ) { + return true; + } + + for( hhModelDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node != this && node->IsOpen() ) { + return false; + } + } + + return true; +} + +//-------------------------------- +// hhModelDoor::CanClose +//-------------------------------- +bool hhModelDoor::CanClose() const { + return true; +} + +//-------------------------------- +// hhModelDoor::OpenDoor +//-------------------------------- +void hhModelDoor::OpenDoor() { + if( IsClosed() && CanOpen() ) { + bTransition = true; + PostEventMS( &EV_ModelDoorOpeningBegin, 0 ); + } +} + +//-------------------------------- +// hhModelDoor::CloseDoor +//-------------------------------- +void hhModelDoor::CloseDoor() { + if( bOpen && !bTransition && CanClose() && !forcedOpen ) { + bTransition = true; + PostEventMS( &EV_ModelDoorClosingBegin, 0 ); + + // CJR: It's possible to open a door, then get back into it while it's animating closed, and then get squished + // the fix for this is to block the player from getting back in while it's closing. Projectiles will still pass through + GetPhysics()->SetContents( CONTENTS_PLAYERCLIP ); + } +} + +//-------------------------------- +// hhModelDoor::SetBlocking +//-------------------------------- +void hhModelDoor::SetBlocking( bool on ) { + GetPhysics()->SetContents( on ? CONTENTS_SOLID : 0 ); +} + +//-------------------------------- +// hhModelDoor::ClosePortal +//-------------------------------- +void hhModelDoor::ClosePortal( void ) { + if ( areaPortal ) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_VIEW ); + } +} + +//-------------------------------- +// hhModelDoor::OpenPortal +//-------------------------------- +void hhModelDoor::OpenPortal( void ) { + if ( areaPortal ) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_NONE ); + } +} + + +//-------------------------------- +// hhModelDoor::InformDone +//-------------------------------- +void hhModelDoor::InformDone() { + idThread::ObjectMoveDone( threadNum, this ); + threadNum = 0; +} + +//-------------------------------- +// hhModelDoor::Damage +//-------------------------------- +void hhModelDoor::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if (fl.takedamage) { + if ( !bOpen && painAnim ) { + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, painAnim, gameLocal.time, 500); + } + + hhAnimatedEntity::Damage(inflictor, attacker, dir, damageDefName, damageScale, location); + } +} + +//-------------------------------- +// hhModelDoor::Killed +//-------------------------------- +void hhModelDoor::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if (IsLocked()) { + Lock(0); + } + OpenDoor(); + fl.takedamage = false; + forcedOpen = true; +} + +//-------------------------------- +// hhModelDoor::SetBuddiesShaderParm +//-------------------------------- +void hhModelDoor::SetBuddiesShaderParm( int parm, float value ) { + idEntity* buddy = NULL; + + for( int ix = buddyNames.Num() - 1; ix >= 0; --ix ) { + if( !buddyNames[ix].Length() ) { + continue; + } + + buddy = gameLocal.FindEntity( buddyNames[ix].c_str() ); + if( !buddy ) { + continue; + } + + buddy->SetShaderParm( parm, value ); + } +} + +//-------------------------------- +// hhModelDoor::ToggleBuddiesShaderParm +//-------------------------------- +void hhModelDoor::ToggleBuddiesShaderParm( int parm, float firstValue, float secondValue, float toggleDelay ) { + SetBuddiesShaderParm( parm, firstValue ); + + CancelEvents( &EV_SetBuddiesShaderParm ); + PostEventSec( &EV_SetBuddiesShaderParm, toggleDelay, parm, secondValue ); +} + +//-------------------------------- +// hhModelDoor::DetermineTeamMaster +//-------------------------------- +idEntity* hhModelDoor::DetermineTeamMaster( const char* teamName ) { + idEntity* ent = NULL; + + if ( teamName && teamName[0] ) { + // find the first entity spawned on this team (which could be us) + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if (ent->IsType(hhModelDoor::Type) && !idStr::Icmp( static_cast(ent)->GetAirLockTeamName(), teamName )) { + return ent; + } + if (ent->IsType(hhDoor::Type) && !idStr::Icmp( static_cast(ent)->GetAirLockTeamName(), teamName )) { + return ent; + } + } + } + + return NULL; +} + +//-------------------------------- +// hhModelDoor::JoinAirLockTeam +//-------------------------------- +void hhModelDoor::JoinAirLockTeam( hhModelDoor *master ) { + assert( master ); + + airlockTeam.AddToEnd( master->airlockTeam ); +} + +//-------------------------------- +// hhModelDoor::VerifyAirlockTeamStatus +//-------------------------------- +void hhModelDoor::VerifyAirlockTeamStatus() { + if( !GetAirLockMaster() ) { + return; + } + + for( hhModelDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node != this && !node->IsClosed() ) { + gameLocal.Warning( "Airlock team '%s' has more than one member starting open", GetAirLockTeamName() ); + } + } +} + +//-------------------------------- +// hhModelDoor::StartOpen +//-------------------------------- +void hhModelDoor::StartOpen() { + VerifyAirlockTeamStatus(); + + bTransition = false; + bOpen = true; + OpenPortal(); + SetBlocking(false); + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, openAnim, gameLocal.time, 500); + Event_STATE_OpenBegin(); +} + +//-------------------------------- +// hhModelDoor::StartClosed +//-------------------------------- +void hhModelDoor::StartClosed() { + bTransition = false; + bOpen = false; + Event_STATE_ClosedBegin(); +} + +//-------------------------------- +// hhModelDoor::Event_SpawnNewDoorTrigger +// +// All of the parts of a door have been spawned, so create +// a trigger that encloses all of them +//-------------------------------- +void hhModelDoor::Event_SpawnNewDoorTrigger( void ) { + idBounds bounds,localbounds; + idBounds triggerBounds, soundTriggerBounds; + int i; + int best; + + // Since models bounds are overestimated, we need to use the bounds from the + // clipmodel, which was set before the over-estimation + localbounds = GetPhysics()->GetBounds(); + + // Save the original bounds in case we need to crush something + crusherBounds = GetPhysics()->GetAbsBounds(); + + // find the thinnest axis, which will be the one we expand + best = 0; + for ( i = 1 ; i < 3 ; i++ ) { + if ( localbounds[1][ i ] - localbounds[0][ i ] < localbounds[1][ best ] - localbounds[0][ best ] ) { + best = i; + } + } + triggerBounds = localbounds; + triggerBounds[1][ best ] += triggersize; + triggerBounds[0][ best ] -= triggersize; + + // Now transform into absolute coordintates + if ( GetPhysics()->GetAxis().IsRotated() ) { + bounds.FromTransformedBounds( triggerBounds, GetPhysics()->GetOrigin(), GetPhysics()->GetAxis() ); + } + else { + bounds[0] = triggerBounds[0] + GetPhysics()->GetOrigin(); + bounds[1] = triggerBounds[1] + GetPhysics()->GetOrigin(); + } + + // create a trigger with this size + idDict args; + args.Set( "mins", bounds[0].ToString(0) ); + args.Set( "maxs", bounds[1].ToString(0) ); + + doorTrigger = ( hhDoorTrigger * ) gameLocal.SpawnEntityType( hhDoorTrigger::Type, &args ); + doorTrigger->GetPhysics()->SetContents( CONTENTS_TRIGGER ); + doorTrigger->door = this; + + // Disable the trigger if it is no_touch and not start_open + if ( noTouch ) { + doorTrigger->Disable(); + } + + + // ------------------------ + // Create the sound trigger + // ------------------------ + + float soundTriggerSize = triggersize * 0.3f; + soundTriggerBounds = localbounds; + soundTriggerBounds[1][ best ] += soundTriggerSize; + soundTriggerBounds[0][ best ] -= soundTriggerSize; + + // Now transform into absolute coordintates + if ( GetPhysics()->GetAxis().IsRotated() ) { + bounds.FromTransformedBounds( soundTriggerBounds, GetPhysics()->GetOrigin(), GetPhysics()->GetAxis() ); + } + else { + bounds[0] = soundTriggerBounds[0] + GetPhysics()->GetOrigin(); + bounds[1] = soundTriggerBounds[1] + GetPhysics()->GetOrigin(); + } + bounds[0] -= GetPhysics()->GetOrigin(); + bounds[1] -= GetPhysics()->GetOrigin(); + + // create a trigger clip model + sndTrigger = new idClipModel( idTraceModel( bounds ) ); + sndTrigger->Link( gameLocal.clip, this, 254, GetPhysics()->GetOrigin(), mat3_identity ); + sndTrigger->SetContents( CONTENTS_TRIGGER ); + + // HACK: Also, since all the parts are now spawned, update the buddies' shaderparms + if (locked) { + float parmValue = GetDoorShaderParm( locked != 0, true ); + for ( i = 0; i < buddyNames.Num(); i++ ) { + if ( buddyNames[ i ].Length() ) { + idEntity *buddy = gameLocal.FindEntity( buddyNames[ i ] ); + if( buddy ) { + buddy->SetShaderParm(SHADERPARM_MODE, parmValue); + } + } + } + } + +} + +//-------------------------------- +// hhModelDoor::ToggleDoorState +//-------------------------------- +void hhModelDoor::ToggleDoorState( void ) { + if ( bOpen ) { + CloseDoor(); + } + else { + OpenDoor(); + } +} + +//-------------------------------- +// hhModelDoor::ForceAirLockTeamClosed +//-------------------------------- +void hhModelDoor::ForceAirLockTeamClosed() { + if( !GetAirLockMaster() ) { + return; + } + + ToggleBuddiesShaderParm( SHADERPARM_DIVERSITY, 1.0f, 0.0f, 2.0f ); + + for( hhModelDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node != this && !node->IsClosed() ) { + node->CloseDoor(); + } + } +} + +//-------------------------------- +// hhModelDoor::ForceAirLockTeamOpen +//-------------------------------- +void hhModelDoor::ForceAirLockTeamOpen() { + if( !GetAirLockMaster() ) { + return; + } + + for( hhModelDoor* node = GetAirLockMaster()->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + if( node != this && !node->IsOpen() ) { + node->OpenDoor(); + } + } +} + +// State code +void hhModelDoor::Event_STATE_ClosedBegin() { + + // Alert any entities squished in the door + idEntity *touch[ MAX_GENTITIES ]; + int contentsMask = (CONTENTS_SOLID|CONTENTS_BODY|CONTENTS_CORPSE|CONTENTS_TRIGGER); // corpses, moveableitems, spiritplayers, moveables + int num = hhUtils::EntitiesTouchingClipmodel(GetPhysics()->GetClipModel(), touch, MAX_GENTITIES, contentsMask); + for (int ix=0; ix 0.0f ) { + // HUMANHEAD mdl + // Throw spirit players back to their bodies if they get caught in a closing door + // Don't check this for MP, the damage below is enough to send them back + if ( !gameLocal.isMultiplayer && ent->IsType( hhPlayer::Type ) ) { + hhPlayer *player = reinterpret_cast< hhPlayer * > ( ent ); + if ( player->IsSpiritWalking() ) { + player->StopSpiritWalk(); + continue; + } + } + + if( ent->fl.takedamage ) { + ent->Damage( this, this, vec3_origin, "damage_doorbonk", damage, INVALID_JOINT ); + } + } + + ent->SquishedByDoor(this); + } + } + + SetBlocking(true); + ClosePortal(); + InformDone(); + + if ( idleAnim ) { + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, idleAnim, gameLocal.time, 500 ); + } + + // If we are noTouch, then disable the trigger. (Could have been enabled by GUI activation) + if ( noTouch && doorTrigger ) { + doorTrigger->Disable(); + } + + // Trigger entities if we have finished spawning + if ( !finishedSpawn ) { + return; + } + + ActivatePrefixed("triggerClosed", GetActivator()); + + if( GetAirLockMaster() ) { + // Mark the other team members as locked + for( hhModelDoor* node = airlockMaster->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + + // Skip this door and it's buddies + if( node == this || buddyNames.Find( node->name ) ) { + continue; + } + + // Don't change doors that are really locked + if( node->IsLocked() ) { + continue; + } + + // Mark as locked + node->SetBuddiesShaderParm( SHADERPARM_MISC, 0.0f ); + } + } + + HH_ASSERT( bOpen == true && bTransition == true ); + bTransition = false; + bOpen = false; +} + +void hhModelDoor::Event_STATE_OpeningBegin() { + SetBlocking(false); + OpenPortal(); + StartSound( "snd_open", SND_CHANNEL_ANY ); + + // Fire any triggerStartOpen entities + ActivatePrefixed("triggerStartOpen", GetActivator()); + + idEntity *ent; + idEntity *next; + for( ent = teamChain; ent != NULL; ent = next ) { + next = ent->GetTeamChain(); + if ( ent && ent->IsType( hhProjectile::Type ) ) { + ent->Unbind(); // bjk drops all bound projectiles such as arrows and mines + next = teamChain; + if (ent->IsType(hhProjectileSpiritArrow::Type)) { + ent->PostEventMS(&EV_Remove, 0); + } + //HUMANHEAD bjk PCF (4-28-06) - explode crawlers + if (ent->IsType(hhProjectileStickyCrawlerGrenade::Type)) { + static_cast(ent)->PostEventSec( &EV_Explode, 0.2f ); + } + } + } + + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, openAnim, gameLocal.time, 500); + int opentime = (openAnim) ? GetAnimator()->GetAnim( openAnim )->Length() : 0; + PostEventMS( &EV_ModelDoorOpenBegin, opentime ); + + if( airlockMaster ) { + // Mark the other team members as locked + for( hhModelDoor* node = airlockMaster->airlockTeam.ListHead()->Owner(); node != NULL; node = node->airlockTeam.Next() ) { + + // Skip this door and it's buddies + if( node == this || buddyNames.Find( node->name ) ) { + continue; + } + + // Don't change doors that are really locked + if( node->IsLocked() ) { + continue; + } + + // Mark as locked + node->SetBuddiesShaderParm( SHADERPARM_MISC, 1.0f ); + } + + // This prevents the our shader from being set as locked from the player moving too quickly between airlock doors + SetBuddiesShaderParm( SHADERPARM_MISC, 0.0f ); + } + + HH_ASSERT( bOpen == false && bTransition == true ); +} + +void hhModelDoor::Event_STATE_OpenBegin() { + InformDone(); + + // If we are no touch, then we were opened by something else, so let us care about the player being in the trigger/enable the trigger + if ( noTouch && doorTrigger ) { + doorTrigger->Enable(); + } + + // Trigger entities if we have finished spawning + if ( !finishedSpawn ) { + return; + } + + ActivatePrefixed("triggerOpened", GetActivator()); + + WakeTouchingEntities(); + + HH_ASSERT( bOpen == false && bTransition == true ); + bOpen = true; + bTransition = false; +} + +void hhModelDoor::Event_STATE_ClosingBegin() { + StartSound("snd_close", SND_CHANNEL_ANY); + + //GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + //FIXME: Despite the blend in/out here, it still pops when the idle is reapplied + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, closeAnim, gameLocal.time, 500); + int closetime = (closeAnim) ? GetAnimator()->GetAnim( closeAnim )->Length() : 0; + PostEventMS( &EV_ModelDoorClosedBegin, closetime ); + + HH_ASSERT( bOpen == true && bTransition == true ); +} + +// ------------------------------------------------------------- +// Non-state code +// ------------------------------------------------------------- + +//-------------------------------- +// hhModelDoor::Event_Blocked +//-------------------------------- +void hhModelDoor::Event_TeamBlocked( idEntity *blockedEntity, idEntity *blockingEntity ) { + // reverse direction + ToggleDoorState(); +} + +//-------------------------------- +// hhModelDoor::Event_PartBlocked +//-------------------------------- +void hhModelDoor::Event_PartBlocked( idEntity *blockingEntity ) { + if ( damage > 0.0f ) { + blockingEntity->Damage( this, this, vec3_origin, "damage_doorbonk", 1.0f, INVALID_JOINT ); + } +} + +//-------------------------------- +// hhModelDoor::Event_Activate +//-------------------------------- +void hhModelDoor::Event_Activate( idEntity *activator ) { + idEntity *buddy = NULL; + + if ( IsLocked() ) { + int oldLocked = locked; + Lock(0); + if ( oldLocked == 2 ) { + return; + } + + ToggleDoorState(); + CancelEvents( &EV_ModelDoorClose ); + PostEventMS( &EV_ModelDoorClose, wait * 1000 ); + } + else { + TryOpen( activator ); + } + +} + + +//-------------------------------- +// hhModelDoor::WakeTouchingEntities +//-------------------------------- +void hhModelDoor::WakeTouchingEntities() { + idEntity *touch[ MAX_GENTITIES ]; + idEntity *ent; + int num; + + num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), MASK_ALL, touch, MAX_GENTITIES ); + for ( int i = 0; i < num; i++ ) { + ent = touch[ i ]; + if ( !ent || ent == this ) { + continue; + } + ent->ActivatePhysics(this); + } +} + +//-------------------------------- +// hhModelDoor::TryOpen +// If conditions are right, open. Otherwise don't open +//-------------------------------- +void hhModelDoor::TryOpen( idEntity *whoTrying ) { + + activatedBy = whoTrying; // Should thie be above OpenDoor + + // Only allow the player to affect airlock doors + if ( GetAirLockMaster() && !whoTrying->IsType( idPlayer::Type ) ) { + return; + } + + // Check if this actor is valid to open this door (if the player is spiritwalking) - cjr + if ( !whoTrying->ShouldTouchTrigger( this ) ) { + return; + } + + // If we're shuttle doors, make sure we only open for actors in vehicles. + if ( bShuttleDoors && ( ! whoTrying->IsType( idActor::Type ) || ! reinterpret_cast ( whoTrying )->InVehicle() ) ) { + return; + } + + if ( !bOpenForMonsters && whoTrying->IsType( hhMonsterAI::Type ) ) { + return; + } + + // Delay Close event since we're still in the doorway + CancelEvents( &EV_ModelDoorClose ); + PostEventMS( &EV_ModelDoorClose, wait * 1000 ); + + //This feels like ahack but I need to get this done + if( IsClosed() && !CanOpen() && GetAirLockMaster() ) { + ForceAirLockTeamClosed(); + if( gameLocal.time > nextAirLockSnd ) { + int length; + StartSound( "snd_airlock", SND_CHANNEL_BODY, 0, false, &length ); + nextAirLockSnd = gameLocal.time + length + airLockSndWait; + } + } + OpenDoor(); +} + + +//-------------------------------- +// hhModelDoor::EntitiesInTrigger +//-------------------------------- +bool hhModelDoor::EntitiesInTrigger() { + idEntity * ents[ MAX_GENTITIES ]; + idEntity * ent; + int num; + + if ( !doorTrigger ) { + return( false ); + } + + num = doorTrigger->GetEntitiesWithin( ents, MAX_GENTITIES ); + for ( int i = 0; i < num; ++i ) { + ent = ents[ i ]; + + if ( ent->fl.touchTriggers && ent->ShouldTouchTrigger( this ) ) { + + if ( !bOpenForMonsters && ent->IsType( hhMonsterAI::Type ) ) { + continue; + } + + if ( bShuttleDoors && ( ! ent->IsType( idActor::Type ) || ! reinterpret_cast ( ent )->InVehicle() ) ) { + continue; + } + + if ( !GetAirLockMaster() || ent->IsType( idPlayer::Type ) ) { // Only allow the player to affect airlock doors + return( true ); + } + } + } + + return( false ); +} + + +//-------------------------------- +// hhModelDoor::Event_Touch +//-------------------------------- +void hhModelDoor::Event_Touch( idEntity *other, trace_t* trace ) { + + if ( sndTrigger && trace->c.id == sndTrigger->GetId() ) { + if (other && other->IsType(hhPlayer::Type) && IsLocked() && gameLocal.time > nextSndTriggerTime) { + StartSound("snd_locked", SND_CHANNEL_ANY, 0, false, NULL ); + nextSndTriggerTime = gameLocal.time + 10000; + } + return; + } + + // Skip if locked, or noTouch + if( IsLocked() || noTouch ) { + // play locked sound + return; + } + + TryOpen( other ); +} + + +/* +================ +idModelDoor::GetActivator +================ +*/ +idEntity *hhModelDoor::GetActivator( void ) const { + return activatedBy.GetEntity(); +} + +/* +================ +hhModelDoor::ClientPredictionThink +================ +*/ +void hhModelDoor::ClientPredictionThink( void ) { + hhAnimatedEntity::ClientPredictionThink(); +} + +/* +================ +hhModelDoor::WriteToSnapshot +================ +*/ +void hhModelDoor::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhAnimatedEntity::WriteToSnapshot(msg); + hhAnimator *animator = (hhAnimator *)GetAnimator(); + const idAnimBlend *anim = animator->CurrentAnim(ANIMCHANNEL_ALL); + if (anim) { + msg.WriteBits(1, 1); + msg.WriteBits(anim->AnimNum(), 32); + } + else { + msg.WriteBits(0, 1); + } + + msg.WriteBits(GetPhysics()->GetContents(), 32); +} + +/* +================ +hhModelDoor::ReadFromSnapshot +================ +*/ +void hhModelDoor::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhAnimatedEntity::ReadFromSnapshot(msg); + + bool hasAnim = !!msg.ReadBits(1); + if (hasAnim) { + int animNum = msg.ReadBits(32); + hhAnimator *animator = (hhAnimator *)GetAnimator(); + const idAnimBlend *anim = animator->CurrentAnim(ANIMCHANNEL_ALL); + if (!anim || anim->AnimNum() != animNum) { + animator->PlayAnim(ANIMCHANNEL_ALL, animNum, gameLocal.time, 500); + } + } + + int contents = msg.ReadBits(32); + if (contents != GetPhysics()->GetContents()) { + GetPhysics()->SetContents(contents); + } +} + + +//-------------------------------- +// hhModelDoor::Event_ToggleDoorState +//-------------------------------- +void hhModelDoor::Event_ToggleDoorState( void ) { + ToggleDoorState(); +} + +//-------------------------------- +// hhModelDoor::Event_OpenDoor +//-------------------------------- +void hhModelDoor::Event_OpenDoor() { + OpenDoor(); +} + +//-------------------------------- +// hhModelDoor::Event_CloseDoor +//-------------------------------- +void hhModelDoor::Event_CloseDoor() { + + // Added due to the fact that monsters which don't move, also don't ActivateTriggers. + // So now we check before we close if we can actually close + + // We can't actually close if are transitioning or if we have someone in our trigger that we care about + if ( bTransition || EntitiesInTrigger() ) { + // So if someone here, cancel any further events + CancelEvents( &EV_ModelDoorClose ); + // And try again in a bit. + PostEventMS( &EV_ModelDoorClose, wait * 1000 ); + return; + } + + CloseDoor(); +} + +//-------------------------------- +// hhModelDoor::Event_SetCallback +//-------------------------------- +void hhModelDoor::Event_SetCallback( void ) { + if ( !threadNum && bTransition ) { + threadNum = idThread::CurrentThreadNum(); + idThread::ReturnInt( true ); + } else { + idThread::ReturnInt( false ); + } +} + +//-------------------------------- +// hhModelDoor::Event_SetBuddiesShaderParm +//-------------------------------- +void hhModelDoor::Event_SetBuddiesShaderParm( int parm, float value ) { + SetBuddiesShaderParm( parm, value ); +} diff --git a/src/Prey/game_modeldoor.h b/src/Prey/game_modeldoor.h new file mode 100644 index 0000000..b259cc8 --- /dev/null +++ b/src/Prey/game_modeldoor.h @@ -0,0 +1,139 @@ +#ifndef __GAME_MODELDOOR_H__ +#define __GAME_MODELDOOR_H__ + +extern const idEventDef EV_ModelDoorOpen; +extern const idEventDef EV_ModelDoorClose; + +extern const idEventDef EV_SetBuddiesShaderParm; + +class hhDoorTrigger; // Stupid C++ forward decl for hhModelDoor + +//-------------------------------- +// hhModelDoor +//-------------------------------- +class hhModelDoor : public hhAnimatedEntity { + +public: + CLASS_PROTOTYPE( hhModelDoor ); + + hhModelDoor(); + virtual ~hhModelDoor(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + // Overridden methods + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void ClientPredictionThink( void ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + void ToggleDoorState( void ); + void OpenDoor(); + void CloseDoor(); + void Lock( int f ); + bool IsLocked() const { return locked != 0; } + ID_INLINE bool IsOpen() const { return ( bOpen || bTransition ); } + ID_INLINE bool IsClosed() const { return ( !bOpen && !bTransition ); } + bool CanOpen() const; + bool CanClose() const; + void ForceAirLockTeamClosed(); + void ForceAirLockTeamOpen(); + const char* GetAirLockTeamName() const { return airlockTeamName.c_str(); } + hhModelDoor* GetAirLockMaster() const { return airlockMaster; } + idEntity * GetActivator() const; + + idLinkList airlockTeam; + +protected: + void SetBlocking( bool on ); + void ClosePortal( void ); + void OpenPortal( void ); + void InformDone(); + + void SetBuddiesShaderParm( int parm, float value ); + void ToggleBuddiesShaderParm( int parm, float firstValue, float secondValue, float toggleDelay ); + void WakeTouchingEntities(); + + idEntity* DetermineTeamMaster( const char* teamName ); + void JoinAirLockTeam( hhModelDoor *master ); + void VerifyAirlockTeamStatus(); + + void StartOpen(); + void StartClosed(); + void TryOpen( idEntity *whoTrying ); + bool EntitiesInTrigger(); + + void Event_TeamBlocked( idEntity *blockedEntity, idEntity *blockingEntity ); + void Event_PartBlocked( idEntity *blockingEntity ); + void Event_SpawnNewDoorTrigger( void ); + void Event_SetCallback( void ); + void Event_SetBuddiesShaderParm( int parm, float value ); + void Event_Touch( idEntity *other, trace_t* trace ); + void Event_Activate( idEntity *activator ); + void Event_ToggleDoorState( void ); + void Event_OpenDoor(); + void Event_CloseDoor(); + void Event_STATE_ClosedBegin(); + void Event_STATE_OpeningBegin(); + void Event_STATE_OpenBegin(); + void Event_STATE_ClosingBegin(); + +protected: + float damage; + float wait; + float triggersize; + qhandle_t areaPortal; // 0 = no portal + idList buddyNames; + int locked; + int openAnim; + int closeAnim; + int idleAnim; + int painAnim; + bool forcedOpen; // Is the door forced open + bool bOpenForMonsters; // Door opens for monsters + bool noTouch; // Can you touch this door. + bool bOpen; // HUMANHEAD mdl: True if door is open + bool bTransition; // HUMANHEAD mdl: True if door is in transition between opening and closing + bool bShuttleDoors; + int threadNum; // Thread used for sys.WaitFor() calls + hhDoorTrigger * doorTrigger; + idClipModel * sndTrigger; + float airLockSndWait; // Time in milliseconds between airlock sounds from spawnarg airlockwait + float nextAirLockSnd; // Next time to play an airlock locked door sound + int nextSndTriggerTime; // next time to play door locked sound + + idStr airlockTeamName; + hhModelDoor *airlockMaster; + + idBounds crusherBounds; + + idEntityPtr activatedBy; + + bool finishedSpawn; // Used to determine if we are still spawning in or not. + +}; + +class hhDoorTrigger : public idEntity { + CLASS_PROTOTYPE( hhDoorTrigger ); + +public: + void Disable() { enabled = false; } + void Enable() { enabled = true; } + bool IsEnabled() { return( enabled ); } + int GetEntitiesWithin( idEntity **ents, int entsLength ); + + hhModelDoor *door; + bool enabled; + +protected: + hhDoorTrigger(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void Event_TriggerDoor( idEntity *other, trace_t *trace ); +}; + +#endif /* __GAME_MODELDOOR_H__ */ diff --git a/src/Prey/game_modeltoggle.cpp b/src/Prey/game_modeltoggle.cpp new file mode 100644 index 0000000..4e8937e --- /dev/null +++ b/src/Prey/game_modeltoggle.cpp @@ -0,0 +1,316 @@ +/*********************************************************************** + hhModelToggle + + Usage: + Set target to a hhViewedModel entity that will change model. + + operations: + next/prev model + next/prev anim + toggle rotation + toggle translation + toggle cycle + +***********************************************************************/ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +// hhViewedModel ----------------------------------------------------------- + +CLASS_DECLARATION(hhAnimatedEntity, hhViewedModel) +END_CLASS + +void hhViewedModel::Spawn(void) { + + physicsObj.SetSelf(this); + physicsObj.SetOrigin( GetPhysics()->GetOrigin() ); + physicsObj.SetAxis( GetPhysics()->GetAxis() ); + SetPhysics( &physicsObj ); + + rotationAmount = 0; + BecomeActive(TH_THINK|TH_TICKER); +} + +void hhViewedModel::SetRotationAmount(float amount) { + rotationAmount = amount; +} + +void hhViewedModel::Ticker() { + idAngles ang; + + // update rotation + physicsObj.GetAngles( ang ); + physicsObj.SetAngularExtrapolation( extrapolation_t(EXTRAPOLATION_LINEAR|EXTRAPOLATION_NOSTOP), gameLocal.time, gameLocal.msec, ang, idAngles( 0, rotationAmount * 360.0f / 60.0f, 0 ), ang_zero ); + + // update visuals so that the skeleton is drawn for non-rotating models + UpdateVisuals(); +} + + +// hhModelToggle ------------------------------------------------------------ + +const idEventDef EV_SetInitialAnims("", NULL); +const idEventDef EV_RequireTargets("", NULL); + +CLASS_DECLARATION(hhConsole, hhModelToggle) + EVENT( EV_SetInitialAnims, hhModelToggle::Event_SetInitialAnims ) + EVENT( EV_RequireTargets, hhModelToggle::Event_RequireTargets ) +END_CLASS + + +void hhModelToggle::Spawn(void) { + const idKeyValue *arg; + int i, num; + + bTranslation = false; + bRotation = false; + bCycle = true; + + // Retrieve list of definitions + defList.Clear(); + num = spawnArgs.GetNumKeyVals(); + for( i = 0; i < num; i++ ) { + arg = spawnArgs.GetKeyVal( i ); + if ( !arg->GetKey().Icmpn( "defentity", 9 ) ) { + defList.Append(arg->GetValue()); + } + } + + FillResourceList(); + BecomeActive(TH_THINK); + + PostEventMS(&EV_RequireTargets, 0); // Target list not built until after Spawn() + PostEventMS(&EV_SetInitialAnims, 0); // Can't set anims in Spawn() +} + +void hhModelToggle::FillResourceList() { + const idDict *dict; + ResourceSet set; + int i; + const idKeyValue *kv; + + for (i=0; iFindKey("model_view"); + if (!kv) { + kv = dict->FindKey("model"); + if (!kv) { + gameLocal.Warning("No model for %s", defList[i].c_str()); + continue; + } + } + + set.model = kv->GetValue(); + set.animList.Clear(); + set.animFileList.Clear(); + set.args = dict; + + const idDeclModelDef *modelDef = static_cast( declManager->FindType( DECL_MODELDEF, kv->GetValue(), false ) ); + if (modelDef) { + int num = modelDef->NumAnims(); + for (int i=1; iGetAnim(i); + idStr animName = anim->FullName(); + set.animList.Append(animName); + idStr animFileName = anim->MD5Anim(0)->Name(); + set.animFileList.Append(animFileName); + } + } + else { + kv = NULL; + while (1) { + kv = dict->MatchPrefix("anim ", kv); + if (!kv) { + break; + } + idStr animName = kv->GetKey().c_str() + 5; + set.animList.Append(animName); + set.animFileList.Append(kv->GetValue()); + } + } + + if (set.animList.Num() == 0) { + gameLocal.Warning("No anims for %s", defList[i].c_str()); + continue; + } + + resources.Append(set); + } +} + +void hhModelToggle::UpdateGUIValues() { + if (renderEntity.gui[0]) { + renderEntity.gui[0]->SetStateString("modelname", resources[currentModel].model.c_str()); + if (resources[currentModel].animList.Num() > 0) { + renderEntity.gui[0]->SetStateString("animname", resources[currentModel].animList[currentAnim].c_str()); + renderEntity.gui[0]->SetStateString("animfile", resources[currentModel].animFileList[currentAnim].c_str()); + } + renderEntity.gui[0]->SetStateInt("translation", bTranslation); + renderEntity.gui[0]->SetStateInt("rotation", bRotation); + renderEntity.gui[0]->SetStateInt("cycle", bCycle); + } +} + +void hhModelToggle::SetTargetsToModel(const char *modelname) { + for (int t=0; tSetModel(modelname); + } + } + + UpdateGUIValues(); +} + +void hhModelToggle::SetTargetsToAnim(const char *animname) { + for (int t=0; tGetAnimator()->GetAnim(animname); + + ent->GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + + if (bCycle) { + ent->GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, anim, gameLocal.time, 0); + } + else { + ent->GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, anim, gameLocal.time, 0); + } + + ent->GetAnimator()->RemoveOriginOffset(!bTranslation); + } + } + + UpdateGUIValues(); +} + +void hhModelToggle::SetTargetsToRotate(bool bRotate) { + for (int t=0; tIsType(hhViewedModel::Type)) { + static_cast(ent)->SetRotationAmount(bRotate?5:0); + } + } + } + + UpdateGUIValues(); +} + + +void hhModelToggle::NextModel(void) { + if (resources.Num() > 0) { + currentModel = (currentModel + 1) % resources.Num(); + currentAnim = 0; + + SetTargetsToModel(resources[currentModel].model.c_str()); + SetTargetsToAnim(resources[currentModel].animList[currentAnim].c_str()); + } +} + +void hhModelToggle::PrevModel(void) { + if (resources.Num() > 0) { + currentModel = (currentModel - 1 + resources.Num()) % resources.Num(); + currentAnim = 0; + + SetTargetsToModel(resources[currentModel].model.c_str()); + SetTargetsToAnim(resources[currentModel].animList[currentAnim].c_str()); + } +} + +void hhModelToggle::NextAnim(void) { + int numanims = resources[currentModel].animList.Num(); + if (numanims > 0) { + currentAnim = (currentAnim + 1) % numanims; + SetTargetsToAnim(resources[currentModel].animList[currentAnim].c_str()); + } +} + +void hhModelToggle::PrevAnim(void) { + int numanims = resources[currentModel].animList.Num(); + if (numanims > 0) { + currentAnim = (currentAnim - 1 + numanims) % numanims; + SetTargetsToAnim(resources[currentModel].animList[currentAnim].c_str()); + } +} + +bool hhModelToggle::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + + idToken token; + + if (!src->ReadToken(&token)) { + return false; + } + + if (token == ";") { + return false; + } + + if (token.Icmp("nextmodel") == 0) { + NextModel(); + return true; + } + else if (token.Icmp("prevmodel") == 0) { + PrevModel(); + return true; + } + else if (token.Icmp("nextanim") == 0) { + NextAnim(); + return true; + } + else if (token.Icmp("prevanim") == 0) { + PrevAnim(); + return true; + } + else if (token.Icmp("togglerotation") == 0) { + // Instead, have gui call a script function and handle rotation with a mover + bRotation ^= 1; + SetTargetsToRotate(bRotation); + return true; + } + else if (token.Icmp("toggletranslation") == 0) { + bTranslation ^= 1; + SetTargetsToModel(resources[currentModel].model.c_str()); + SetTargetsToAnim(resources[currentModel].animList[currentAnim].c_str()); + return true; + } + else if (token.Icmp("togglecycle") == 0) { + bCycle ^= 1; + SetTargetsToModel(resources[currentModel].model.c_str()); + SetTargetsToAnim(resources[currentModel].animList[currentAnim].c_str()); + return true; + } + + src->UnreadToken(&token); + return false; +} + +void hhModelToggle::Event_SetInitialAnims() { + // Set to currentModel, currentAnim + currentModel = currentAnim = 0; + if (resources.Num() > 0) { + SetTargetsToModel(resources[currentModel].model.c_str()); + if (resources[currentModel].animList.Num() > 0) { + SetTargetsToAnim(resources[currentModel].animList[currentAnim].c_str()); + } + } +} + +void hhModelToggle::Event_RequireTargets() { + // Require a valid target: must be delayed after spawn because targets aren't build until then + if (!targets.Num()) { + gameLocal.Error( "ModelViewer requires a valid target." ); + PostEventMS(&EV_Remove, 0); + } +} + diff --git a/src/Prey/game_modeltoggle.h b/src/Prey/game_modeltoggle.h new file mode 100644 index 0000000..b20ccc1 --- /dev/null +++ b/src/Prey/game_modeltoggle.h @@ -0,0 +1,62 @@ +#ifndef __GAME_MODELTOGGLE_H__ +#define __GAME_MODELTOGGLE_H__ + +typedef struct ResourceSet_s{ + idStr model; + idList animList; + idList animFileList; + const idDict * args; +} ResourceSet; + + +class hhViewedModel : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhViewedModel ); + + void Spawn(); + void SetRotationAmount(float amount); + + idPhysics_Parametric physicsObj; + float rotationAmount; + +protected: + virtual void Ticker(); +}; + + +class hhModelToggle : public hhConsole { +public: + CLASS_PROTOTYPE( hhModelToggle ); + + void Spawn( void ); + virtual bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + +protected: + void NextModel(void); + void PrevModel(void); + void NextAnim(void); + void PrevAnim(void); + void SetTargetsToModel(const char *modelname); + void SetTargetsToAnim(const char *animname); + void FillResourceList(); + void UpdateGUIValues(); + void Update(); + void SetTargetsToRotate(bool bRotate); + + void Event_SetInitialAnims(); + void Event_RequireTargets(); + +protected: + idList defList; + idList resources; + + int currentModel; + int currentAnim; + bool bTranslation; + bool bRotation; + bool bCycle; + +}; + + +#endif /* __GAME_MODELTOGGLE_H__ */ diff --git a/src/Prey/game_monster_ai.cpp b/src/Prey/game_monster_ai.cpp new file mode 100644 index 0000000..2d8ed3a --- /dev/null +++ b/src/Prey/game_monster_ai.cpp @@ -0,0 +1,2156 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//----------------------------------------------------- +// hhNoClipEnt +//----------------------------------------------------- +CLASS_DECLARATION(idEntity, hhNoClipEnt) +END_CLASS + +void hhNoClipEnt::Spawn( void ) { + if(GetPhysics()) + GetPhysics()->SetContents(0); +} + +//----------------------------------------------------- +// hhAINode +//----------------------------------------------------- +CLASS_DECLARATION( idEntity, hhAINode ) +END_CLASS + +hhAINode::hhAINode( void ) { + user.Clear(); +} + +void hhAINode::Save( idSaveGame *savefile ) const { + user.Save( savefile ); +} + +void hhAINode::Restore( idRestoreGame *savefile ) { + user.Restore( savefile ); +} + +//----------------------------------------------------- +// hhMonsterAI +//----------------------------------------------------- + +idList hhMonsterAI::allSimpleMonsters; + +hhMonsterAI::hhMonsterAI() { + lookOffset = ang_zero; + shootTarget = NULL; + bBossBar = false; + spawnThinkFlags = 0; + lastContactTime = gameLocal.time; + nextSpeechTime = gameLocal.time; + frozen = false; + soundOnModel = false; +} + +hhMonsterAI::~hhMonsterAI() { + allSimpleMonsters.Remove(this); + + if ( bBossBar && spawnArgs.GetBool( "remove_bar_on_dissolve" ) ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( player && player->hud ) { + bBossBar = false; + player->hud->HandleNamedEvent("HideProgressBar"); + player->hud->StateChanged(gameLocal.time); + } + } +} + +void hhMonsterAI::Spawn() { + AI_HAS_RANGE_ATTACK = spawnArgs.GetBool("has_range_attack", "0"); + AI_HAS_MELEE_ATTACK = spawnArgs.GetBool("has_melee_attack", "0"); + + targetReaction.reactionIndex = -1; + targetReaction.entity = NULL; + + allSimpleMonsters.AddUnique(this); + bBindAxis = spawnArgs.GetBool( "bind_axis" ); + bCanFall = spawnArgs.GetBool( "can_fall", "0" ); + bSeeThroughPortals = spawnArgs.GetBool( "can_see_portals", "0" ); + bBindOrient = spawnArgs.GetBool( "bind_orient", "0" ); + hearingRange = spawnArgs.GetInt( "hearing_range", "1024" ); + bCanWallwalk = spawnArgs.GetBool( "can_wallwalk", "0" ); + fallDelay = spawnArgs.GetInt( "fall_delay" ); + bOverrideKilledByGravityZones = spawnArgs.GetBool( "overrideKilledByGravityZones" ); + bNeverTarget = spawnArgs.GetBool( "never_target", "0" ); + bNoCombat = spawnArgs.GetBool( "no_combat", "0" ); + const char *temp; + if ( spawnArgs.GetString( "fx_custom_blood", "", &temp ) ) { + bCustomBlood = true; + } else { + bCustomBlood = false; + } + + nextSpiritProxyCheck = 0; + nextTurnUpdate = 0; + frozen = false; + + //handle initial rotation and sticking on wallwalk + if ( !IsHidden() ) { + PostEventSec( &MA_InitialWallwalk, 0.1f ); + PostEventMS(&MA_EnemyOnSpawn, 10); + } + PostEventMS(&EV_PostSpawn, 0); + if ( spawnArgs.GetInt( "wander_radius" ) ) { + spawnOrigin = GetOrigin(); + } + + // CJR: Clear the DDA values used for tracking damage inflicted on the player + totalDDADamage = 0; +} + +void hhMonsterAI::Event_PostSpawn() { + CreateHealthTriggers(); + + //TODO: Move to character/girlfriend code when it exists + // Spawn earrings for girlfriend + idEntity *ent; + const char *defName = spawnArgs.GetString("def_earring", NULL); + if (defName && defName[0] && head.IsValid()) { + const char *boneNameL = spawnArgs.GetString("earringboneL"); + const char *boneNameR = spawnArgs.GetString("earringboneR"); + idDict args; + args.Clear(); + args.Set( "origin", GetPhysics()->GetOrigin().ToString() ); + + ent = gameLocal.SpawnObject(defName, &args); + if (ent) { + ent->MoveToJoint(head.GetEntity(), boneNameL); + ent->BindToJoint(head.GetEntity(), boneNameL, false); + } + + ent = gameLocal.SpawnObject(defName, &args); + if (ent) { + ent->MoveToJoint(head.GetEntity(), boneNameR); + ent->BindToJoint(head.GetEntity(), boneNameR, false); + } + } +} + + +bool hhMonsterAI::HasPathTo(const idVec3 &destPt) { + if(!GetAAS()) // || !allowAAS) //TODO: support the allowAAS functionality + return FALSE; + + idVec3 fromPos = GetPhysics()->GetOrigin(); + + int currAreaNum = PointReachableAreaNum(fromPos); + int toAreaNum = PointReachableAreaNum(destPt); + + + // Invalid #'s ? + if(toAreaNum == 0 || currAreaNum == 0) + return FALSE; + + aasPath_t path; + return PathToGoal(path, currAreaNum, fromPos, toAreaNum, destPt); +} + +/* +============================== +hhMonsterAI::LinkScriptVariables(void) +============================== +*/ +#define LinkScriptVariable( name ) name.LinkTo( scriptObject, #name ) +void hhMonsterAI::LinkScriptVariables(void) { + + idAI::LinkScriptVariables(); + + LinkScriptVariable( AI_HAS_RANGE_ATTACK ); // required for reaction system + LinkScriptVariable( AI_HAS_MELEE_ATTACK ); // required for reaction system + LinkScriptVariable( AI_USING_REACTION ); // TRUE if monster is currently using reaction + LinkScriptVariable( AI_REACTION_FAILED ); // TRUE if the last reaction attempt failed (path blocked, exclusive problem, etc) + LinkScriptVariable( AI_REACTION_ANIM ); + LinkScriptVariable( AI_BACKWARD ); + LinkScriptVariable( AI_STRAFE_LEFT ); + LinkScriptVariable( AI_STRAFE_RIGHT ); + LinkScriptVariable( AI_UPWARD ); + LinkScriptVariable( AI_DOWNWARD ); + LinkScriptVariable( AI_SHUTTLE_DOCKED ); + LinkScriptVariable( AI_VEHICLE_ATTACK ); + LinkScriptVariable( AI_VEHICLE_ALT_ATTACK ); + LinkScriptVariable( AI_WALLWALK ); + LinkScriptVariable( AI_FALLING ); + LinkScriptVariable( AI_PATHING ); + LinkScriptVariable( AI_TURN_DIR ); + LinkScriptVariable( AI_FLY_NO_SEEK ); + LinkScriptVariable( AI_FOLLOWING_PATH ); +} + +void hhMonsterAI::Think( void ) { + PROFILE_SCOPE("AI", PROFMASK_NORMAL|PROFMASK_AI); + if (ai_skipThink.GetBool()) { //HUMANHEAD rww + return; + } + + idVec3 oldOrigin = physicsObj.GetOrigin(); + idVec3 oldVelocity = physicsObj.GetLinearVelocity(); + + if ( thinkFlags & TH_THINK ) { + + // Update vehicle guns + if(AI_VEHICLE && InVehicle()) { + usercmd_t cmds; + const signed char speed = 64; // In range [0..127] + memset( &cmds, 0, sizeof(usercmd_t) ); + cmds.buttons |= (AI_VEHICLE_ATTACK) ? BUTTON_ATTACK : 0; + cmds.buttons |= (AI_VEHICLE_ALT_ATTACK) ? BUTTON_ATTACK_ALT : 0; + + if(AI_FORWARD) { + cmds.forwardmove = speed; + } else if(AI_BACKWARD) { + cmds.forwardmove = -speed; + } + + if(AI_STRAFE_RIGHT) { + cmds.rightmove = speed; + } else if(AI_STRAFE_LEFT) { + cmds.rightmove = -speed; + } + + if(AI_UPWARD) { + cmds.upmove = speed; + } else if(AI_DOWNWARD) { + cmds.upmove = -speed; + } + + GetVehicleInterfaceLocal()->BufferPilotCmds( &cmds, NULL ); + AI_SHUTTLE_DOCKED = GetVehicleInterfaceLocal()->IsVehicleDocked(); + } + + // clear out the enemy when he dies or is hidden + idActor *enemyEnt = enemy.GetEntity(); + if ( enemyEnt ) { + if ( enemyEnt->IsType( hhSpiritProxy::Type ) ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( player && player->AI_DEAD ) { + EnemyDead(); + } + } else if ( enemyEnt->health <= 0 ) { + EnemyDead(); + } + } + + //HUMANHEAD: aob - vehicle updates our viewAxis + if( !InVehicle() ) { + current_yaw += deltaViewAngles.yaw; + ideal_yaw = idMath::AngleNormalize180( ideal_yaw + deltaViewAngles.yaw ); + deltaViewAngles.Zero(); + viewAxis = idAngles( 0, current_yaw, 0 ).ToMat3(); + + // Determine turn dir + if (gameLocal.time > nextTurnUpdate) { + AI_TURN_DIR = GetTurnDir(); + nextTurnUpdate = gameLocal.time + 250; + } + } + //HUMANHEAD END + + if ( num_cinematics ) { + if ( !IsHidden() && torsoAnim.AnimDone( 0 ) ) { + PlayCinematic(); + } + RunPhysics(); + } else if ( !allowHiddenMovement && IsHidden() ) { + // hidden monsters + UpdateAIScript(); + // HUMANHEAD pdm: Vehicle support + } else if ( InVehicle() ) { + UpdateEnemyPosition(); + UpdateAIScript(); + FlyMove(); + // HUMANHEAD END + + } else { + // clear the ik before we do anything else so the skeleton doesn't get updated twice + walkIK.ClearJointMods(); + + // HUMANHEAD NLA + physicsObj.ResetNumTouchEnt(0); + // HUMANHEAD END + switch( move.moveType ) { + case MOVETYPE_DEAD : + // dead monsters + UpdateAIScript(); + DeadMove(); + break; + + case MOVETYPE_FLY : + // flying monsters + UpdateEnemyPosition(); + UpdateAIScript(); + FlyMove(); + PlayChatter(); + CheckBlink(); + break; + + case MOVETYPE_STATIC : + // static monsters + UpdateEnemyPosition(); + UpdateAIScript(); + StaticMove(); + PlayChatter(); + CheckBlink(); + break; + + case MOVETYPE_ANIM : + // animation based movement + UpdateEnemyPosition(); + UpdateAIScript(); + AnimMove(); + PlayChatter(); + CheckBlink(); + break; + + case MOVETYPE_SLIDE : + // velocity based movement + UpdateEnemyPosition(); + UpdateAIScript(); + SlideMove(); + PlayChatter(); + CheckBlink(); + break; + } + // HUMANHEAD NLA + ClientImpacts(); + // HUMANHEAD END + } + + // clear pain flag so that we recieve any damage between now and the next time we run the script + AI_PAIN = false; + AI_SPECIAL_DAMAGE = 0; + AI_PUSHED = false; + } else if ( thinkFlags & TH_PHYSICS ) { + RunPhysics(); + } + + // HUMANHEAD jrm - need to call ticker function per aaron + if (thinkFlags & TH_TICKER) { + Ticker(); + } + + if ( af_push_moveables ) { + PushWithAF(); + } + + if ( fl.hidden && allowHiddenMovement ) { + // UpdateAnimation won't call frame commands when hidden, so call them here when we allow hidden movement + animator.ServiceAnims( gameLocal.previousTime, gameLocal.time ); + } + + UpdateMuzzleFlash(); + UpdateAnimation(); + UpdateParticles(); + UpdateWounds(); + Present(); + UpdateDamageEffects(); + LinkCombat(); + + CrashLand( oldOrigin, oldVelocity ); + + if(health > 0) { + idStr tmp; + if(ai_showNoAAS.GetBool() && spawnArgs.GetString("use_aas", "", tmp) && spawnArgs.GetBool("noaas_warning","1")) { + if(!aas) { + gameRenderWorld->DrawText("?", this->GetEyePosition() + idVec3(0.0f, 0.0f, 12.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + } + } + + if ( !AI_DEAD && bCanFall && !IsHidden() && !fl.isTractored ) { + if ( physicsObj.HasContacts() || physicsObj.GetLinearVelocity().LengthSqr() < 4 ) { + lastContactTime = gameLocal.time; + if ( AI_FALLING ) { + AI_FALLING = false; + bCanFall = false; + SetState( GetScriptFunction( "state_Idle" ) ); + SetWaitState( "" ); + } + } else if ( !AI_FALLING && !af.IsActive() && physicsObj.GetLinearVelocity().LengthSqr() > 0 ) { + if ( gameLocal.time - lastContactTime > fallDelay ) { + AI_FALLING = true; + SetState( GetScriptFunction( "state_Nothing" ) ); + Event_AnimState(ANIMCHANNEL_TORSO, "Torso_Fall", 4); + Event_AnimState(ANIMCHANNEL_LEGS, "Legs_Fall", 4); + } + } + } + + //update wallwalk + if ( bCanWallwalk && !fl.isTractored ) { + trace_t TraceInfo; + gameLocal.clip.TracePoint(TraceInfo, GetOrigin(), GetOrigin() + this->GetPhysics()->GetGravityNormal() * 50, GetPhysics()->GetClipMask(), this); + if( TraceInfo.fraction < 1.0f && gameLocal.GetMatterType(TraceInfo, NULL) == SURFTYPE_WALLWALK ) { + SetGravity( -TraceInfo.c.normal * DEFAULT_GRAVITY ); + AI_WALLWALK = true; + } else if ( AI_WALLWALK ) { + SetGravity( idVec3(0,0,-DEFAULT_GRAVITY) ); + AI_WALLWALK = false; + } + } else if ( AI_WALLWALK ) { + SetGravity( idVec3(0,0,-DEFAULT_GRAVITY) ); + AI_WALLWALK = false; + } + + if ( bBossBar ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( player && player->hud ) { + player->hud->SetStateFloat( "progress", idMath::ClampFloat( 0.0f, 1.0f, float(health) / (float)spawnHealth ) ); + player->hud->StateChanged(gameLocal.time); + player->hud->Redraw( gameLocal.realClientTime ); + } + } + + if( ai_debugBrain.GetInteger() > 0 && !IsHidden() ) { + if ( enemy.IsValid() && enemy->GetHealth() > 0 ) { + gameRenderWorld->DebugArrow( colorWhite, GetOrigin(), enemy->GetOrigin(), 10 ); + float dist = ( GetPhysics()->GetOrigin() - enemy->GetPhysics()->GetOrigin() ).LengthFast(); + gameRenderWorld->DrawText( va("%i", int(dist)), this->GetEyePosition() + idVec3(0.0f, 0.0f, 60.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + if ( state ) { + if ( physicsObj.GetClipMask() == 0 ) { + gameRenderWorld->DrawText(state->Name(), GetEyePosition() + idVec3(0.0f, 0.0f, 40.0f), 0.75f, colorRed, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } else { + gameRenderWorld->DrawText(state->Name(), GetEyePosition() + idVec3(0.0f, 0.0f, 40.0f), 0.75f, colorGreen, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + } + gameRenderWorld->DrawText(torsoAnim.state, this->GetEyePosition() + idVec3(0.0f, 0.0f, 20.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + gameRenderWorld->DrawText(legsAnim.state, this->GetEyePosition() + idVec3(0.0f, 0.0f, 0.0f), 0.75f, colorYellow, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + if ( !walkIK.IsActivated() ) { + gameRenderWorld->DrawText("ik disabled", GetEyePosition() + idVec3(0.0f, 0.0f, -20.0f), 0.75f, colorRed, gameLocal.GetLocalPlayer()->viewAngles.ToMat3()); + } + } +} + +void hhMonsterAI::ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &force ) { + //we don't want monsters pushed around by the player, projectiles, or splashDamange, so early out if they are. + if( !af.IsActive() ) { + if ( ent && (ent->IsType( hhProjectile::Type ) || ent->IsType( idWorldspawn::Type ) || ent->IsType( idPlayer::Type )) ) { + return; + } + } + + af.GetPhysics()->UpdateTime( gameLocal.GetTime() ); + BecomeActive(TH_THINK); + + idAI::ApplyImpulse( ent, id, point, force ); +} + +// +// hhMonsterAI::CheckValidReaction +// Helper function for using reactions. +// +bool hhMonsterAI::CheckValidReaction() { + hhReaction *reaction = targetReaction.GetReaction(); + if( !reaction || !reaction->causeEntity.IsValid() ) { + return false; + } + return true; +} + +// +// hhMonsterAI::FinishReaction +// Called once a reaction has been used/failed. If it failed, set the failed flag and exit. +// Otherwise, set any 'finish_key' we have. +// +void hhMonsterAI::FinishReaction( bool bFailed ) { + AI_USING_REACTION = false; + if( bFailed ) { + AI_REACTION_FAILED = true; + } + else { + hhReaction *reaction = targetReaction.GetReaction(); + if( reaction && reaction->desc->finish_key.Length() ) { + spawnArgs.Set( reaction->desc->finish_key.c_str(), reaction->desc->finish_val.c_str() ); + } + } + + //monster is finished with this reaction. let others use it + if ( targetReaction.entity.IsValid() ) { + targetReaction.entity->spawnArgs.Set( "react_inuse", "0" ); + } +} + +void hhMonsterAI::Killed(idEntity *inflictor, idEntity *attacker, + int damage, const idVec3 &dir, int location ) +{ + if ( AI_DEAD ) { + AI_DAMAGE = true; + return; + } + + if ( bBossBar && spawnArgs.GetBool( "remove_bar_on_death" ) ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( player && player->hud ) { + bBossBar = false; + player->hud->HandleNamedEvent("HideProgressBar"); + player->hud->StateChanged(gameLocal.time); + } + } + + HandleNoGore(); + + idAI::Killed(inflictor, attacker, damage, dir, location); + + fl.noPortal = 0; // CJR: Set so that killed monsters never collide with portals + + SendDamageToDDA(); // CJR DDA: Send any accumulated damage to the dda system + + // General non-item dropping (for monsters, souls, etc.) + const idKeyValue *kv = NULL; + kv = spawnArgs.MatchPrefix( "def_drops", NULL ); + while ( kv ) { + + idStr drops = kv->GetValue(); + idDict args; + + idStr last5 = kv->GetKey().Right(5); + if ( drops.Length() && idStr::Icmp( last5, "Joint" ) != 0) { + + args.Set( "classname", drops ); + + // HUMANHEAD pdm: specify monster so souls can call back to remove body when picked up + args.Set("monsterSpawnedBy", name.c_str()); + + idVec3 origin; + idMat3 axis; + idStr jointKey = kv->GetKey() + idStr("Joint"); + idStr jointName = spawnArgs.GetString( jointKey ); + idStr joint2JointKey = kv->GetKey() + idStr("Joint2Joint"); + idStr j2jName = spawnArgs.GetString( joint2JointKey ); + + idEntity *newEnt = NULL; + gameLocal.SpawnEntityDef( args, &newEnt ); + HH_ASSERT(newEnt != NULL); + + // Spin to correct heading + if(newEnt->IsType(hhMonsterAI::Type)) { + hhMonsterAI *newAI = static_cast(newEnt); + newAI->current_yaw = current_yaw; + newAI->ideal_yaw = ideal_yaw; + } + + if(jointName.Length()) { + jointHandle_t joint = GetAnimator()->GetJointHandle( jointName ); + if (!GetAnimator()->GetJointTransform( joint, gameLocal.time, origin, axis ) ) { + gameLocal.Printf( "%s refers to invalid joint '%s' on entity '%s'\n", (const char*)jointKey.c_str(), (const char*)jointName, (const char*)name ); + origin = renderEntity.origin; + axis = renderEntity.axis; + } + axis *= renderEntity.axis; + origin = renderEntity.origin + origin * renderEntity.axis; + newEnt->SetAxis(axis); + newEnt->SetOrigin(origin); + } + else { + + newEnt->SetAxis(viewAxis); + newEnt->SetOrigin(GetOrigin()); + } + + } + + kv = spawnArgs.MatchPrefix( "def_drops", kv ); + } +} + +void hhMonsterAI::Event_Remove( void ) { + if( InVehicle() ) { + GetVehicleInterface()->GetVehicle()->EjectPilot(); + } + + idAI::Event_Remove(); +} + +void hhMonsterAI::EnterVehicle( hhVehicle* vehicle ) { + if (!vehicle->WillAcceptPilot(this)) { + return; + } + spawnArgs.Set( "use_aas", spawnArgs.GetString( "aas_shuttle", "aasDroid" ) ); + SetAAS(); + idAI::EnterVehicle( vehicle ); + PostEventSec( &MA_SetVehicleState, 0.1f ); +} + +bool hhMonsterAI::TestMelee( void ) const { + trace_t trace; + idActor *enemyEnt = enemy.GetEntity(); + + if ( !enemyEnt || !melee_range ) { + return false; + } + + //FIXME: make work with gravity vector + idVec3 org = physicsObj.GetOrigin(); + const idBounds &myBounds = physicsObj.GetBounds(); + idBounds bounds; + + idBounds meleeBounds( spawnArgs.GetVector( "melee_boundmin", "0 0 0" ), spawnArgs.GetVector( "melee_boundmax", "0 0 0" ) ); + if ( meleeBounds != bounds_zero ) { + //check custom rotated meleebound + idBox meleeBox( meleeBounds, org, renderEntity.axis ); + idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds(); + enemyBounds.TranslateSelf( enemyEnt->GetPhysics()->GetOrigin() ); + idBox enemyBox( enemyBounds ); + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBox( colorYellow, meleeBox ); + } + + if ( !enemyBox.IntersectsBox( meleeBox ) ) { + return false; + } + } else { + // expand the bounds out by our melee range + bounds[0][0] = -melee_range; + bounds[0][1] = -melee_range; + bounds[0][2] = myBounds[0][2] - 4.0f; + bounds[1][0] = melee_range; + bounds[1][1] = melee_range; + bounds[1][2] = myBounds[1][2] + 4.0f; + bounds.TranslateSelf( org ); + + idVec3 enemyOrg = enemyEnt->GetPhysics()->GetOrigin(); + idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds(); + enemyBounds.TranslateSelf( enemyOrg ); + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorYellow, bounds, vec3_zero, gameLocal.msec ); + } + + if ( !bounds.IntersectsBounds( enemyBounds ) ) { + return false; + } + } + + idVec3 start = GetEyePosition(); + idVec3 end = enemyEnt->GetEyePosition(); + + gameLocal.clip.TracePoint( trace, start, end, MASK_SHOT_BOUNDINGBOX, this ); + if ( ( trace.fraction == 1.0f ) || ( gameLocal.GetTraceEntity( trace ) == enemyEnt ) ) { + return true; + } + + return false; +} + +bool hhMonsterAI::MoveToPosition( const idVec3 &pos, bool enemyBlocks ) { + if(AI_VEHICLE && InVehicle()) { + GetVehicleInterfaceLocal()->ThrustTowards( pos, 1.0f ); + return true; + } else { + return idAI::MoveToPosition( pos, enemyBlocks ); + } +} + +bool hhMonsterAI::TurnToward( const idVec3 &pos ) { + if (AI_VEHICLE && InVehicle()) { + GetVehicleInterfaceLocal()->OrientTowards( pos, 0.5 ); + return true; + } else { + return idAI::TurnToward( pos ); + } +} + +bool hhMonsterAI::CanSee( idEntity *ent, bool useFov ) { + trace_t tr; + idVec3 eye; + idVec3 toPos; + + if ( ent->IsHidden() ) { + return false; + } + + if ( ent->IsType( idActor::Type ) ) { + idActor *act = static_cast(ent); + + // If this actor is in a vehicle, look at the vehicle, not the actor + if(act->InVehicle()) { + ent = act->GetVehicleInterface()->GetVehicle(); + } + } + + if ( ent->IsType( idActor::Type ) ) { + toPos = ( ( idActor * )ent )->GetEyePosition(); + } else { + toPos = ent->GetPhysics()->GetOrigin(); + } + + if ( useFov && !CheckFOV( toPos ) ) { + return false; + } + + eye = GetEyePosition(); + + if ( InVehicle() ) { + gameLocal.clip.TracePoint( tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, GetVehicleInterface()->GetVehicle() ); // HUMANHEAD JRM + if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == ent ) ) { + return true; + } + } else { + gameLocal.clip.TracePoint( tr, eye, toPos, MASK_SHOT_BOUNDINGBOX, this ); // HUMANHEAD JRM + if ( tr.fraction >= 1.0f || ( gameLocal.GetTraceEntity( tr ) == ent ) ) { + return true; + } else if ( bSeeThroughPortals && aas ) { + shootTarget = NULL; + int myArea = gameRenderWorld->PointInArea( GetOrigin() ); + int numPortals = gameRenderWorld->NumGamePortalsInArea( myArea ); + if ( numPortals > 0 ) { + int enemyArea = gameRenderWorld->PointInArea( ent->GetOrigin() ); + for ( int i=0;iGetSoundPortal( myArea, i ).areas[0] == enemyArea ) { + //find the portal and set it as this monster's shoottarget + idEntity *spawnedEnt = NULL; + for( spawnedEnt = gameLocal.spawnedEntities.Next(); spawnedEnt != NULL; spawnedEnt = spawnedEnt->spawnNode.Next() ) { + if ( !spawnedEnt->IsType( hhPortal::Type ) ) { + continue; + } + if ( gameRenderWorld->PointInArea( spawnedEnt->GetOrigin() ) == myArea) { + shootTarget = spawnedEnt; + return true; + } + } + } + } + } + } + } + + return false; +} + +idPlayer* hhMonsterAI::GetClosestPlayer(void) { + idEntity *closestEnt = NULL; + float closestDist = idMath::INFINITY; + + for(int i=0;iIsType(idPlayer::Type)); + + float l = (ent->GetOrigin()-GetOrigin()).Length(); + if(l < closestDist || !closestEnt) { + closestDist = l; + closestEnt = ent; + } + } + } + + return static_cast(closestEnt); +} + +void hhMonsterAI::Show() { + if ( spawnThinkFlags != 0 ) { + int temp = spawnThinkFlags; + spawnThinkFlags = 0; + BecomeActive( temp ); + } + idAI::Show(); + PostEventMS(&MA_EnemyOnSpawn, 10); + Event_InitialWallwalk(); +} + +bool hhMonsterAI::GetFacePosAngle( const idVec3 &pos, float &delta ) { + float diff; + float angle1; + float angle2; + idVec3 sourceOrigin; + idVec3 targetOrigin; + + sourceOrigin = GetPhysics()->GetOrigin(); + targetOrigin = pos;//target->GetPhysics()->GetOrigin(); + + angle1 = DEG2RAD( GetGravViewAxis()[0].ToYaw() ); // VIEWAXIS_TO_GETGRAVVIEWAXIS + angle2 = hhUtils::PointToAngle( targetOrigin.x - sourceOrigin.x, targetOrigin.y - sourceOrigin.y ); + if(angle2 > angle1) { + diff = angle2 - angle1; + + if( diff > DEG2RAD(180.0f) ) { + delta = DEG2RAD(359.9f) - diff; + return false; + } + else { + delta = diff; + return true; + } + } + else { + diff = angle1 - angle2; + if( diff > DEG2RAD(180.0f) ) { + delta = DEG2RAD(359.9f) - diff; + return true; + } + else { + delta = diff; + return false; + } + } +} + +void hhMonsterAI::Distracted( idActor *newEnemy ) { + SetEnemy( newEnemy ); +} + +void hhMonsterAI::SetEnemy( idActor *newEnemy ) { + idAI::SetEnemy( newEnemy ); +} + +idVec3 hhMonsterAI::GetTouchPos(idEntity *ent, const hhReactionDesc *desc ) { + assert(ent != NULL); + assert(desc != NULL); + + idVec3 pos = ent->GetOrigin(); + + idVec3 offset = desc->touchOffsets.GetVector("all", "0 0 0"); + if ( offset == vec3_zero ) { + // Each monster def type has its own offset etc. + // touchoffset_monster_hunter + offset = desc->touchOffsets.GetVector(GetEntityDefName(), "0 0 0"); + } + idStr touchDirType = desc->touchDir; + + // When touching this ent, move to it as if it were 'cover' + if( touchDirType == idStr("cover") && enemy.IsValid() ) { + idVec3 goalPos = ent->GetOrigin() + offset.x * (ent->GetOrigin() - GetEnemy()->GetOrigin()).ToNormal(); + goalPos.z += offset.z; + return goalPos; + } + // Move directly toward this ent from our pos + else if(touchDirType == idStr("direct")) { + idVec3 dir = GetOrigin() - pos; + dir.Normalize(); + offset *= dir.ToMat3(); + return pos + offset; + } + else if(touchDirType == idStr("object")) { + ent->GetFloorPos( 64.f, pos ); + idVec3 dir = GetOrigin() - pos; + dir.Normalize(); + return pos + (dir * offset.x); + } + else { + offset *= ent->GetAxis(); + return pos + offset; + } + + +} + +bool hhMonsterAI::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + if ( af.IsActive() ) { + af.GetPhysicsToVisualTransform( origin, axis ); + return true; + } + origin = modelOffset; + if ( GetBindMaster() && bindJoint != INVALID_JOINT ) { + idMat3 masterAxis; + idVec3 masterOrigin; + GetMasterPosition( masterOrigin, masterAxis ); + axis = physicsObj.localAxis * masterAxis; + origin = masterOrigin + physicsObj.GetLocalOrigin() * masterAxis; + } else if ( ( InVehicle() || bBindOrient ) && GetBindMaster() ) { + axis = GetBindMaster()->GetAxis(); + } else { + axis = viewAxis; + } + return true; +} + +void hhMonsterAI::UpdateModelTransform( void ) { + idVec3 origin; + idMat3 axis; + + if ( GetPhysicsToVisualTransform( origin, axis ) ) { + if ( bBindAxis && GetBindMaster() ) { + renderEntity.axis = GetBindMaster() ->GetAxis(); + } else { + renderEntity.axis = axis * GetPhysics()->GetAxis(); + } + if ( GetBindMaster() && bindJoint != INVALID_JOINT ) { + if ( head.IsValid() && head->GetPhysics() ) { + head->GetPhysics()->Evaluate(gameLocal.time-gameLocal.previousTime, gameLocal.time); + } + renderEntity.origin = origin; + } else { + if ( GetBindMaster() && head.IsValid() && head->GetPhysics() ) { + head->GetPhysics()->Evaluate(gameLocal.time-gameLocal.previousTime, gameLocal.time); + } + renderEntity.origin = GetPhysics()->GetOrigin() + origin * renderEntity.axis; + } + } else { + renderEntity.axis = GetPhysics()->GetAxis(); + renderEntity.origin = GetPhysics()->GetOrigin(); + } +} + +void hhMonsterAI::UpdateFromPhysics( bool moveBack ) { + // set master delta angles for actors + if ( GetBindMaster() ) { + if( !InVehicle() ) { + idAngles delta = GetDeltaViewAngles(); + if ( moveBack ) { + delta.yaw -= physicsObj.GetMasterDeltaYaw(); + } else { + delta.yaw += physicsObj.GetMasterDeltaYaw(); + } + + SetDeltaViewAngles( delta ); + } else { + SetAxis( GetBindMaster()->GetAxis() ); + } + } + + if ( UpdateAnimationControllers() ) { + BecomeActive( TH_ANIMATE ); + } + + UpdateVisuals(); +} + +void hhMonsterAI::CreateHealthTriggers() { + const char keyPrefix[] = "health_percent_trigger_"; + int keyPrefixLen = strlen(keyPrefix); + + const idKeyValue *kv = spawnArgs.MatchPrefix(keyPrefix, NULL); + healthTriggers.Clear(); + while(kv) { + hhMonsterHealthTrigger t; + idStr k = kv->GetKey(); + idStr perct = k.Right(k.Length() - strlen(keyPrefix)); + float p = float(atoi(perct.c_str())) * 0.01f; + t.healthThresh = int(float(health) * p); + t.triggerEnt = gameLocal.FindEntity(kv->GetValue().c_str()); + if(!t.triggerEnt.GetEntity()) { + gameLocal.Warning("%s specified %s key with entity \"%s\", but that entity does not exist!", name.c_str(), kv->GetKey().c_str(), kv->GetValue().c_str()); + } + healthTriggers.Append(t); + kv = spawnArgs.MatchPrefix(keyPrefix, kv); + } +} + +void hhMonsterAI::UpdateHealthTriggers(int oldHealth, int currHealth) { + + // Trigger them + for(int i=0;iProcessEvent(&EV_Activate, this); + healthTriggers[i].triggerCount++; + } + } + } +} + +// +// Damage() +// +void hhMonsterAI::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + // Return if we don't take damage + if ( !fl.takedamage ) { + return; + } + + if ( spawnArgs.GetBool( "noPlayerDamage", "0" ) ) { + if ( attacker && attacker->IsType( idPlayer::Type ) ) { + return; + } + } + + int oldHealth = health; + idAI::Damage(inflictor, attacker, dir, damageDefName, damageScale, location); + UpdateHealthTriggers(oldHealth, health); + + if ( bCustomBlood && inflictor ) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_custom_blood", inflictor->GetOrigin(), mat3_identity, &fxInfo ); + } + + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( AI_DEAD && damageDef ) { + if ( damageDef->GetBool( "ice" ) && spawnArgs.GetBool( "can_freeze", "0" ) ) { + SetSkinByName( spawnArgs.GetString( "skin_freeze" ) ); + + if( af.IsLoaded() ) { + af.GetPhysics()->SetSelfCollision(false); + af.GetPhysics()->SetContactFrictionScale(0); + af.GetPhysics()->SetTimeScaleRamp(0,2); + + for(int i=0; i < af.GetPhysics()->GetNumConstraints(); i++) { + idAFConstraint* constraint = af.GetPhysics()->GetConstraint(i); + switch( constraint->GetType() ) { + case CONSTRAINT_BALLANDSOCKETJOINT: + static_cast(constraint)->SetFriction(200.0f); + static_cast(constraint)->SetNoLimit(); + break; + case CONSTRAINT_UNIVERSALJOINT: + static_cast(constraint)->SetFriction(200.0f); + static_cast(constraint)->SetNoLimit(); + break; + case CONSTRAINT_HINGE: + static_cast(constraint)->SetFriction(200.0f); + static_cast(constraint)->SetNoLimit(); + break; + } + } + } + + spawnArgs.Set("fx_deatheffect", spawnArgs.GetString( "fx_ice" )); + spawnArgs.Set("produces_splats", "0"); + if( modelDefHandle > 0 ) + gameRenderWorld->RemoveDecals( modelDefHandle ); + //SetDeformation(DEFORMTYPE_DEATHEFFECT, gameLocal.time + 5000, 12000); + SetShaderParm(SHADERPARM_TIME_OF_DEATH, MS2SEC(gameLocal.time+100000)); + CancelEvents( &EV_Dispose ); + PostEventSec( &EV_Dispose, 5 ); + } + if ( damageDef->GetBool( "burn" ) && !spawnArgs.GetBool( "no_burn" ) ) { + SetSkinByName( spawnArgs.GetString( "skin_burn" ) ); + spawnArgs.Set("fx_deatheffect", spawnArgs.GetString( "fx_burn" )); + if( modelDefHandle > 0 ) + gameRenderWorld->RemoveDecals( modelDefHandle ); + CancelEvents( &EV_Dispose ); + PostEventSec( &EV_Dispose, 0 ); + } + if ( damageDef->GetBool( "acid" ) && spawnArgs.GetBool( "acidburn" ) ) { + SetSkinByName( spawnArgs.GetString( "skin_acidburn" ) ); + SetDeformation(DEFORMTYPE_DEATHEFFECT, gameLocal.time + 2500, 8000); // starttime, duration + PostEventSec( &EV_StartSound, 1.5f, "snd_acid", SND_CHANNEL_ANY, 1 ); + spawnArgs.Set("fx_deatheffect", spawnArgs.GetString( "fx_acid" )); + spawnArgs.Set("mtr_splat1", spawnArgs.GetString( "mtr_acidsplat" )); + spawnArgs.Delete("mtr_splat2"); + spawnArgs.Delete("mtr_splat3"); + spawnArgs.Delete("mtr_splat4"); + spawnArgs.Set("keepDecals", "1"); + + CancelEvents( &EV_Dispose ); + PostEventSec( &EV_Dispose, 1.5 ); + } + } +} + +bool hhMonsterAI::NearEnoughTouchPos( idEntity* ent, const idVec3& targetPos, idBounds& bounds ) { + bounds.TranslateSelf( targetPos ); + + idBounds bnds( idVec3(-16.f, -16.f, -8.f), idVec3(16.f, 16.f, 64.f) ); + bnds.TranslateSelf( physicsObj.GetOrigin() ); +//uncomment these to show debug lines +// gameRenderWorld->DebugBounds( colorOrange, bounds ); +// gameRenderWorld->DebugArrow( colorRed, targetPos, targetPos + idVec3(0, 0, 10), 5 ); +// gameRenderWorld->DebugBounds( colorRed, bnds ); + if( bnds.IntersectsBounds( bounds ) ) { + return true; + } + return false; +} + +bool hhMonsterAI::GetTouchPosBound( const hhReactionDesc *desc, idBounds & bnds ) { + idStr minName, maxName; + idVec3 min, max; + + minName = va("min_%s", GetEntityDefName()); + maxName = va("max_%s", GetEntityDefName()); + + if( !desc->touchOffsets.GetVector(minName.c_str(), "0 0 0", min) || !desc->touchOffsets.GetVector(maxName.c_str(), "0 0 0", max) ) { + return false; + } + min -= idVec3( 16.f, 16.f, 8.f ); + max += idVec3( 16.f, 16.f, 64.f ); + + bnds.Clear(); + bnds[ 0 ] = min; + bnds[ 1 ] = max; + return true; +} + +int hhMonsterAI::EvaluateReaction( const hhReaction *react ) { +// Volume/Distance + // If a volume in specified, then use that as our spatial check + float distSq = ( react->causeEntity->GetOrigin() - GetOrigin() ).LengthSqr(); + + if( react->desc->listenerVolumes.Num() > 0 ) { + int count = 0; + int i; + for( i = 0; i < react->desc->listenerVolumes.Num(); i++ ) { + assert( react->desc->listenerVolumes[ i ]->GetPhysics() != NULL ); + assert( GetPhysics() != NULL ); + if( react->desc->listenerVolumes[ i ]->GetPhysics()->GetAbsBounds().IntersectsBounds( GetPhysics()->GetAbsBounds() ) ) { + //MDC-TODO: Draw bounds debugging info... + count++; //one is good enough to continue - no need to check the rest + break; + } + } + // If we are in ZERO of the listener volumes, then we can bail because this reaction doesn't apply to us + if( count == 0 ) { + return 0; + } + } + if ( react->desc->effectVolumes.Num() > 0 && GetEnemy() ) { + int count = 0; + for( int i = 0; i < react->desc->effectVolumes.Num(); i++ ) { + assert( react->desc->effectVolumes[ i ]->GetPhysics() != NULL ); + assert( GetPhysics() != NULL ); + if( react->desc->effectVolumes[ i ]->GetPhysics()->GetAbsBounds().IntersectsBounds( GetEnemy()->GetPhysics()->GetAbsBounds() ) ) { + count++; //one is good enough to return false - no need to check the rest + break; + } + } + // If enemy is in ZERO of the effect volumes, then we can bail because this reaction doesn't apply to us + if( count == 0 ) { + return 0; + } + } + if( react->desc->listenerRadius > 0.f || react->desc->listenerMinRadius > 0.f ) { + // TOO FAR + if( react->desc->listenerRadius > 0.f && distSq > react->desc->listenerRadius * react->desc->listenerRadius ) { + return 0; + } + // TOO CLOSE + if( react->desc->listenerMinRadius > 0.f && distSq < react->desc->listenerMinRadius * react->desc->listenerMinRadius ) { + return 0; + } + } +// Path-finding + if( react->desc->CauseRequiresPathfinding() ) { + if( !HasPathTo( GetTouchPos(react->causeEntity.GetEntity(), react->desc ) ) ) { + //MDC-TODO: draw path debugging + return 0; + } + else { + //MDC-TODO: draw path debugging + } + } +// Can-See + if( react->desc->flags & hhReactionDesc::flagReq_CanSee ) { + if( react->causeEntity.IsValid() ) { + if( !CanSee(react->causeEntity.GetEntity(), TRUE) ) { + return 0; + } + } + } + + return 100; +} + +int hhMonsterAI::ReactionTo( const idEntity *ent ) { + if ( bNoCombat ) { + return ATTACK_IGNORE; + } + const idActor *actor = static_cast( ent ); + if( actor && actor->IsType(hhDeathProxy::Type) ) { + return ATTACK_IGNORE; + } + + //only attack spiritwalking players if they hurt me + if ( ent->IsType( hhPlayer::Type ) ) { + const hhPlayer *player = static_cast( ent ); + if ( nextSpiritProxyCheck == 0 && player && player->IsSpiritWalking() ) { + nextSpiritProxyCheck = gameLocal.time + SEC2MS(2); + return ATTACK_ON_DAMAGE; + } + } + + if ( ent->IsType( hhSpiritProxy::Type ) ) { + if ( gameLocal.time > nextSpiritProxyCheck ) { + nextSpiritProxyCheck = 0; + //attack spiritproxy on sight if we have no enemy or if its closer than our current enemy + if ( enemy.IsValid() && enemy->IsType( hhPlayer::Type ) ) { + float distToEnemy = (enemy->GetOrigin() - GetOrigin()).LengthSqr(); + float distToProxy = (ent->GetOrigin() - GetOrigin()).LengthSqr(); + if ( distToProxy < distToEnemy ) { + return ATTACK_ON_SIGHT; + } else { + //HUMANHEAD jsh PCF 4/29/06 fixed logic error with spiritproxy checking + return ATTACK_IGNORE; + } + } else { + return ATTACK_ON_SIGHT; + } + } else { + return ATTACK_IGNORE; + } + } + + if ( ent->IsType( hhMonsterAI::Type ) ) { + const hhMonsterAI *entAI = static_cast( ent ); + if ( entAI && entAI->bNeverTarget ) { + return ATTACK_IGNORE; + } + } + + return idAI::ReactionTo( ent ); +} + +bool hhMonsterAI::UpdateAnimationControllers( void ) { + idVec3 local; + idVec3 focusPos; + idVec3 left; + idVec3 dir; + idVec3 orientationJointPos; + idVec3 localDir; + idAngles newLookAng; + idAngles diff; + idMat3 mat; + idMat3 axis; + idMat3 orientationJointAxis; + idAFAttachment *headEnt = head.GetEntity(); + idVec3 eyepos; + idVec3 pos; + int i; + idAngles jointAng; + float orientationJointYaw; + + if ( AI_DEAD ) { + return idActor::UpdateAnimationControllers(); + } + + if ( orientationJoint == INVALID_JOINT ) { + orientationJointAxis = viewAxis; + orientationJointPos = physicsObj.GetOrigin(); + orientationJointYaw = current_yaw; + } else { + GetJointWorldTransform( orientationJoint, gameLocal.time, orientationJointPos, orientationJointAxis ); + orientationJointYaw = orientationJointAxis[ 2 ].ToYaw(); + orientationJointAxis = idAngles( 0.0f, orientationJointYaw, 0.0f ).ToMat3(); + } + + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorCyan, orientationJointPos, orientationJointPos + orientationJointAxis[0] * 64.0, 10, 1 ); + } + + if ( focusJoint != INVALID_JOINT ) { + if ( headEnt ) { + headEnt->GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis ); + } else { + // JRMMERGE_GRAVAXIS - What about GetGravAxis() are we still using/needing that? + GetJointWorldTransform( focusJoint, gameLocal.time, eyepos, axis ); + } + eyeOffset.z = eyepos.z - physicsObj.GetOrigin().z; + } else { + eyepos = GetEyePosition(); + } + + if ( headEnt ) { + CopyJointsFromBodyToHead(); + } + + // Update the IK after we've gotten all the joint positions we need, but before we set any joint positions. + // Getting the joint positions causes the joints to be updated. The IK gets joint positions itself (which + // are already up to date because of getting the joints in this function) and then sets their positions, which + // forces the heirarchy to be updated again next time we get a joint or present the model. If IK is enabled, + // or if we have a seperate head, we end up transforming the joints twice per frame. Characters with no + // head entity and no ik will only transform their joints once. Set g_debuganim to the current entity number + // in order to see how many times an entity transforms the joints per frame. + idActor::UpdateAnimationControllers(); + + idEntity *focusEnt = focusEntity.GetEntity(); + //HUMANHEAD jsh allow eyefocus independent from allowJointMod + if ( ( !allowJointMod && !allowEyeFocus ) || ( gameLocal.time >= focusTime && focusTime != -1 ) || GetPhysics()->GetGravityNormal() != idVec3( 0,0,-1) ) { + focusPos = GetEyePosition() + orientationJointAxis[ 0 ] * 512.0f; + } else if ( focusEnt == NULL ) { + // keep looking at last position until focusTime is up + focusPos = currentFocusPos; + } else if ( focusEnt == enemy.GetEntity() ) { + focusPos = lastVisibleEnemyPos + lastVisibleEnemyEyeOffset - eyeVerticalOffset * enemy.GetEntity()->GetPhysics()->GetGravityNormal(); + } else if ( focusEnt->IsType( idActor::Type ) ) { + focusPos = static_cast( focusEnt )->GetEyePosition() - eyeVerticalOffset * focusEnt->GetPhysics()->GetGravityNormal(); + } else { + focusPos = focusEnt->GetPhysics()->GetOrigin(); + } + + currentFocusPos = currentFocusPos + ( focusPos - currentFocusPos ) * eyeFocusRate; + + // determine yaw from origin instead of from focus joint since joint may be offset, which can cause us to bounce between two angles + dir = focusPos - orientationJointPos; + newLookAng.yaw = idMath::AngleNormalize180( dir.ToYaw() - orientationJointYaw ); + newLookAng.roll = 0.0f; + newLookAng.pitch = 0.0f; + + newLookAng += lookOffset; + +#if 0 + gameRenderWorld->DebugLine( colorRed, orientationJointPos, focusPos, gameLocal.msec ); + gameRenderWorld->DebugLine( colorYellow, orientationJointPos, orientationJointPos + orientationJointAxis[ 0 ] * 32.0f, gameLocal.msec ); + gameRenderWorld->DebugLine( colorGreen, orientationJointPos, orientationJointPos + newLookAng.ToForward() * 48.0f, gameLocal.msec ); +#endif + +//JRMMERGE_GRAVAXIS: This changed to much to merge, see if you can get your monsters on planets changes back in here. I'll leave both versions +#if OLD_CODE + GetGravViewAxis().ProjectVector( dir, localDir ); // HUMANHEAD JRM: VIEWAXIS_TO_GETGRAVVIEWAXIS + lookAng.yaw = idMath::AngleNormalize180( localDir.ToYaw() ); + lookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ); + lookAng.roll = 0.0f; +#else + // determine pitch from joint position + dir = focusPos - eyepos; + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorYellow, eyepos, eyepos + dir, 10, 1 ); + } + dir.NormalizeFast(); + orientationJointAxis.ProjectVector( dir, localDir ); + newLookAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ) + lookOffset.pitch; + newLookAng.roll = 0.0f; +#endif + + diff = newLookAng - lookAng; + + if ( eyeAng != diff ) { + eyeAng = diff; + eyeAng.Clamp( eyeMin, eyeMax ); + idAngles angDelta = diff - eyeAng; + if ( !angDelta.Compare( ang_zero, 0.1f ) ) { + alignHeadTime = gameLocal.time; + } else { + alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime; + } + } + + if ( idMath::Fabs( newLookAng.yaw ) < 0.1f ) { + alignHeadTime = gameLocal.time; + } + + if ( ( gameLocal.time >= alignHeadTime ) || ( gameLocal.time < forceAlignHeadTime ) ) { + alignHeadTime = gameLocal.time + ( 0.5f + 0.5f * gameLocal.random.RandomFloat() ) * focusAlignTime; + destLookAng = newLookAng; + destLookAng.Clamp( lookMin, lookMax ); + } + + diff = destLookAng - lookAng; + if ( ( lookMin.pitch == -180.0f ) && ( lookMax.pitch == 180.0f ) ) { + if ( ( diff.pitch > 180.0f ) || ( diff.pitch <= -180.0f ) ) { + diff.pitch = 360.0f - diff.pitch; + } + } + if ( ( lookMin.yaw == -180.0f ) && ( lookMax.yaw == 180.0f ) ) { + if ( diff.yaw > 180.0f ) { + diff.yaw -= 360.0f; + } else if ( diff.yaw <= -180.0f ) { + diff.yaw += 360.0f; + } + } + lookAng = lookAng + diff * headFocusRate; + lookAng.Normalize180(); + + jointAng.roll = 0.0f; + if ( allowJointMod ) { + for( i = 0; i < lookJoints.Num(); i++ ) { + jointAng.pitch = 0; + jointAng.yaw = lookAng.yaw * lookJointAngles[ i ].yaw; + animator.SetJointAxis( lookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() ); + + idMat3 pitchRot; + hhMath::BuildRotationMatrix( DEG2RAD(-lookAng.pitch * lookJointAngles[ i ].pitch), 0, pitchRot ); + animator.GetJointLocalTransform( lookJoints[ i ], gameLocal.time, pos, mat ); + animator.SetJointAxis( lookJoints[ i ], JOINTMOD_LOCAL_OVERRIDE, pitchRot * mat ); + } + } + + if ( move.moveType == MOVETYPE_FLY ) { + // lean into turns + AdjustFlyingAngles(); + } + + if ( headEnt ) { + idAnimator *headAnimator = headEnt->GetAnimator(); + + // HUMANHEAD pdm: Added support for look joints in head entities + if ( allowJointMod ) { + for( i = 0; i < headLookJoints.Num(); i++ ) { + jointAng.pitch = lookAng.pitch * headLookJointAngles[ i ].pitch; + jointAng.yaw = lookAng.yaw * headLookJointAngles[ i ].yaw; + headAnimator->SetJointAxis( headLookJoints[ i ], JOINTMOD_WORLD, jointAng.ToMat3() ); + } + } + // HUMANHEAD END + + if ( allowEyeFocus ) { + idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3(); idMat3 headTranspose = headEnt->GetPhysics()->GetAxis().Transpose(); + axis = eyeAxis * orientationJointAxis; + left = axis[ 1 ] * eyeHorizontalOffset; + eyepos -= headEnt->GetPhysics()->GetOrigin(); + headAnimator->SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose ); + headAnimator->SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + ( axis[ 0 ] * 64.0f - left ) * headTranspose ); + + //if ( ai_debugMove.GetBool() ) { + // gameRenderWorld->DebugLine( colorRed, orientationJointPos, eyepos + ( axis[ 0 ] * 64.0f + left ) * headTranspose, gameLocal.msec ); + //} + } else { + headEnt->BecomeActive( TH_ANIMATE ); + headAnimator->ClearJoint( leftEyeJoint ); + headAnimator->ClearJoint( rightEyeJoint ); + } + } else { + if ( allowEyeFocus ) { + idMat3 eyeAxis = ( lookAng + eyeAng ).ToMat3(); + axis = eyeAxis * orientationJointAxis; + left = axis[ 1 ] * eyeHorizontalOffset; + eyepos += axis[ 0 ] * 64.0f - physicsObj.GetOrigin(); + animator.SetJointPos( leftEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos + left ); + animator.SetJointPos( rightEyeJoint, JOINTMOD_WORLD_OVERRIDE, eyepos - left ); + } else { + animator.ClearJoint( leftEyeJoint ); + animator.ClearJoint( rightEyeJoint ); + } + } + + //HUMANHEAD pdm jawflap + hhAnimator *theAnimator; + if (head.IsValid()) { + theAnimator = head->GetAnimator(); + } + else { + theAnimator = GetAnimator(); + } + JawFlap(theAnimator); + //END HUMANHEAD + + return true; +} + +//overridden to allow attack nodes farther away than our current enemydistance +void hhMonsterAI::Event_GetCombatNode( void ) { + int i; + float dist; + idEntity *targetEnt; + idCombatNode *node; + float bestDist; + idCombatNode *bestNode; + idActor *enemyEnt = enemy.GetEntity(); + + if ( !targets.Num() ) { + // no combat nodes + idThread::ReturnEntity( NULL ); + return; + } + + if ( !enemyEnt || !EnemyPositionValid() ) { + // don't return a combat node if we don't have an enemy or + // if we can see he's not in the last place we saw him + idThread::ReturnEntity( NULL ); + return; + } + + // find the closest attack node that can see our enemy and is closer than our enemy + bestNode = NULL; + const idVec3 &myPos = physicsObj.GetOrigin(); + bestDist = 9999999.0f ; + for( i = 0; i < targets.Num(); i++ ) { + targetEnt = targets[ i ].GetEntity(); + if ( !targetEnt || !targetEnt->IsType( idCombatNode::Type ) ) { + continue; + } + + node = static_cast( targetEnt ); + if ( !node->IsDisabled() && node->EntityInView( enemyEnt, lastVisibleEnemyPos ) ) { + idVec3 org = node->GetPhysics()->GetOrigin(); + dist = ( myPos - org ).LengthSqr(); + if ( dist < bestDist ) { + bestNode = node; + bestDist = dist; + } + } + } + + idThread::ReturnEntity( bestNode ); +} + +void hhMonsterAI::FlyTurn( void ) { //overridden for vehicle code changes + if ( move.moveCommand == MOVE_FACE_ENEMY ) { + TurnToward( lastVisibleEnemyPos ); + } else if ( ( move.moveCommand == MOVE_FACE_ENTITY ) && move.goalEntity.GetEntity() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + } else if ( move.speed > 0.0f ) { + const idVec3 &vel = physicsObj.GetLinearVelocity(); + if ( vel.ToVec2().LengthSqr() > 0.1f ) { + if ( InVehicle() ) { + if ( move.goalEntity.IsValid() ) { + TurnToward( move.goalEntity.GetEntity()->GetPhysics()->GetOrigin() ); + } else { + TurnToward( GetVehicleInterface()->GetVehicle()->GetPhysics()->GetLinearVelocity().ToYaw() ); + } + } else { + if ( vel.ToVec2().LengthSqr() > 0.1f ) { + TurnToward( vel.ToYaw() ); + } + } + } + } + Turn(); +} + +//overridden to allow custom hearingRange +void hhMonsterAI::UpdateEnemyPosition( void ) { + idActor *enemyEnt = enemy.GetEntity(); + int enemyAreaNum; + int areaNum; + aasPath_t path; + predictedPath_t predictedPath; + idVec3 enemyPos; + bool onGround; + + if ( !enemyEnt ) { + return; + } + + const idVec3 &org = physicsObj.GetOrigin(); + + if ( move.moveType == MOVETYPE_FLY ) { + enemyPos = enemyEnt->GetPhysics()->GetOrigin(); + onGround = true; + } else { + onGround = enemyEnt->GetFloorPos( 64.0f, enemyPos ); + if ( enemyEnt->OnLadder() ) { + onGround = false; + } + } + + if ( onGround ) { + // when we don't have an AAS, we can't tell if an enemy is reachable or not, + // so just assume that he is. + if ( !aas ) { + enemyAreaNum = 0; + lastReachableEnemyPos = enemyPos; + } else { + enemyAreaNum = PointReachableAreaNum( enemyPos, 1.0f ); + if ( enemyAreaNum ) { + areaNum = PointReachableAreaNum( org ); + if ( PathToGoal( path, areaNum, org, enemyAreaNum, enemyPos ) ) { + lastReachableEnemyPos = enemyPos; + } + } + } + } + + AI_ENEMY_IN_FOV = false; + AI_ENEMY_VISIBLE = false; + + if ( CanSee( enemyEnt, false ) ) { + AI_ENEMY_VISIBLE = true; + if ( CheckFOV( enemyEnt->GetPhysics()->GetOrigin() ) ) { + AI_ENEMY_IN_FOV = true; + } + + SetEnemyPosition(); + } else { + // check if we heard any sounds in the last frame + if ( enemyEnt == gameLocal.GetAlertEntity() ) { + float dist = ( enemyEnt->GetPhysics()->GetOrigin() - org ).LengthSqr(); + //allow the sound's own radius to override hearingRange, if it is set + if ( gameLocal.lastAIAlertRadius ) { + if ( dist < Square( gameLocal.lastAIAlertRadius ) ) { + SetEnemyPosition(); + } + } else if ( dist < Square( hearingRange ) ) { + SetEnemyPosition(); + } + } + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorLtGrey, enemyEnt->GetPhysics()->GetBounds(), lastReachableEnemyPos, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorWhite, enemyEnt->GetPhysics()->GetBounds(), lastVisibleReachableEnemyPos, gameLocal.msec ); + } +} + +void hhMonsterAI::BecameBound(hhBindController *b) { + SetState( GetScriptFunction( "state_Nothing" ) ); +} + +void hhMonsterAI::BecameUnbound(hhBindController *b) { + if ( health > 0 ) { + SetState( GetScriptFunction( "state_Idle" ) ); + } +} + +void hhMonsterAI::CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ) { + const trace_t& trace = physicsObj.GetGroundTrace(); + if ( af.IsActive() || (!physicsObj.HasGroundContacts() || trace.fraction == 1.0f) && !IsBound() ) { + return; + } + + //aob - only check when we land on the ground + //If we get here we can assume we currently have ground contacts + if( physicsObj.HadGroundContacts() ) { + return; + } + + // if the monster wasn't going down + if ( ( oldVelocity * -physicsObj.GetGravityNormal() ) >= 0.0f ) { + return; + } + + idVec3 deltaVelocity = DetermineDeltaCollisionVelocity( oldVelocity, trace ); + float delta = (IsBound()) ? deltaVelocity.Length() : deltaVelocity * physicsObj.GetGravityNormal(); + + if ( delta < spawnArgs.GetFloat( "fatal_fall_velocity", "900" ) ) { + return; // Early out + } + if( trace.fraction == 1.0f ) { + return; + } + Damage( NULL, NULL, oldVelocity.ToNormal(), "damage_monsterfall", 1, INVALID_JOINT ); +} + +hhEntityFx* hhMonsterAI::SpawnFxLocal( const char *fxName, const idVec3 &origin, const idMat3& axis, const hhFxInfo* const fxInfo, bool forceClient ) { + //overridden to use GetJointWorldTransform to set location if binding to a bone + idDict fxArgs; + hhEntityFx * fx = NULL; + + if ( g_skipFX.GetBool() ) { + return NULL; + } + + if( !fxName || !fxName[0] ) { + return NULL; + } + + // Spawn an fx + fxArgs.Set( "fx", fxName ); + fxArgs.SetBool( "start", fxInfo ? fxInfo->StartIsSet() : true ); + fxArgs.SetVector( "origin", origin ); + fxArgs.SetMatrix( "rotation", axis ); + //HUMANHEAD: aob + if( fxInfo ) { + fxArgs.SetBool( "removeWhenDone", fxInfo->RemoveWhenDone() ); + fxArgs.SetBool( "onlyVisibleInSpirit", fxInfo->OnlyVisibleInSpirit() ); // CJR + fxArgs.SetBool( "onlyInvisibleInSpirit", fxInfo->OnlyInvisibleInSpirit() ); // tmj + fxArgs.SetBool( "toggle", fxInfo->Toggle() ); + } + //HUMANHEAD END + + //HUMANHEAD rww - use forceClient + if (forceClient) { + //this can happen on the "server" in the case of listen servers as well + fx = (hhEntityFx *)gameLocal.SpawnClientObject( "func_fx", &fxArgs ); + } + else { + assert(!gameLocal.isClient); + fx = (hhEntityFx *)gameLocal.SpawnObject( "func_fx", &fxArgs ); + } + if( fxInfo ) { + fx->SetFxInfo( *fxInfo ); + } + + if( fxInfo && fxInfo->EntityIsSet() ) { + fx->fl.noRemoveWhenUnbound = fxInfo->NoRemoveWhenUnbound(); + if( fxInfo->BindBoneIsSet() ) { + idVec3 bonePos; + idMat3 boneAxis; + GetJointWorldTransform( fxInfo->GetBindBone(), bonePos, boneAxis ); + fx->SetOrigin( bonePos ); + fx->BindToJoint( fxInfo->GetEntity(), fxInfo->GetBindBone(), true ); + } else if( fx && fx->Joint() && *fx->Joint() ) { + fx->MoveToJoint( fxInfo->GetEntity(), fx->Joint() ); + fx->BindToJoint( fxInfo->GetEntity(), fx->Joint(), true ); + } else { + fx->Bind( fxInfo->GetEntity(), true ); + } + } + + fx->Show(); + + return fx; +} + +bool hhMonsterAI::AttackMelee( const char *meleeDefName ) { + const idDict *meleeDef; + idActor *enemyEnt = enemy.GetEntity(); + const char *p; + const idSoundShader *shader; + + meleeDef = gameLocal.FindEntityDefDict( meleeDefName, false ); + if ( !meleeDef ) { + gameLocal.Error( "Unknown melee '%s'", meleeDefName ); + } + + if ( !enemyEnt ) { + p = meleeDef->GetString( "snd_miss" ); + if ( p && *p ) { + shader = declManager->FindSound( p ); + StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL ); + } + return false; + } + + // make sure the trace can actually hit the enemy + if ( !TestMeleeDef(meleeDefName) ) { + // missed + p = meleeDef->GetString( "snd_miss" ); + if ( p && *p ) { + shader = declManager->FindSound( p ); + StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL ); + } + return false; + } + + // + // do the damage + // + p = meleeDef->GetString( "snd_hit" ); + if ( p && *p ) { + shader = declManager->FindSound( p ); + StartSoundShader( shader, SND_CHANNEL_DAMAGE, 0, false, NULL ); + } + + idVec3 kickDir; + meleeDef->GetVector( "kickDir", "0 0 0", kickDir ); + + idVec3 globalKickDir; + globalKickDir = ( viewAxis * physicsObj.GetGravityAxis() ) * kickDir; + + if ( spawnArgs.GetBool( "smart_knockback", "0" ) ) { + //do some traces to determine which dir to kick enemy + idAngles ang = viewAxis.ToAngles(); + idVec3 forward, right, up; + ang.ToVectors( &forward, &right, &up ); + trace_t trace; + gameLocal.clip.TraceBounds( trace, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + -forward * 400, enemyEnt->GetPhysics()->GetBounds(), MASK_SOLID, this ); + if ( trace.fraction != 1.0f ) { + //there's room to kick the player back + globalKickDir = -forward; + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + globalKickDir * 500, 10, 10000 ); + } + } else { + gameLocal.clip.TraceBounds( trace, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + right * 400, enemyEnt->GetPhysics()->GetBounds(), MASK_SOLID, this ); + if ( trace.fraction != 1.0f ) { + globalKickDir = right; + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + globalKickDir * 500, 10, 10000 ); + } + } else { + gameLocal.clip.TraceBounds( trace, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + right * 400, enemyEnt->GetPhysics()->GetBounds(), MASK_SOLID, this ); + if ( trace.fraction != 1.0f ) { + globalKickDir = -right; + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, enemyEnt->GetOrigin(), enemyEnt->GetOrigin() + globalKickDir * 500, 10, 10000 ); + } + } + } + } + } + + enemyEnt->Damage( this, this, globalKickDir, meleeDefName, 1.0f, INVALID_JOINT ); + + //swap skin if we have successfully hit + idStr hitSkin = spawnArgs.GetString( "skin_melee_hit", "" ); + if ( hitSkin.Length() ) { + SetSkinByName( hitSkin.c_str() ); + } + + lastAttackTime = gameLocal.time; + + return true; +} + +bool hhMonsterAI::TestMeleeDef( const char *meleeDefName ) const { + //tests using "melee_boundmin" and "melee_boundmax" in the damage def + trace_t trace; + idActor *enemyEnt = enemy.GetEntity(); + const idDict *meleeDef; + + meleeDef = gameLocal.FindEntityDefDict( meleeDefName, false ); + if ( !meleeDef ) { + gameLocal.Error( "Unknown melee '%s'", meleeDefName ); + } + + if ( !enemyEnt || !melee_range ) { + return false; + } + + //FIXME: make work with gravity vector + idVec3 org = physicsObj.GetOrigin(); + const idBounds &myBounds = physicsObj.GetBounds(); + idBounds bounds; + + idBounds meleeBounds( meleeDef->GetVector( "melee_boundmin" ), meleeDef->GetVector( "melee_boundmax" ) ); + + if ( meleeBounds != bounds_zero ) { + //check custom rotated meleebound + idBox meleeBox( meleeBounds, org, renderEntity.axis ); + idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds(); + enemyBounds.TranslateSelf( enemyEnt->GetPhysics()->GetOrigin() ); + idBox enemyBox( enemyBounds ); + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBox( colorYellow, meleeBox, 5000 ); + } + + if ( !enemyBox.IntersectsBox( meleeBox ) ) { + return false; + } + } else { + // expand the bounds out by our melee range + bounds[0][0] = -melee_range; + bounds[0][1] = -melee_range; + bounds[0][2] = myBounds[0][2] - 4.0f; + bounds[1][0] = melee_range; + bounds[1][1] = melee_range; + bounds[1][2] = myBounds[1][2] + 4.0f; + bounds.TranslateSelf( org ); + + idVec3 enemyOrg = enemyEnt->GetPhysics()->GetOrigin(); + idBounds enemyBounds = enemyEnt->GetPhysics()->GetBounds(); + enemyBounds.TranslateSelf( enemyOrg ); + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugBounds( colorYellow, bounds, vec3_zero, gameLocal.msec ); + } + + if ( !bounds.IntersectsBounds( enemyBounds ) ) { + return false; + } + } + + idVec3 start = GetEyePosition(); + idVec3 end = enemyEnt->GetEyePosition(); + + gameLocal.clip.TracePoint( trace, start, end, MASK_SHOT_BOUNDINGBOX, this ); + if ( ( trace.fraction == 1.0f ) || ( gameLocal.GetTraceEntity( trace ) == enemyEnt ) ) { + return true; + } + + return false; +} + +void hhMonsterAI::PrintDebug() { + if ( enemy.IsValid() ) { + int dist = int(( GetPhysics()->GetOrigin() - enemy->GetPhysics()->GetOrigin() ).LengthFast()); + gameLocal.Printf( " Enemy: %s\n", enemy->GetName() ); + gameLocal.Printf( " Distance to Enemy: %d\n", dist ); + gameLocal.Printf( " Enemy Area: %i\n", PointReachableAreaNum(enemy->GetOrigin()) ); + gameLocal.Printf( " Enemy Reachable: %s\n", AI_ENEMY_REACHABLE ? "yes" : "no" ); + gameLocal.Printf( " Enemy Visible: %s\n", AI_ENEMY_VISIBLE ? "yes" : "no" ); + } else { + gameLocal.Printf( " Enemy: None\n" ); + } + gameLocal.Printf( " Current Area: %i\n", PointReachableAreaNum(GetOrigin()) ); + if ( state ) { + gameLocal.Printf( " State: %s\n", state->Name() ); + } + gameLocal.Printf( " Health: %i/%i\n", GetHealth(), spawnArgs.GetInt( "health" ) ); + gameLocal.Printf( " Torso State: %s\n", torsoAnim.state.c_str() ); + gameLocal.Printf( " Legs State: %s\n", legsAnim.state.c_str() ); + gameLocal.Printf( " IK: %s\n", walkIK.IsActivated() ? "enable" : "disabled" ); + gameLocal.Printf( " Turnrate: %f\n", turnRate ); +} + +bool hhMonsterAI::NewWanderDir( const idVec3 &dest ) { + if ( spawnArgs.GetInt( "wander_radius" ) ) { + //pick a new dest based on radius from starting origin + idVec3 offset = hhUtils::RandomVector() * float(spawnArgs.GetInt( "wander_radius" )); + offset.z = 0.0f; + const idVec3 newDest = spawnOrigin + offset; + return idAI::NewWanderDir( newDest ); + } else { + return idAI::NewWanderDir( dest ); + } +} + +/* +===================== +hhMonsterAI::Save +===================== +*/ +void hhMonsterAI::Save( idSaveGame *savefile ) const { + savefile->WriteInt( targetReaction.reactionIndex ); + targetReaction.entity.Save( savefile); + shootTarget.Save( savefile ); + currPassageway.Save( savefile ); + + savefile->WriteBool( bCanFall ); + + int i, num = healthTriggers.Num(); + savefile->WriteInt( num ); + for (i = 0; i < num; i++) { + savefile->WriteInt( healthTriggers[i].healthThresh ); + healthTriggers[i].triggerEnt.Save( savefile ); + savefile->WriteInt( healthTriggers[i].triggerCount ); + } + savefile->WriteInt( hearingRange ); + savefile->WriteInt( lastContactTime ); + savefile->WriteBool( bSeeThroughPortals ); + savefile->WriteBool( bBossBar ); + savefile->WriteInt( nextSpeechTime ); + savefile->WriteAngles( lookOffset ); + savefile->WriteVec3( spawnOrigin ); + savefile->WriteBool( bBindOrient ); + savefile->WriteInt( numDDADamageSamples ); + savefile->WriteFloat( totalDDADamage ); + savefile->WriteInt( spawnThinkFlags ); + savefile->WriteBool( bCanWallwalk ); + savefile->WriteBool( bOverrideKilledByGravityZones ); + savefile->WriteInt( fallDelay ); + savefile->WriteBool( soundOnModel ); + savefile->WriteBool( bBindAxis ); + savefile->WriteBool( bCustomBlood ); + savefile->WriteBool( bNoCombat ); + savefile->WriteBool( bNeverTarget ); + savefile->WriteInt( nextSpiritProxyCheck ); +}; + +/* +===================== +hhMonsterAI::Restore +===================== +*/ +void hhMonsterAI::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( targetReaction.reactionIndex ); + targetReaction.entity.Restore( savefile); + + shootTarget.Restore( savefile ); + currPassageway.Restore( savefile ); + + savefile->ReadBool( bCanFall ); + + int i, num; + savefile->ReadInt( num ); + healthTriggers.SetNum( num ); + for (i = 0; i < num; i++) { + savefile->ReadInt( healthTriggers[i].healthThresh ); + healthTriggers[i].triggerEnt.Restore( savefile ); + savefile->ReadInt( healthTriggers[i].triggerCount ); + } + savefile->ReadInt( hearingRange ); + savefile->ReadInt( lastContactTime ); + savefile->ReadBool( bSeeThroughPortals ); + savefile->ReadBool( bBossBar ); + savefile->ReadInt( nextSpeechTime ); + savefile->ReadAngles( lookOffset ); + savefile->ReadVec3( spawnOrigin ); + savefile->ReadBool( bBindOrient ); + savefile->ReadInt( numDDADamageSamples ); + savefile->ReadFloat( totalDDADamage ); + savefile->ReadInt( spawnThinkFlags ); + savefile->ReadBool( bCanWallwalk ); + savefile->ReadBool( bOverrideKilledByGravityZones ); + savefile->ReadInt( fallDelay ); + savefile->ReadBool( soundOnModel ); + savefile->ReadBool( bBindAxis ); + savefile->ReadBool( bCustomBlood ); + savefile->ReadBool( bNoCombat ); + savefile->ReadBool( bNeverTarget ); + savefile->ReadInt( nextSpiritProxyCheck ); + + allSimpleMonsters.AddUnique(this); + + nextTurnUpdate = 0; + + if ( AI_VEHICLE ) { + if ( GetVehicleInterface() ) { + ResetClipModel(); + hhVehicle *vehicle = GetVehicleInterface()->GetVehicle(); + spawnArgs.Set( "use_aas", spawnArgs.GetString( "aas_shuttle", "aasDroid" ) ); + SetAAS(); + SetState( GetScriptFunction( "state_VehicleCombat" ) ); + } + } +}; + +/* +===================== +hhMonsterAI::GetTurnDir +Returns -1 if left, 0 if facing ideal, and 1 if turning right +===================== +*/ +int hhMonsterAI::GetTurnDir( void ) { + float diff; + + if ( !turnRate ) { + return 0; + } + + diff = idMath::AngleNormalize180( current_yaw - ideal_yaw ); + if ( idMath::Fabs( diff ) < 0.01f ) { + // force it to be exact + current_yaw = ideal_yaw; + return 0; + } + + return (diff < 0 ? -1 : 1); +} + + + +//============================================================================= +// +// hhMonsterAI::SendDamageToDDA() +// +// Informs the player of the amount of damage inflicted upon it until this +// creature was killed. +//============================================================================= + +void hhMonsterAI::SendDamageToDDA() { + if ( gameLocal.isMultiplayer ) { + return; + } + + int ddaIndex = spawnArgs.GetInt( "ddaIndex", "0" ); + + if ( gameLocal.GetDDA() && ddaIndex >= 0 ) { // Only include certain creatures in the DDA calculation (exclude such things as crawlers and NPCs) + gameLocal.GetDDA()->DDA_AddDamage( ddaIndex, totalDDADamage ); + gameLocal.GetDDA()->DDA_AddSurvivalHealth( ddaIndex, gameLocal.GetLocalPlayer()->GetHealth() ); + } + + totalDDADamage = 0; +} + +void hhMonsterAI::FlyMove( void ) { + idVec3 goalPos; + idVec3 oldorigin; + idVec3 newDest; + + AI_BLOCKED = false; + if ( ( move.moveCommand != MOVE_NONE ) && ReachedPos( move.moveDest, move.moveCommand ) ) { + StopMove( MOVE_STATUS_DONE ); + } + + if ( move.moveCommand != MOVE_TO_POSITION_DIRECT ) { + idVec3 vel = physicsObj.GetLinearVelocity(); + + if ( GetMovePos( goalPos ) ) { + CheckObstacleAvoidance( goalPos, newDest ); + goalPos = newDest; + } + + if ( !AI_FLY_NO_SEEK && move.speed ) { + FlySeekGoal( vel, goalPos ); + } + + // add in bobbing + AddFlyBob( vel ); + + if ( enemy.GetEntity() && ( move.moveCommand != MOVE_TO_POSITION ) ) { + AdjustFlyHeight( vel, goalPos ); + } + + AdjustFlySpeed( vel ); + + physicsObj.SetLinearVelocity( vel ); + } + + // turn + FlyTurn(); + + // run the physics for this frame + oldorigin = physicsObj.GetOrigin(); + physicsObj.UseFlyMove( true ); + physicsObj.UseVelocityMove( false ); + physicsObj.SetDelta( vec3_zero ); + physicsObj.ForceDeltaMove( disableGravity ); + RunPhysics(); + + monsterMoveResult_t moveResult = physicsObj.GetMoveResult(); + if ( !af_push_moveables && attack.Length() && TestMelee() ) { + DirectDamage( attack, enemy.GetEntity() ); + } else { + idEntity *blockEnt = physicsObj.GetSlideMoveEntity(); + if ( blockEnt && blockEnt->IsType( idMoveable::Type ) && blockEnt->GetPhysics()->IsPushable() ) { + KickObstacles( viewAxis[ 0 ], kickForce, blockEnt ); + } else if ( moveResult == MM_BLOCKED ) { + move.blockTime = gameLocal.time + 500; + AI_BLOCKED = true; + } + } + + idVec3 org = physicsObj.GetOrigin(); + if ( oldorigin != org ) { + TouchTriggers(); + } + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugLine( colorCyan, oldorigin, physicsObj.GetOrigin(), 4000 ); + gameRenderWorld->DebugBounds( colorOrange, physicsObj.GetBounds(), org, gameLocal.msec ); + gameRenderWorld->DebugBounds( colorMagenta, physicsObj.GetBounds(), move.moveDest, gameLocal.msec ); + gameRenderWorld->DebugLine( colorRed, org, org + physicsObj.GetLinearVelocity(), gameLocal.msec, true ); + gameRenderWorld->DebugLine( colorBlue, org, goalPos, gameLocal.msec, true ); + gameRenderWorld->DebugLine( colorYellow, org + EyeOffset(), org + EyeOffset() + viewAxis[ 0 ] * physicsObj.GetGravityAxis() * 16.0f, gameLocal.msec, true ); + DrawRoute(); + } +} + +void hhMonsterAI::Activate( idEntity *activator ) { + if ( spawnThinkFlags != 0 ) { + int temp = spawnThinkFlags; + spawnThinkFlags = 0; + BecomeActive( temp ); + } + idAI::Activate( activator ); +} + +void hhMonsterAI::Hide() { + if ( spawnThinkFlags == 0 && ai_hideSkipThink.GetBool() ) { + if ( ( spawnArgs.GetBool( "hide" ) || spawnArgs.GetBool( "portal" ) ) ) { + spawnThinkFlags = thinkFlags; + thinkFlags = 0; + } + } + idAI::Hide(); +} + +void hhMonsterAI::HideNoDormant() { + idAI::Hide(); +} + +void hhMonsterAI::BecomeActive( int flags ) { + if ( spawnThinkFlags != 0 ) { + spawnThinkFlags |= flags; + return; + } + idAI::BecomeActive( flags ); +} + +void hhMonsterAI::HandleNoGore(void) { + if (GERMAN_VERSION || g_nogore.GetBool()) { + fl.takedamage = false; + fl.canBeTractored = false; + GetPhysics()->SetContents(0); + float time = spawnArgs.GetFloat("nogore_dispose_time", "1"); + if (time > 0.0f) { + //PostEventSec(&EV_Dispose, time); + } + } +} + +bool hhMonsterAI::GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis ) { + if ( soundOnModel ) { + idVec3 boneOrigin; + idMat3 boneAxis; + jointHandle_t bone = animator.GetJointHandle( spawnArgs.GetString( "sound_joint", "b1" ) ); + if ( bone != INVALID_JOINT && animator.GetJointTransform( bone, gameLocal.time, boneOrigin, boneAxis ) ) { + origin = boneOrigin; + axis = boneAxis * viewAxis; + return true; + } + } + return idAI::GetPhysicsToSoundTransform( origin, axis ); +} + +void hhMonsterAI::AddLocalMatterWound( jointHandle_t jointNum, const idVec3 &localOrigin, const idVec3 &localNormal, const idVec3 &localDir, int damageDefIndex, const idMaterial *collisionMaterial ) { + if ( bCustomBlood ) { + return; + } + + const idDeclEntityDef *def = static_cast( declManager->DeclByIndex( DECL_ENTITYDEF, damageDefIndex ) ); + if ( def == NULL ) { + return; + } + + surfTypes_t matterType = gameLocal.GetMatterType( this, collisionMaterial, "idEntity::AddLocalMatterWound" ); + + GetWoundManager()->AddWounds( def, matterType, jointNum, localOrigin, localNormal, localDir ); + + if ( head.IsValid() ) { + head->AddLocalMatterWound( jointNum, localOrigin, localNormal, localDir, damageDefIndex, collisionMaterial ); + } + //TODO: Grab head projection code from below and put into ApplyImpactMark() +} diff --git a/src/Prey/game_monster_ai.h b/src/Prey/game_monster_ai.h new file mode 100644 index 0000000..de62c90 --- /dev/null +++ b/src/Prey/game_monster_ai.h @@ -0,0 +1,262 @@ + +#ifndef __PREY_MONSTER_AI_H__ +#define __PREY_MONSTER_AI_H__ + +extern const idEventDef MA_AttackMissileEx; +extern const idEventDef MA_FindReaction; +extern const idEventDef MA_InitialWallwalk; +extern const idEventDef MA_EnemyOnSpawn; +extern const idEventDef MA_SetVehicleState; +extern const idEventDef MA_OnProjectileLaunch; +extern const idEventDef MA_OnProjectileHit; +extern const idEventDef MA_FallNow; +extern const idEventDef MA_EnemyIsSpirit; +extern const idEventDef MA_EnemyIsPhysical; +extern const idEventDef MA_EnemyPortal; + +// +// hhNoClipEnt +// +// Dummy entity that sets contents to 0 - created for use for ForceAAS entities +// +class hhNoClipEnt : public idEntity { +public: + CLASS_PROTOTYPE(hhNoClipEnt); + + void Spawn( void ); +}; + +class hhAINode : public idEntity { +public: + CLASS_PROTOTYPE( hhAINode ); + hhAINode(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + idEntityPtr< idEntity> user; +}; + +//! Somewhat of a hack. This should in an a .h file someplace. +// Not in a .cpp file as id has it now. +//#define CONDITION( name ) name.PointTo( scriptObject, #name ) + +struct hhMonsterReaction { + hhMonsterReaction(void) : reactionIndex(-1) { } + inline hhReaction *GetReaction() const { return ( entity.IsValid() && reactionIndex >= 0 ? entity->GetReaction(reactionIndex) : NULL ); } + int reactionIndex; + idEntityPtr entity; +}; + +// +// hhMonsterHealthTrigger +// +struct hhMonsterHealthTrigger { + hhMonsterHealthTrigger() {healthThresh = 0; triggerEnt = NULL; triggerCount = 0;} + + int healthThresh; // When our health drops below this value, trigger our ent + idEntityPtr triggerEnt; // The entity to trigger + int triggerCount; // Number of times we have tripped this trigger +}; + +class hhMonsterAI : public idAI { + +public: + CLASS_PROTOTYPE(hhMonsterAI); + + hhMonsterAI(); + ~hhMonsterAI(); + virtual void Think(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + const char * GetJointForFrameCommand( const char *cmd ) { idStr tmp( cmd ); int i = tmp.Find( " " ); if ( i < 0 ) { return( va( "%s", cmd ) ); } else { return( va( "%s", tmp.Left( i ).c_str() ) ); } }; + void Spawn(); + virtual void LinkScriptVariables(void); + bool HasPathTo(const idVec3 &destPt); + void ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &force ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void EnterVehicle( hhVehicle* vehicle ); + virtual bool TestMelee( void ) const; + virtual bool MoveToPosition( const idVec3 &pos, bool enemyBlocks = false ); + virtual bool TurnToward( const idVec3 &pos ); + ID_INLINE virtual bool TurnToward( float yaw ) { return idAI::TurnToward( yaw ); } // HUMANHEAD mdl: Needed because of bizarre inheritance issue that resulted in TurnToward(idVec3) being called + virtual bool CanSee( idEntity *ent, bool useFov ); + idPlayer* GetClosestPlayer( void ); + void Show(); + bool GetFacePosAngle( const idVec3 &pos, float &delta ); + virtual void SetEnemy( idActor *newEnemy ); //made public because hhAI does + void Pickup( hhPlayer *player ); + bool GiveToPlayer( hhPlayer* player ); + idVec3 GetTouchPos(idEntity *ent, const hhReactionDesc *desc ); + void UpdateFromPhysics( bool moveBack ); + virtual bool GetTouchPosBound( const hhReactionDesc *desc, idBounds& bounds ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + void CreateHealthTriggers(); + void UpdateHealthTriggers(int oldHealth, int currHealth); + int ReactionTo( const idEntity *ent ); + virtual bool UpdateAnimationControllers( void ); + virtual bool ShouldTouchTrigger( idEntity* entity ) const { return fl.touchTriggers; } + virtual void FlyTurn( void ); + bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); + void UpdateEnemyPosition( void ); + void BecameBound(hhBindController *b); + void BecameUnbound(hhBindController *b); + void UpdateModelTransform(); + virtual // HUMANHEAD CJR + void CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ); + hhEntityFx* SpawnFxLocal( const char *fxName, const idVec3 &origin, const idMat3& axis = mat3_identity, const hhFxInfo* const fxInfo = NULL, bool forceClient = false ); + bool AttackMelee( const char *meleeDefName ); + bool TestMeleeDef( const char *meleeDefName ) const; + virtual void PrintDebug(); + int GetTurnDir(); + bool FacingEnemy( float range ); + virtual void Activate( idEntity *activator ); + void BecomeActive( int flags ); + bool OverrideKilledByGravityZones() { return bOverrideKilledByGravityZones; } + virtual void Hide(); + virtual void HideNoDormant(); + void AddLocalMatterWound( jointHandle_t jointNum, const idVec3 &localOrigin, const idVec3 &localNormal, const idVec3 &localDir, int damageDefIndex, const idMaterial *collisionMaterial ); + ID_INLINE virtual bool IsDead() { return AI_DEAD != 0; } + bool Pushes() { return( pushes && move.moveCommand != MOVE_NONE ); } // only push if moving + void GravClipModelAxis( bool enable ) { physicsObj.SetGravClipModelAxis( enable ); } + virtual void Distracted( idActor *newEnemy ); + +// Events. + virtual void Event_PostSpawn(); + void Event_AttackMissileEx(const char *params, int boneDir); + virtual void Event_AttackMissile( const char *jointname, const idDict *projDef, int boneDir ); + void Event_SetMeleeRange( float newMeleeRange ); + virtual void Event_FindReaction( const char* effect ); + virtual void Event_UseReaction(); + void Event_EnemyOnSide(); + void Event_HitCheck( idEntity *ent, const char *animname ); + void Event_CreateMonsterPortal(); + void Event_GetShootTarget(); + void Event_TriggerReactEnt(); + virtual void Event_Remove( void ); + void Event_OnProjectileLaunch(hhProjectile *proj); + void Event_InitialWallwalk( void ); + void Event_GetVehicle(); + void Event_EnemyAimingAtMe(); + void Event_ReachedEntity( idEntity *ent ); + void Event_EnemyOnSpawn(); + void Event_SpawnFX( char *fxFile ); + void Event_SplashDamage(char *damage); + void Event_SetVehicleState(); + void Event_TestAnimMoveTowardEnemy( const char *animname ); + void Event_GetLastReachableEnemyPos(); + void Event_FollowPath( const char *pathName ); + void Event_EnemyIsA( const char* testclass ); + void Event_Subtitle( idList* parmList ); + void Event_SubtitleOff(); + void Event_EnableHeadlook(); + void Event_DisableHeadlook(); + void Event_EnableEyelook(); + void Event_DisableEyelook(); + void Event_FacingEnemy(float range); + void Event_BossBar( int onOff ); + virtual void Event_GetCombatNode(); + void Event_FallNow(); + void Event_AllowFall( int allowFall ); + virtual void Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ); + virtual void Event_EnemyIsPhysical( hhPlayer *player, hhSpiritProxy *proxy ); + void Event_InPlayerFov(); + void Event_IsRagdoll(); + void Event_MoveDone(); + void Event_SetShootTarget(idEntity *ent); + void Event_EnemyInGravityZone(void); + void Event_LookAtEntity( idEntity *ent, float duration ); + void Event_LookAtEnemy( float duration ); + void Event_SetLookOffset( idAngles const &ang ); + void Event_SetHeadFocusRate( float rate ); + void Event_HeardSound( int ignore_team ); + virtual void Event_FlyZip(); + void Event_UseConsole( idEntity *ent ); + void Event_TestMeleeDef( const char *meleeDefName ) const; + void Event_EnemyInVehicle(); + void Event_EnemyOnWallwalk(); + void Event_AlertAI( idEntity *ent, float radius ); + void Event_TestAnimMoveBlocked( const char *animname ); + void Event_InGravityZone(); + void Event_StartSoundDelay( const char *soundName, int channel, int netSync, float delay ); + void Event_SetTeam( int new_team ); + virtual void Event_GetAttackPoint(); + void Event_HideNoDormant(); + void Event_SoundOnModel(); + void Event_ActivatePhysics(); + void Event_IsVehicleDocked(); + void Event_SetNeverDormant( int enable ); + void Event_EnemyInSpirit(); + void Event_EnemyPortal( idEntity *ent ); + + // CJR DDA TEST + int numDDADamageSamples; + float totalDDADamage; + + void DamagedPlayer( int damage ) { numDDADamageSamples++; totalDDADamage += damage; } + void SendDamageToDDA(); + // END CJR DDA TEST + + //passageway code + bool IsInsidePassageway(void) const {return currPassageway != NULL;} + hhAIPassageway* GetCurrPassageway(void) {return currPassageway.GetEntity();} +public: + idScriptBool AI_HAS_RANGE_ATTACK; // TRUE if this monster has a range attack (required for reaction system) + idScriptBool AI_HAS_MELEE_ATTACK; // TRUE if this monster has a melee attack (required for reaction system) + idScriptBool AI_USING_REACTION; // TRUE if monster is currently using reaction + idScriptBool AI_REACTION_FAILED; // TRUE if the last reaction attempt failed (path blocked, exclusive problem, etc) + idScriptBool AI_REACTION_ANIM; // TRUE if the current reaction is using an anim to 'cause' + idScriptBool AI_BACKWARD; + idScriptBool AI_STRAFE_LEFT; + idScriptBool AI_STRAFE_RIGHT; + idScriptBool AI_UPWARD; + idScriptBool AI_DOWNWARD; + idScriptBool AI_SHUTTLE_DOCKED; + idScriptBool AI_VEHICLE_ATTACK; + idScriptBool AI_VEHICLE_ALT_ATTACK; + idScriptBool AI_WALLWALK; + idScriptBool AI_FALLING; + idScriptBool AI_PATHING; + idScriptFloat AI_TURN_DIR; + idScriptBool AI_FLY_NO_SEEK; + idScriptBool AI_FOLLOWING_PATH; + + bool bNeverTarget; + static idList allSimpleMonsters; // Global list of all hhMonsterAI's currently created +protected: + bool NearEnoughTouchPos( idEntity* ent, const idVec3& targetPos, idBounds& bounds ); + bool CheckValidReaction(); + void FinishReaction( bool bFailed = false ); + virtual int EvaluateReaction( const hhReaction *react ); + bool NewWanderDir( const idVec3 &dest ); + void FlyMove(); + virtual void HandleNoGore(); + bool GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis ); +protected: + hhMonsterReaction targetReaction; // Info about our current reaction + idEntityPtr shootTarget; + idEntityPtr currPassageway; // The passage node this monster is currently in + idList healthTriggers; + int lastContactTime; + int hearingRange; + int nextSpeechTime; + idAngles lookOffset; + int nextTurnUpdate; + idVec3 spawnOrigin; //location of where this monster spawned + int spawnThinkFlags; + int fallDelay; + bool bCanFall; + bool bSeeThroughPortals; + bool bBossBar; + bool bBindOrient; //orient monster to its bindmaster + bool bCanWallwalk; + bool bOverrideKilledByGravityZones; + bool soundOnModel; + bool bBindAxis; + bool bCustomBlood; + bool bNoCombat; + int nextSpiritProxyCheck; +}; + + +#endif /* __PREY_MONSTER_AI_H__ */ + + diff --git a/src/Prey/game_monster_ai_events.cpp b/src/Prey/game_monster_ai_events.cpp new file mode 100644 index 0000000..d973af0 --- /dev/null +++ b/src/Prey/game_monster_ai_events.cpp @@ -0,0 +1,1302 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef MA_AttackMissileEx("", "sd",NULL); +const idEventDef MA_SetMeleeRange("setMeleeRange", "f",NULL); +const idEventDef MA_FindReaction("findReaction", "s", 'E'); +const idEventDef MA_UseReaction("useReaction"); +const idEventDef MA_EnemyOnSide("enemyOnSide", NULL, 'f'); +const idEventDef MA_HitCheck( "hitCheck", "Es", 'd' ); +const idEventDef MA_CreateMonsterPortal("createMonsterPortal", NULL, 'f'); +const idEventDef MA_GetShootTarget("getShootTarget", NULL,'E'); +const idEventDef MA_TriggerReactEnt("triggerReactEnt"); +const idEventDef MA_InitialWallwalk(""); +const idEventDef MA_GetVehicle("getVehicle",NULL,'E'); +const idEventDef MA_EnemyAimingAtMe("enemyAimingAtMe",NULL,'d'); +const idEventDef MA_ReachedEntity("reachedEntity","e",'d'); +const idEventDef MA_EnemyOnSpawn("",NULL,NULL); +const idEventDef MA_SpawnFX( "spawnFX", "s" ); +const idEventDef MA_SplashDamage( "splashDamage", "s" ); +const idEventDef MA_SetVehicleState( "",NULL,NULL ); +const idEventDef MA_FollowPath( "followPath", "s", NULL ); +const idEventDef MA_GetLastReachableEnemyPos( "getLastReachableEnemyPos", "", 'v' ); +const idEventDef MA_OnProjectileLaunch("", "e"); +const idEventDef MA_EnemyIsA("enemyIsA", "s", 'd'); +const idEventDef MA_Subtitle("subtitle", "d"); +const idEventDef MA_SubtitleOff(""); +const idEventDef MA_EnableHeadlook("enableHeadlook"); +const idEventDef MA_DisableHeadlook("disableHeadlook"); +const idEventDef MA_EnableEyelook("enableEyelook"); +const idEventDef MA_DisableEyelook("disableEyelook"); +const idEventDef MA_FacingEnemy("facingEnemy", "f", 'd'); +const idEventDef MA_BossBar("bossBar", "d"); +const idEventDef MA_FallNow(""); +const idEventDef MA_AllowFall("allowFall", "d"); //HUMANHEAD jsh PCF 5/3/06 Changed parameter from "f" to "d" +const idEventDef MA_InPlayerFov("inPlayerFov", NULL, 'd'); +const idEventDef MA_EnemyIsSpirit("", "ee"); +const idEventDef MA_EnemyIsPhysical("", "ee"); +const idEventDef MA_IsRagdoll("isRagdoll", NULL, 'd'); +const idEventDef MA_MoveDone("moveDone", NULL, 'd'); +const idEventDef MA_SetShootTarget("setShootTarget", "E"); +const idEventDef EV_EnemyInGravityZone( "enemyInGravityZone", NULL, 'f' ); +const idEventDef MA_SetLookOffset( "setLookOffset", "v" ); +const idEventDef MA_SetHeadFocusRate( "setHeadFocusRate", "f" ); +const idEventDef MA_FlyZip( "flyZip" ); +const idEventDef MA_UseConsole( "useConsole", "e" ); +const idEventDef MA_TestMeleeDef("testMeleeDef", "s",'f'); +const idEventDef MA_EnemyInVehicle("enemyInVehicle", NULL, 'd'); +const idEventDef MA_EnemyOnWallwalk("enemyOnWallwalk", NULL, 'd'); +const idEventDef MA_AlertAI("alertAI", "ef" ); +const idEventDef MA_TestAnimMoveBlocked("testAnimMoveBlocked", "s", 'e'); +const idEventDef MA_InGravityZone("inGravityZone", NULL, 'd'); +const idEventDef MA_StartSoundDelay( "startSoundDelay", "sddf", 'f' ); +const idEventDef MA_SetTeam( "setTeam", "d" ); +const idEventDef MA_GetAttackPoint ("getAttackPoint", NULL, 'v'); +const idEventDef MA_HideNoDormant("hideNoDormant" ); +const idEventDef MA_SoundOnModel("soundOnModel"); +const idEventDef MA_ActivatePhysics("activatePhysics"); +const idEventDef MA_IsVehicleDocked("isVehicleDocked"); +const idEventDef MA_EnemyInSpirit( "enemyInSpirit", NULL, 'd' ); +const idEventDef MA_GetAttackNode( "getAttackNode", NULL, 'v' ); + +CLASS_DECLARATION(idAI, hhMonsterAI) + EVENT( MA_AttackMissileEx, hhMonsterAI::Event_AttackMissileEx ) + EVENT( MA_FindReaction, hhMonsterAI::Event_FindReaction ) + EVENT( MA_UseReaction, hhMonsterAI::Event_UseReaction ) + EVENT( MA_SetMeleeRange, hhMonsterAI::Event_SetMeleeRange ) + EVENT( MA_EnemyOnSide, hhMonsterAI::Event_EnemyOnSide ) + EVENT( MA_HitCheck, hhMonsterAI::Event_HitCheck ) + EVENT( MA_CreateMonsterPortal, hhMonsterAI::Event_CreateMonsterPortal ) + EVENT( MA_GetShootTarget, hhMonsterAI::Event_GetShootTarget ) + EVENT( MA_TriggerReactEnt, hhMonsterAI::Event_TriggerReactEnt ) + EVENT( MA_InitialWallwalk, hhMonsterAI::Event_InitialWallwalk ) + EVENT( MA_GetVehicle, hhMonsterAI::Event_GetVehicle ) + EVENT( MA_EnemyAimingAtMe, hhMonsterAI::Event_EnemyAimingAtMe ) + EVENT( MA_ReachedEntity, hhMonsterAI::Event_ReachedEntity ) + EVENT( MA_EnemyOnSpawn, hhMonsterAI::Event_EnemyOnSpawn ) + EVENT( MA_SpawnFX, hhMonsterAI::Event_SpawnFX ) + EVENT( MA_SplashDamage, hhMonsterAI::Event_SplashDamage) + EVENT( MA_SetVehicleState, hhMonsterAI::Event_SetVehicleState ) + EVENT( EV_PostSpawn, hhMonsterAI::Event_PostSpawn ) + EVENT( MA_FollowPath, hhMonsterAI::Event_FollowPath ) + EVENT( MA_GetLastReachableEnemyPos, hhMonsterAI::Event_GetLastReachableEnemyPos ) + EVENT( MA_EnemyIsA, hhMonsterAI::Event_EnemyIsA ) + EVENT( MA_Subtitle, hhMonsterAI::Event_Subtitle ) + EVENT( MA_SubtitleOff, hhMonsterAI::Event_SubtitleOff ) + EVENT( MA_EnableHeadlook, hhMonsterAI::Event_EnableHeadlook ) + EVENT( MA_DisableHeadlook, hhMonsterAI::Event_DisableHeadlook ) + EVENT( MA_EnableEyelook, hhMonsterAI::Event_EnableEyelook ) + EVENT( MA_DisableEyelook, hhMonsterAI::Event_DisableEyelook ) + EVENT( MA_FacingEnemy, hhMonsterAI::Event_FacingEnemy ) + EVENT( MA_BossBar, hhMonsterAI::Event_BossBar ) + EVENT( MA_FallNow, hhMonsterAI::Event_FallNow ) + EVENT( MA_AllowFall, hhMonsterAI::Event_AllowFall ) + EVENT( MA_InPlayerFov, hhMonsterAI::Event_InPlayerFov ) + EVENT( MA_EnemyIsSpirit, hhMonsterAI::Event_EnemyIsSpirit ) + EVENT( MA_EnemyIsPhysical, hhMonsterAI::Event_EnemyIsPhysical ) + EVENT( MA_IsRagdoll, hhMonsterAI::Event_IsRagdoll ) + EVENT( MA_MoveDone, hhMonsterAI::Event_MoveDone ) + EVENT( MA_SetShootTarget, hhMonsterAI::Event_SetShootTarget) + EVENT( EV_EnemyInGravityZone, hhMonsterAI::Event_EnemyInGravityZone ) + EVENT( MA_SetLookOffset, hhMonsterAI::Event_SetLookOffset ) + EVENT( MA_SetHeadFocusRate, hhMonsterAI::Event_SetHeadFocusRate ) + EVENT( MA_FlyZip, hhMonsterAI::Event_FlyZip ) + EVENT( MA_UseConsole, hhMonsterAI::Event_UseConsole ) + EVENT( MA_TestMeleeDef, hhMonsterAI::Event_TestMeleeDef ) + EVENT( MA_EnemyInVehicle, hhMonsterAI::Event_EnemyInVehicle ) + EVENT( MA_EnemyOnWallwalk, hhMonsterAI::Event_EnemyOnWallwalk ) + EVENT( MA_AlertAI, hhMonsterAI::Event_AlertAI ) + EVENT( MA_TestAnimMoveBlocked, hhMonsterAI::Event_TestAnimMoveBlocked ) + EVENT( MA_InGravityZone, hhMonsterAI::Event_InGravityZone ) + EVENT( MA_StartSoundDelay, hhMonsterAI::Event_StartSoundDelay ) + EVENT( MA_SetTeam, hhMonsterAI::Event_SetTeam ) + EVENT( MA_GetAttackPoint, hhMonsterAI::Event_GetAttackPoint) + EVENT( MA_HideNoDormant, hhMonsterAI::Event_HideNoDormant ) + EVENT( MA_SoundOnModel, hhMonsterAI::Event_SoundOnModel ) + EVENT( MA_ActivatePhysics, hhMonsterAI::Event_ActivatePhysics ) + EVENT( MA_IsVehicleDocked, hhMonsterAI::Event_IsVehicleDocked ) + EVENT( MA_EnemyInSpirit, hhMonsterAI::Event_EnemyInSpirit ) +END_CLASS + +void hhMonsterAI::Event_EnemyAimingAtMe() { + if ( enemy.IsValid() ) { + if ( enemy->GetAxis()[0] * -(enemy->GetOrigin() - GetOrigin()).ToNormal() ) { + idThread::ReturnInt( true ); + return; + } + } + + idThread::ReturnInt( false ); +} + +//A little after Spawn() or Show(), check if monster is stuck on wallwalk. +void hhMonsterAI::Event_InitialWallwalk() { + if ( IsHidden() ) { + return; + } + idMat3 initRot = spawnArgs.GetMatrix( "rotation", "1 0 0 0 1 0 0 0 1" ); + if ( initRot == mat3_identity ) { + return; + } + trace_t TraceInfo; + gameLocal.clip.TracePoint(TraceInfo, GetOrigin(), GetOrigin() - initRot[2]*100, GetPhysics()->GetClipMask(), this); + if( TraceInfo.fraction < 1.0f && health > 0 ) { + if ( gameLocal.GetMatterType(TraceInfo, NULL) == SURFTYPE_WALLWALK ) { + DisableIK(); + SetOrigin( TraceInfo.c.point + initRot * idVec3(0,0,1) ); + GetPhysics()->SetAxis( initRot ); + viewAxis = initRot; + SetGravity( -TraceInfo.c.normal * 1066 ); + physicsObj.SetClipModelAxis(); + renderEntity.axis = initRot; + + idVec3 local_dir; + physicsObj.GetAxis().ProjectVector( initRot[0], local_dir ); + ideal_yaw = idMath::AngleNormalize180( local_dir.ToYaw() ); + current_yaw = ideal_yaw; + } + } +} + +void hhMonsterAI::Event_EnemyOnSide() { + if( enemy.IsValid() ) { + idVec3 povPos, targetPos; + povPos = enemy->GetPhysics()->GetOrigin(); + targetPos = GetPhysics()->GetOrigin(); + idVec3 povToTarget = targetPos - povPos; + povToTarget.z = 0.f; + povToTarget.Normalize(); + float dot = GetPhysics()->GetAxis()[ 1 ] * povToTarget; + idThread::ReturnFloat( dot ); + } + else { + idThread::ReturnFloat( 0.f ); + } +} + +void hhMonsterAI::Event_EnemyIsA( const char* testclass ) { + idTypeInfo* type = idClass::GetClass( testclass ); + if( type ) { + if( enemy.IsValid() ) { + if( enemy->GetType()->IsType(*type) ) { + idThread::ReturnInt( 1 ); + return; + } + } + } + idThread::ReturnInt( 0 ); +} + +void hhMonsterAI::Event_SetMeleeRange( float newRange ) { + melee_range = newRange; +} + +// +// Event_UseReaction +// +// Notes: +// + Need to support the actual various types of Causes, not just this one specific case. +// + Need to support blends (in/out) on animation causes +void hhMonsterAI::Event_UseReaction() { + if( !AI_USING_REACTION ) { + AI_USING_REACTION = true; + AI_REACTION_FAILED = false; + AI_REACTION_ANIM = false; + } + + if( !CheckValidReaction() ) { + AI_USING_REACTION = false; + AI_REACTION_FAILED = false; + AI_REACTION_ANIM = false; + return; + } + + hhReaction *reaction = targetReaction.GetReaction(); + assert(reaction); // CheckValidReaction() should ensure this is a valid pointer. + if( reaction->desc->flags & hhReactionDesc::flag_Exclusive ) { + if ( !reaction->exclusiveOwner.IsValid() ) { + reaction->exclusiveOwner = this; + } else if ( reaction->exclusiveOwner != this ) { + AI_USING_REACTION = false; + AI_REACTION_FAILED = false; + AI_REACTION_ANIM = false; + return; + } + } + + idVec3 tp = GetTouchPos( reaction->causeEntity.GetEntity(), reaction->desc ); + idBounds tpb; + bool validTpb = GetTouchPosBound( reaction->desc, tpb ); + + if( AI_REACTION_ANIM ) { + if( torsoAnim.AnimDone( 0 ) || GetAnimator()->CurrentAnim( ANIMCHANNEL_TORSO )->GetEndTime() < 0.f ) { + AI_REACTION_ANIM = false; + Event_AnimState( ANIMCHANNEL_LEGS, "Legs_Idle", 0 ); + Event_AnimState( ANIMCHANNEL_TORSO, "Torso_Idle", 0 ); + ProcessEvent( &AI_EnablePain ); + FinishReaction(); + } + } + else { + //play reaction sound if one exists + if ( !ai_skipSpeech.GetBool() && gameLocal.GetTime() > nextSpeechTime ) { + idStr speechDesc = idStr("snd_speech_"); + speechDesc += hhReactionDesc::EffectToStr( reaction->desc->effect ); + speechDesc += idStr( "_" ); + speechDesc += targetReaction.entity->spawnArgs.GetString( "speech_name" ); + if ( ai_debugBrain.GetBool() ) { + gameLocal.Printf( "reaction speech(%s)\n", speechDesc.c_str() ); + } + const idKeyValue *kv = spawnArgs.MatchPrefix( speechDesc ); + if( kv ) { + StartSound(kv->GetKey(), SND_CHANNEL_VOICE, 0, true, NULL); + } + nextSpeechTime = gameLocal.GetTime() + int(spawnArgs.GetFloat( "speech_wait", "2.0" ) * 1000); + } + if( (validTpb && !NearEnoughTouchPos(reaction->causeEntity.GetEntity(), tp, tpb)) || (!validTpb && !ReachedPos(tp, move.moveCommand)) ) { + if ( !MoveToPosition( tp, true ) ) { + //position is unreachable so stop using this reaction + AI_USING_REACTION = false; + AI_REACTION_FAILED = false; + AI_REACTION_ANIM = false; + } + } else { + SlideToPosition( tp, 0.5 ); + StopMove( MOVE_STATUS_DONE ); + if( reaction->desc->flags & hhReactionDesc::flag_SnapToPoint ) { + SetOrigin( idVec3(tp.x, tp.y, tp.z) ); + } + //Turn to face direction specified by node + if( reaction->causeEntity->spawnArgs.GetFloat("face_dir") ) { + idAngles faceAngles = reaction->causeEntity->GetAxis()[0].ToAngles(); + ideal_yaw = faceAngles.yaw; + current_yaw = faceAngles.yaw; + SetAxis( reaction->causeEntity->GetAxis() ); + } + else if( reaction->desc->flags & hhReactionDesc::flag_AnimFaceCauseDir ) { + idAngles faceAngles = ( reaction->causeEntity->GetOrigin() - GetOrigin() ).ToAngles(); + ideal_yaw = faceAngles.yaw; + current_yaw = faceAngles.yaw; + idAngles newAngles = GetAxis().ToAngles(); + newAngles.yaw = faceAngles.yaw; + SetAxis( newAngles.ToMat3() ); + } + idEntity *ent = NULL; + switch( reaction->desc->cause ) { + case hhReactionDesc::Cause_Touch: + FinishReaction(); + break; + case hhReactionDesc::Cause_Use: + ent = targetReaction.entity.GetEntity(); + if( ent && ent->IsType(hhConsole::Type)) { + hhConsole *cons = static_cast(ent); + if(!cons->CanUse(this)) { + return; + } + cons->Use(this); + } + // Using vehicle -- just bail! + else if(ent->IsType(hhVehicle::Type)) { + EnterVehicle( static_cast(ent) ); + FinishReaction(); + return; + } else { + gameLocal.Warning("\n AI TRIED TO USE UNUSABLE ENTITY!"); + } + FinishReaction(); + break; + case hhReactionDesc::Cause_PlayAnim: + AI_REACTION_ANIM = true; + GetAnimator()->Clear( ANIMCHANNEL_TORSO, gameLocal.GetTime(), 0 ); + GetAnimator()->Clear( ANIMCHANNEL_TORSO, gameLocal.GetTime(), 0 ); + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); + Event_AnimState( ANIMCHANNEL_TORSO, "Torso_DoNothing", 0 ); + Event_AnimState(ANIMCHANNEL_LEGS, "Legs_DoNothing", 0 ); + Event_OverrideAnim( ANIMCHANNEL_LEGS ); + Event_PlayAnim( ANIMCHANNEL_TORSO, reaction->desc->anim ); + ProcessEvent(&AI_DisablePain); + break; + default: + break; + } + } + } +} + +// +// Event_FindReaction +// +void hhMonsterAI::Event_FindReaction( const char* effect ) { + idEntity* ent; + hhReaction* react; + hhReactionDesc::Effect react_effect; + idEntity* bestEnt = NULL; + float bestDistance = -1; + int bestRank = -1, bestReactIndex = -1; + + react_effect = hhReactionDesc::StrToEffect( effect ); + + if( react_effect == hhReactionDesc::Effect_Invalid ) { + gameLocal.Warning( "unknown effect '%s' requested from FindReaction", effect ); + idThread::ReturnEntity( NULL ); + return; + } + + for ( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if( !ent || ent->fl.isDormant || !ent->fl.refreshReactions ) { + continue; + } + + for( int j = 0; j < ent->GetNumReactions(); j++ ) { + react = ent->GetReaction( j ); + if( !react || !react->IsActive() ) { + continue; + } + if( react_effect != react->desc->effect ) { + continue; + } + if( react->desc->flags & hhReactionDesc::flag_Exclusive ) { //check exclusiveness + if ( react->exclusiveOwner.IsValid() && react->exclusiveOwner->health <= 0 ) { + react->exclusiveOwner.Clear(); + } + if( react->exclusiveOwner.GetEntity() && react->exclusiveOwner != this ) { + continue; + } + } + //Skip based on flag requirements + if( (react->desc->flags & hhReactionDesc::flagReq_RangeAttack) && !AI_HAS_RANGE_ATTACK ) { + continue; + } + // Skip monsters without melee attack + if( (react->desc->flags & hhReactionDesc::flagReq_MeleeAttack) && !AI_HAS_MELEE_ATTACK ) { + continue; + } + if( react->desc->flags & hhReactionDesc::flagReq_KeyValue ) { + idStr val; + if( spawnArgs.GetString(react->desc->key, "", val) ) { + if( val != react->desc->keyVal ) { + continue; + } + } + else { + continue; + } + } + // Skip monsters without specific animation? + if( react->desc->flags & hhReactionDesc::flagReq_Anim ) { + if( !GetAnimator()->HasAnim(react->desc->anim) ) { + continue; + } + } + + // Skip monsters in vehicles? + if( (react->desc->flags & hhReactionDesc::flagReq_NoVehicle) && InVehicle() ) { + continue; + } + + // Check actual specifics for reaction type + switch( react->desc->effect ) { + case hhReactionDesc::Effect_HaveFun: + case hhReactionDesc::Effect_Vehicle: + case hhReactionDesc::Effect_VehicleDock: + case hhReactionDesc::Effect_Heal: + case hhReactionDesc::Effect_ProvideCover: + case hhReactionDesc::Effect_Climb: + case hhReactionDesc::Effect_Passageway: + break; + case hhReactionDesc::Effect_DamageEnemy: + if ( enemy.IsValid() && react->desc->effectRadius > 0 ) { + float distSq = (enemy->GetOrigin() - react->causeEntity->GetOrigin()).LengthSqr(); + if ( distSq > react->desc->effectRadius * react->desc->effectRadius ) { + continue; + } + } + break; + case hhReactionDesc::Effect_Damage: + //jshtodo temp workaround for legacy stuff. remove when old reaction system is removed + if ( !ent->IsType( hhConsole::Type ) ) { + continue; + } + break; + default: + gameLocal.Error( "effect '%s' not supported yet under simple_ai", hhReactionDesc::EffectToStr(react->desc->effect) ); + } + // if we have actually gotten this far, do our intense calculations last.. + int rank = EvaluateReaction( react ); + if( rank == 0 ) { + continue; + } + +// We have a valid reaction... + float distSq = ( react->causeEntity->GetOrigin() - GetOrigin() ).LengthSqr(); + if ( bestRank == -1 || bestRank <= rank ) { // Check the reaction rank against our best rank + if (rank == bestRank && distSq > bestDistance) { // If they are the same rank, but the current one is farther then ignore it. + continue; + } + bestEnt = ent; + bestReactIndex = j; + bestDistance = distSq; + bestRank = rank; + } + } + } + + if ( bestEnt ) { + targetReaction.entity = bestEnt; + targetReaction.reactionIndex = bestReactIndex; + hhReaction *reaction = targetReaction.GetReaction(); + if( reaction && reaction->desc->flags & hhReactionDesc::flagReq_RangeAttack ) { + shootTarget = bestEnt; + } else { + shootTarget = NULL; + } + idThread::ReturnEntity( targetReaction.entity.GetEntity() ); + } else { + idThread::ReturnEntity( NULL ); + } +} + +// +// Event_AttackMissileEx() +// +void hhMonsterAI::Event_AttackMissileEx( const char *str, int boneDir ) { + const idDict *projDef = NULL; + idList parmList; + + hhUtils::SplitString( idStr(str), parmList, ' ' ); + + if( animator.GetJointHandle(parmList[0].c_str()) == INVALID_JOINT ) { + return gameLocal.Error( "Event_AttackMissileEx: Joint '%s' not found", parmList[0].c_str() ); + } + + if( parmList.Num() == 2 ) { + projDef = gameLocal.FindEntityDefDict( parmList[1].c_str(), false ); + } + + Event_AttackMissile( parmList[0].c_str(), projDef, boneDir ); +} + +//HUMANHEAD jsh PCF 4/27/06 initialized proj and made sure ReturnEntity is called +void hhMonsterAI::Event_AttackMissile( const char *jointname, const idDict *projDef, int boneDir ) { + idProjectile *proj = NULL; + + // Bonedir launch? + if((BOOL)boneDir) { + proj = hhProjectile::SpawnProjectile(projDef); + if ( proj ) { + idMat3 axis; + idVec3 muzzle; + GetMuzzle( jointname, muzzle, axis ); + proj->Create(this, muzzle, axis); + proj->Launch(muzzle, axis, vec3_zero); + } + } + else { + if ( shootTarget.IsValid() ) { + proj = LaunchProjectile( jointname, shootTarget.GetEntity(), true, projDef ); //HUMANHEAD mdc - pass projDef on for multiple proj support + } else { + proj = LaunchProjectile( jointname, enemy.GetEntity(), true, projDef ); //HUMANHEAD mdc - pass projDef on for multiple proj support + } + } + idThread::ReturnEntity( proj ); +} + +void hhMonsterAI::Event_HitCheck( idEntity *ent, const char *animname ) { + int anim; + idVec3 dir; + idVec3 local_dir; + idVec3 fromPos; + idMat3 axis; + idVec3 start; + trace_t tr; + float distance; + + if ( !AI_ENEMY_VISIBLE || !ent ) { + idThread::ReturnInt( false ); + return; + } + + anim = GetAnim( ANIMCHANNEL_LEGS, animname ); + if ( !anim ) { + idThread::ReturnInt( false ); + return; + } + + //// just do a ray test if close enough + //if ( ent->GetPhysics()->GetAbsBounds().IntersectsBounds( physicsObj.GetAbsBounds().Expand( 16.0f ) ) ) { + // Event_CanHitEnemy(); + // return; + //} + + // calculate the world transform of the launch position + const idVec3 &org = physicsObj.GetOrigin(); + dir = ent->GetOrigin() - org; + physicsObj.GetGravityAxis().ProjectVector( dir, local_dir ); + local_dir.z = 0.0f; + local_dir.ToVec2().Normalize(); + axis = local_dir.ToMat3(); + fromPos = physicsObj.GetOrigin() + missileLaunchOffset[ anim ] * axis; + + if ( projectileClipModel == NULL ) { + CreateProjectileClipModel(); + } + + // check if the owner bounds is bigger than the projectile bounds + const idBounds &ownerBounds = physicsObj.GetAbsBounds(); + const idBounds &projBounds = projectileClipModel->GetBounds(); + if ( ( ( ownerBounds[1][0] - ownerBounds[0][0] ) > ( projBounds[1][0] - projBounds[0][0] ) ) && + ( ( ownerBounds[1][1] - ownerBounds[0][1] ) > ( projBounds[1][1] - projBounds[0][1] ) ) && + ( ( ownerBounds[1][2] - ownerBounds[0][2] ) > ( projBounds[1][2] - projBounds[0][2] ) ) ) { + if ( (ownerBounds - projBounds).RayIntersection( org, viewAxis[ 0 ], distance ) ) { + start = org + distance * viewAxis[ 0 ]; + } else { + start = ownerBounds.GetCenter(); + } + } else { + // projectile bounds bigger than the owner bounds, so just start it from the center + start = ownerBounds.GetCenter(); + } + + gameLocal.clip.Translation( tr, start, fromPos, projectileClipModel, mat3_identity, MASK_SHOT_RENDERMODEL, this ); + fromPos = tr.endpos; + + if ( GetAimDir( fromPos, ent, this, dir ) ) { + idThread::ReturnInt( true ); + } else { + idThread::ReturnInt( false ); + } +} + +void hhMonsterAI::Event_CreateMonsterPortal(void) { + static const char * passPrefix = "portal_"; + const char * portalDef; + idEntity * portal; + idDict portalArgs; + idList xferKeys; + idList xferValues; + const idKeyValue * buddyKV; + + + // Find out which portal def to spawn - If none specified, then exit; + buddyKV = spawnArgs.FindKey( "portal_buddy" ); + if ( buddyKV && buddyKV->GetValue().Length() && gameLocal.FindEntity(buddyKV->GetValue().c_str()) ) { + // Case of a valid portal_buddy key, make a real portal + portalDef = spawnArgs.GetString( "def_portal" ); + } else { + portalDef = spawnArgs.GetString( "def_fakeportal" ); + } + + if ( !portalDef || !portalDef[0] ) { + return; + } + + // Set the origin of the portal to us. + portalArgs.SetVector( "origin", GetOrigin() ); + + // Pass along any angle key, if set. + if ( spawnArgs.GetBool( "portal_face" ) && enemy.IsValid() ) { + portalArgs.SetInt( "angle", (enemy->GetOrigin() - GetOrigin() ).ToAngles().yaw ); + ideal_yaw = ( enemy->GetOrigin() - GetOrigin() ).ToAngles().yaw; + current_yaw = ideal_yaw; + } else if ( spawnArgs.GetString( "angle", NULL) ) { + portalArgs.Set( "angle", spawnArgs.GetString( "angle" ) ); + } else { + portalArgs.Set( "rotation", spawnArgs.GetString( "rotation" ) ); + } + + // Pass along all 'portal_' keys to the portal's spawnArgs; + hhUtils::GetKeysAndValues( spawnArgs, passPrefix, xferKeys, xferValues ); + for ( int i = 0; i < xferValues.Num(); ++i ) { + xferKeys[ i ].StripLeadingOnce( passPrefix ); + //gameLocal.Printf( "Passing %s => %s\n", xferKeys[ i ].c_str(), xferValues[ i ].c_str() ); + portalArgs.Set( xferKeys[ i ].c_str(), xferValues[ i ].c_str() ); + } + + // Set the name of the associated game portal so it can be turned on and off + portalArgs.Set( "gamePortalName", GetName() ); + + // Spawn the portal + portal = gameLocal.SpawnObject( portalDef, &portalArgs ); + if ( !portal ) { + return; + } + + // Move the portal up some pre determinted amt, since its origin is in the middle of it + float offset = spawnArgs.GetFloat( "offset_portal", 0 ); + portal->GetPhysics()->SetOrigin( portal->GetPhysics()->GetOrigin() + (portal->GetAxis()[2] * offset) ); + + // Move the portal by some offset vector + idVec3 vectorOffset = spawnArgs.GetVector( "offset_portal_vector", "0 0 0" ); + portal->GetPhysics()->SetOrigin( portal->GetPhysics()->GetOrigin() + (portal->GetAxis() * vectorOffset ) ); + + // Update the camera stuff + portal->ProcessEvent( &EV_UpdateCameraTarget ); + + // Open the portal - Need to delay this, so that PostSpawn gets called/sets up the partner portal + //? Should we always pass in the player? + portal->PostEventSec( &EV_Activate, 0, gameLocal.GetLocalPlayer() ); + // Maybe wait for it to finish? + + +} + +void hhMonsterAI::Event_GetShootTarget() { + idThread::ReturnEntity( shootTarget.GetEntity() ); +} + +void hhMonsterAI::Event_TriggerReactEnt(void) { + if ( targetReaction.entity.IsValid() ) { + targetReaction.entity->PostEventMS( &EV_Activate, 0, this ); + } +} + +void hhMonsterAI::Event_GetVehicle() { + idThread::ReturnEntity( vehicleInterface->GetVehicle() ); +} + +void hhMonsterAI::Event_ReachedEntity( idEntity *ent ) { + if ( ent ) { + idVec3 pos = ent->GetPhysics()->GetOrigin(); + if ( physicsObj.GetAbsBounds().IntersectsBounds( idBounds( pos ).Expand( 8.0f ) ) ) { + idThread::ReturnInt( true ); + return; + } + } + + idThread::ReturnInt( false ); +} + +void hhMonsterAI::Event_EnemyOnSpawn(void) +{ + idEntity *ent = NULL; + idStr entName; + if(spawnArgs.GetString("enemy_on_spawn"," ", entName)) + { + if ( entName == idStr("closest_player") || entName == idStr("players_in_view") ) { + ent = GetClosestPlayer(); + } + if ( !ent ) { + ent = gameLocal.FindEntity(entName); + } + if( ent && ent->IsType(idActor::Type) ) { + SetEnemy( static_cast(ent) ); + } + } +} + +void hhMonsterAI::Event_SpawnFX( char *fxFile ) { + + if ( !fxFile || !fxFile[0] ) { + return; + } + + idVec3 offset; + hhFxInfo fxInfo; + + offset = spawnArgs.GetVector( "particle_offset", "0 0 0" ); + + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfo( fxFile, GetOrigin() + offset * GetAxis(), GetAxis(), &fxInfo ); + +} + +void hhMonsterAI::Event_SplashDamage(char *damage) { + + if(!damage || !damage[0]) + return; + + idStr splash_damage; + + // Need to set takedamage to false in order to prevent infinite loop! + //fl.takedamage = false; + splash_damage = spawnArgs.GetString(damage); + if ( splash_damage.Length() ) { + gameLocal.RadiusDamage( GetPhysics()->GetOrigin(), NULL, NULL, this, this, splash_damage ); + } +} + +void hhMonsterAI::Event_FollowPath( const char *pathName ) { + if ( scriptObject.HasObject() ) { + const function_t *func; + idThread *thread; + + func = scriptObject.GetFunction( "follow_alternate_path" ); + if ( !func ) { + gameLocal.Error( "Function 'follow_alternate_path' not found on entity '%s' for function call from '%s'", name.c_str(), name.c_str() ); + } + if ( func->type->NumParameters() != 1 ) { + gameLocal.Error( "Function 'follow_alternate_path' on entity '%s' has the wrong number of parameters for function call from '%s'", name.c_str(), name.c_str() ); + } + if ( !scriptObject.GetTypeDef()->Inherits( func->type->GetParmType( 0 ) ) ) { + gameLocal.Error( "Function 'follow_alternate_path' on entity '%s' is the wrong type for function call from '%s'", name.c_str(), name.c_str() ); + } + spawnArgs.Set( "alt_path", pathName ); + // create a thread and call the function + thread = new idThread(); + thread->CallFunction( this, func, true ); + thread->Start(); + } +} + +void hhMonsterAI::Event_SetVehicleState() { + const function_t *newstate = NULL; + if ( GetVehicleInterface()->UnderScriptControl() ) { + newstate = GetScriptFunction( "state_Nothing" ); + } + if ( newstate ) { + SetState( newstate ); + } +} + +void hhMonsterAI::Event_Subtitle( idList* parmList ) { + if ( !parmList || !parmList->Num() ) { + return; + } + idPlayer *player = gameLocal.GetLocalPlayer(); + player->hud->SetStateInt("subtitlex", 0 ); + player->hud->SetStateInt("subtitley", 400 ); + player->hud->SetStateInt("subtitlecentered", true); + player->hud->SetStateString("subtitletext", common->GetLanguageDict()->GetString( (*parmList)[0].c_str() )); + player->hud->StateChanged(gameLocal.time); + player->hud->HandleNamedEvent("DisplaySubtitle"); + + CancelEvents(&MA_SubtitleOff); + if ( parmList->Num() == 1 ) { + PostEventSec(&MA_SubtitleOff, (float)atof((*parmList)[1].c_str())); + } else { + PostEventSec(&MA_SubtitleOff, 2.0); + } +} + +void hhMonsterAI::Event_SubtitleOff() { + idPlayer *player = gameLocal.GetLocalPlayer(); + player->hud->HandleNamedEvent("RemoveSubtitleInstant"); +} + +/* +===================== +hhMonsterAI::Event_GetLastReachableEnemyPos +===================== +*/ +void hhMonsterAI::Event_GetLastReachableEnemyPos() { + idThread::ReturnVector( lastReachableEnemyPos ); +} + +void hhMonsterAI::Event_EnableEyelook() { + allowEyeFocus = true; +} + +void hhMonsterAI::Event_DisableEyelook() { + allowEyeFocus = false; +} + +void hhMonsterAI::Event_EnableHeadlook() { + allowJointMod = true; +} + +void hhMonsterAI::Event_DisableHeadlook() { + allowJointMod = false; +} + +void hhMonsterAI::Event_FacingEnemy( float range ) { + if ( FacingEnemy( range ) ) { + idThread::ReturnInt( 1 ); + } else { + idThread::ReturnInt( 0 ); + } +} + +bool hhMonsterAI::FacingEnemy( float range ) { + if ( !enemy.IsValid() ) { + return false; + } + + if ( !turnRate ) { + return true; + } + + idVec3 dir; + idVec3 local_dir; + float lengthSqr; + float local_yaw; + + dir = enemy->GetOrigin() - physicsObj.GetOrigin(); + physicsObj.GetAxis().ProjectVector( dir, local_dir ); + local_dir.z = 0.0f; + lengthSqr = local_dir.LengthSqr(); + local_yaw = idMath::AngleNormalize180( local_dir.ToYaw() ); + + float diff; + + diff = idMath::AngleNormalize180( current_yaw - local_yaw ); + return ( idMath::Fabs( diff ) < range ); +} + +void hhMonsterAI::Event_AllowFall( int allowFall ) { + bCanFall = allowFall != 0; +} + +void hhMonsterAI::Event_BossBar( int onOff ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( !player || !player->hud ) { + return; + } + + if ( bBossBar ) { + if ( !onOff ) { + player->hud->HandleNamedEvent("HideProgressBar"); + player->hud->StateChanged(gameLocal.time); + bBossBar = false; + } + } else { + if ( onOff ) { + player->hud->HandleNamedEvent("ShowProgressBar"); + player->hud->SetStateBool( "progressbar", true ); + bBossBar = true; + } + } +} + +void hhMonsterAI::Event_FallNow() { + SetGravity( DEFAULT_GRAVITY_VEC3 ); + GetPhysics()->SetLinearVelocity( idVec3( 0,0,-500 ) ); +} + +//taken from idEntity::Event_PlayerCanSee and removed trace +void hhMonsterAI::Event_InPlayerFov() { + int i; + idEntity *ent; + hhPlayer *player; + trace_t traceInfo; + bool result = false; + + // Check if this entity is in the player's PVS + if ( gameLocal.InPlayerPVS( this ) ) { + for ( i = 0; i < gameLocal.numClients ; i++ ) { + ent = gameLocal.entities[ i ]; + + if ( !ent || !ent->IsType( hhPlayer::Type ) ) { + continue; + } + + // Get the player + player = static_cast( ent ); + + // Check if the entity is in the player's FOV, based upon the "fov" key/value + if ( player->CheckYawFOV( this->GetOrigin() ) ) { + result = true; + } + } + } + + if ( result ) { + idThread::ReturnInt( true ); + } else { + idThread::ReturnInt( false ); + } +} + +void hhMonsterAI::Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ) { + HH_ASSERT( player == enemy.GetEntity() ); + //HUMANHEAD jsh PCF 4/29/06 stop looking and make enemy not visible for a frame + Event_LookAtEntity( NULL, 0.0f ); + AI_ENEMY_VISIBLE = false; + enemy = proxy; +} + +void hhMonsterAI::Event_EnemyIsPhysical( hhPlayer *player, hhSpiritProxy *proxy ) { + // If we weren't targetting the physical player before, and we can't see them now, + // lose target + //HUMANHEAD jsh PCF 4/29/06 stop looking and make enemy not visible for a frame + Event_LookAtEntity( NULL, 0.0f ); + AI_ENEMY_VISIBLE = false; + if ( !proxy && !CanSee( player, true ) ) { + enemy = NULL; + } else { + enemy = player; + } +} + +void hhMonsterAI::Event_IsRagdoll() { + if ( af.IsActive() ) { + idThread::ReturnInt( true ); + return; + } + idThread::ReturnInt( false ); +} + +void hhMonsterAI::Event_MoveDone() { + if ( AI_MOVE_DONE ) { + idThread::ReturnInt( true ); + return; + } + idThread::ReturnInt( false ); +} + +void hhMonsterAI::Event_SetShootTarget(idEntity *ent) { + shootTarget = ent; +} + +void hhMonsterAI::Event_LookAtEntity( idEntity *ent, float duration ) { + if ( ent == this ) { + ent = NULL; + } + + if ( ( ent != focusEntity.GetEntity() ) || ( focusTime < gameLocal.time ) ) { + focusEntity = ent; + alignHeadTime = gameLocal.time; + forceAlignHeadTime = gameLocal.time + SEC2MS( 1 ); + blink_time = 0; + } + + if ( duration == -1 ) { + focusTime = -1; + } else { + focusTime = gameLocal.time + SEC2MS( duration ); + } +} + +void hhMonsterAI::Event_LookAtEnemy( float duration ) { + idActor *enemyEnt; + + enemyEnt = enemy.GetEntity(); + if ( ( enemyEnt != focusEntity.GetEntity() ) || ( focusTime < gameLocal.time ) ) { + focusEntity = enemyEnt; + alignHeadTime = gameLocal.time; + forceAlignHeadTime = gameLocal.time + SEC2MS( 1 ); + blink_time = 0; + } + + if ( duration == -1 ) { + focusTime = -1; + } else { + focusTime = gameLocal.time + SEC2MS( duration ); + } +} + +void hhMonsterAI::Event_SetLookOffset( idAngles const &ang ) { + lookOffset = ang; +} + +void hhMonsterAI::Event_SetHeadFocusRate( float rate ) { + headFocusRate = rate; +} + +void hhMonsterAI::Event_EnemyInGravityZone(void) { + if ( !enemy.IsValid() ) { + idThread::ReturnFloat( 1.0f ); + return; + } + idThread::ReturnFloat( enemy->InGravityZone() ); +} + +void hhMonsterAI::Event_FlyZip() { + //if enemy is real far away, instantly teleport to a spot near him + if ( !enemy.IsValid() || move.moveType != MOVETYPE_FLY ) { + return; + } + + //find a good spot to try and teleport to + float distance = spawnArgs.GetFloat( "zip_range", "700" ); + idVec3 testPoint; + idVec3 finalPoint = vec3_zero; + bool clipped = false; + float yaw = (GetOrigin() - enemy->GetOrigin()).ToYaw(); + idBounds bounds; + idVec3 size; + bounds.Zero(); + if ( spawnArgs.GetVector( "mins", NULL, bounds[0] ) && + spawnArgs.GetVector( "maxs", NULL, bounds[1] ) ) { + } else if ( spawnArgs.GetVector( "size", NULL, size ) ) { + bounds[0].Set( size.x * -0.5f, size.y * -0.5f, 0.0f ); + bounds[1].Set( size.x * 0.5f, size.y * 0.5f, size.z ); + } else { // Default bounds + bounds.Expand( 1.0f ); + } + + //test 8 points around the enemy, starting with one directly in front of it + for ( int i=0;i<8;i++ ) { + testPoint = enemy->GetOrigin() + distance * idAngles( 0, yaw, 0 ).ToForward(); + yaw += 45; + if ( yaw > 360 ) { + yaw -= 360; + } + + int contents = hhUtils::ContentsOfBounds( bounds, testPoint, mat3_identity, this); + if ( contents & CONTENTS_MONSTERCLIP ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, GetOrigin(), testPoint, 10, 999999 ); + } + continue; + } + + //make sure we can path there + int toAreaNum = PointReachableAreaNum( testPoint ); + int areaNum = PointReachableAreaNum( physicsObj.GetOrigin() ); + aasPath_t path; + if ( !toAreaNum || !PathToGoal( path, areaNum, physicsObj.GetOrigin(), toAreaNum, testPoint ) ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, GetOrigin(), testPoint, 10, 10000 ); + } + continue; + } + + //passed all tests use this point + finalPoint = testPoint; + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorGreen, GetOrigin(), testPoint, 10, 10000 ); + } + break; + } + + if ( finalPoint != vec3_zero ) { + //do the actual teleport + GetPhysics()->SetOrigin( testPoint + idVec3( 0, 0, CM_CLIP_EPSILON ) ); + GetPhysics()->SetLinearVelocity( vec3_origin ); + viewAxis = (enemy->GetOrigin() - GetOrigin()).ToMat3(); + UpdateVisuals(); + } +} + +void hhMonsterAI::Event_UseConsole( idEntity *ent ) { + if ( !ent || !ent->IsType(hhConsole::Type) ) { + return; + } + hhConsole *cons = static_cast(ent); + if(!cons->CanUse(this)) { + return; + } + cons->Use(this); +} + +void hhMonsterAI::Event_TestMeleeDef( const char *meleeDefName ) const { + bool canMelee = TestMeleeDef( meleeDefName ); + + if ( canMelee ) { + idThread::ReturnFloat( 1.0f ); + } else { + idThread::ReturnFloat( 0.0f ); + } +} + +void hhMonsterAI::Event_EnemyInVehicle() { + if ( enemy.IsValid() && enemy->InVehicle() ) { + idThread::ReturnInt( true ); + } else { + idThread::ReturnInt( false ); + } +} + +void hhMonsterAI::Event_HeardSound( int ignore_team ) { + // overridden to use hearingRange instead of AI_HEARING_RANGE + // check if we heard any sounds in the last frame + idActor *actor = gameLocal.GetAlertEntity(); + if ( actor && ( !ignore_team || ( ReactionTo( actor ) & ATTACK_ON_SIGHT ) ) && gameLocal.InPlayerPVS( this ) ) { + idVec3 pos = actor->GetPhysics()->GetOrigin(); + idVec3 org = physicsObj.GetOrigin(); + float dist = ( pos - org ).LengthSqr(); + //allow the sound's own radius to override hearingRange, if it is set + if ( gameLocal.lastAIAlertRadius ) { + if ( dist < Square( gameLocal.lastAIAlertRadius ) ) { + idThread::ReturnEntity( actor ); + return; + } + } else if ( dist < Square( hearingRange ) ) { + idThread::ReturnEntity( actor ); + return; + } + } +} + +void hhMonsterAI::Event_AlertAI( idEntity *ent, float radius ) { + if ( !ent || radius <= 0 ) { + return; + } + gameLocal.AlertAI( ent, radius ); +} + +void hhMonsterAI::Event_EnemyOnWallwalk() { + if ( enemy.IsValid() && enemy->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast(enemy.GetEntity()); + if ( player && player->IsWallWalking() ) { + idThread::ReturnInt( true ); + return; + } + } + + idThread::ReturnInt( false ); +} + +/* +===================== +hhMonsterAI::Event_TestAnimMoveBlocked +===================== +*/ +void hhMonsterAI::Event_TestAnimMoveBlocked( const char *animname ) { + int anim; + predictedPath_t path; + idVec3 moveVec; + + anim = GetAnim( ANIMCHANNEL_LEGS, animname ); + if ( !anim ) { + gameLocal.DWarning( "missing '%s' animation on '%s' (%s)", animname, name.c_str(), GetEntityDefName() ); + idThread::ReturnInt( false ); + return; + } + + moveVec = animator.TotalMovementDelta( anim ) * idAngles( 0.0f, ideal_yaw, 0.0f ).ToMat3() * physicsObj.GetGravityAxis(); + idAI::PredictPath( this, aas, physicsObj.GetOrigin(), moveVec, 1000, 1000, ( move.moveType == MOVETYPE_FLY ) ? SE_BLOCKED : ( SE_ENTER_OBSTACLE | SE_BLOCKED | SE_ENTER_LEDGE_AREA ), path ); + + if ( ai_debugMove.GetBool() ) { + gameRenderWorld->DebugLine( colorGreen, physicsObj.GetOrigin(), physicsObj.GetOrigin() + moveVec, gameLocal.msec ); + gameRenderWorld->DebugBounds( path.endEvent == 0 ? colorYellow : colorRed, physicsObj.GetBounds(), physicsObj.GetOrigin() + moveVec, gameLocal.msec ); + } + + HH_ASSERT( path.endEvent == 0 && !path.blockingEntity || path.blockingEntity ); + + idThread::ReturnEntity( const_cast ( path.blockingEntity ) ); +} + +void hhMonsterAI::Event_InGravityZone() { + idThread::ReturnFloat( InGravityZone() ); +} + +void hhMonsterAI::Event_StartSoundDelay( const char *soundName, int channel, int netSync, float delay ) { + if ( delay < 0 ) { + delay = 0; + } + PostEventSec( &EV_StartSound, delay, soundName, channel, netSync ); +} + +void hhMonsterAI::Event_SetTeam( int new_team ) { + team = new_team; +} + +void hhMonsterAI::Event_HideNoDormant() { + HideNoDormant(); +} + +void hhMonsterAI::Event_GetAttackPoint( void ) { + //if enemy is real far away, instantly teleport to a spot near him + if ( !enemy.IsValid() ) { + idThread::ReturnVector( vec3_zero ); + return; + } + + //find a good spot to attack from + float distance = spawnArgs.GetFloat( "attack_range", "500" ); + idVec3 testPoint; + idVec3 finalPoint = vec3_zero; + bool clipped = false; + float yaw = (GetOrigin() - enemy->GetOrigin()).ToYaw(); + int num, i, j; + idClipModel *cm; + idClipModel *clipModels[ MAX_GENTITIES ]; + idBounds bounds; + + //test 8 points around the enemy, starting with one directly in front of it + idList listo; + for ( i=0;i<8;i++ ) { + testPoint = enemy->GetOrigin() + distance * idAngles( 0, yaw, 0 ).ToForward(); + testPoint.z += spawnArgs.GetFloat( "attack_z", "300" ); + yaw += 45; + if ( yaw > 360 ) { + yaw -= 360; + } + if ( yaw == 180 ) { + continue; + } + + //make sure it wont clip into anything at testPoint + clipped = false; + bounds.FromTransformedBounds( GetPhysics()->GetBounds(), testPoint, GetPhysics()->GetAxis() ); + num = gameLocal.clip.ClipModelsTouchingBounds( bounds, MASK_MONSTERSOLID, clipModels, MAX_GENTITIES ); + for ( j = 0; j < num; j++ ) { + cm = clipModels[ j ]; + // don't check render entities + if ( cm->IsRenderModel() ) { + continue; + } + idEntity *hit = cm->GetEntity(); + if ( ( hit == this ) || !hit->fl.takedamage ) { + continue; + } + if ( physicsObj.ClipContents( cm ) ) { + clipped = true; + break; + } + } + if ( clipped ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugBounds( colorRed, bounds, vec3_origin, 10000 ); + } + continue; + } else { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugBounds( colorGreen, bounds, vec3_origin, 10000 ); + } + } + + //make sure we can path there + if ( !PointReachableAreaNum( testPoint ) ) { + if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorRed, GetOrigin(), testPoint, 10, 10000 ); + } + continue; + } else if ( ai_debugBrain.GetBool() ) { + gameRenderWorld->DebugArrow( colorGreen, GetOrigin(), testPoint, 10, 10000 ); + } + + listo.Append( testPoint ); + } + + if ( listo.Num() ) { + finalPoint = listo[gameLocal.random.RandomInt(listo.Num())]; + } + if ( finalPoint != vec3_zero ) { + idThread::ReturnVector( finalPoint ); + return; + } else { + idThread::ReturnVector( vec3_zero ); + return; + } +} + +void hhMonsterAI::Event_SoundOnModel() { + soundOnModel = !soundOnModel; +} + +void hhMonsterAI::Event_ActivatePhysics() { + ActivatePhysics( this ); +} + +void hhMonsterAI::Event_IsVehicleDocked() { + if ( GetVehicleInterfaceLocal()->IsVehicleDocked() ) { + idThread::ReturnInt( true ); + return; + } + idThread::ReturnInt( false ); +} + +void hhMonsterAI::Event_EnemyInSpirit() { + if ( enemy.IsValid() && enemy->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast( enemy.GetEntity() ); + if ( player && player->IsSpiritWalking() ) { + idThread::ReturnInt( true ); + return; + } + } + + idThread::ReturnInt( false ); +} + +void hhMonsterAI::Event_SetNeverDormant( int enable ) { + if ( head.IsValid() ) { + head->fl.neverDormant = ( enable != 0 ); + } + fl.neverDormant = ( enable != 0 ); + dormantStart = 0; +} + diff --git a/src/Prey/game_mountedgun.cpp b/src/Prey/game_mountedgun.cpp new file mode 100644 index 0000000..c46cd3c --- /dev/null +++ b/src/Prey/game_mountedgun.cpp @@ -0,0 +1,1048 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_ScanForClosestTarget( "" ); +const idEventDef EV_StartAttack( "" ); +const idEventDef EV_NextCycleAnim( "", "ds" ); + +CLASS_DECLARATION( hhAnimatedEntity, hhMountedGun ) + EVENT( EV_Activate, hhMountedGun::Event_Activate ) + EVENT( EV_Deactivate, hhMountedGun::Event_Deactivate ) + EVENT( EV_PostSpawn, hhMountedGun::Event_PostSpawn ) + EVENT( EV_ScanForClosestTarget, hhMountedGun::Event_ScanForClosestTarget ) + EVENT( EV_StartAttack, hhMountedGun::Event_StartAttack ) + + EVENT( EV_NextCycleAnim, hhMountedGun::Event_NextCycleAnim ) +END_CLASS + +/* +================ +hhMountedGun::hhMountedGun +================ +*/ +hhMountedGun::hhMountedGun() { + yawVelocity = 0.0f; +} + +/* +================ +hhMountedGun::Spawn +================ +*/ +void hhMountedGun::Spawn( void ) { + targetingLaser = NULL; + boneHub.Clear(); + boneGun.Clear(); + boneOrigin.Clear(); + + fl.takedamage = false; // Gun doesn't take damage unless it is active + + deathwalkDelay = SEC2MS( spawnArgs.GetInt( "deathwalk_delay", "1" ) ); + burstRateOfFire = SEC2MS( spawnArgs.GetFloat("burstRateOfFire") ); + shotsPerBurst = spawnArgs.GetInt("shotsPerBurst"); + shotsPerBurstCounter = 0; + + firingRange = spawnArgs.GetFloat( "firingRange" ); + nextFireTime = 0; + projectileDict = gameLocal.FindEntityDefDict( spawnArgs.GetString( "def_projectile" ) ); + if ( !projectileDict ) { + gameLocal.Error( "Unknown def_projectile: %s\n", spawnArgs.GetString( "def_projectile" ) ); + } + + // Set up muzzle flash light + memset( &muzzleFlash, 0, sizeof(renderLight_t) ); + muzzleFlash.lightId = 500 + entityNumber; + muzzleFlash.shader = declManager->FindMaterial( spawnArgs.GetString("mtr_flashShader"), false ); + muzzleFlashHandle = -1; + muzzleFlash.pointLight = true; + muzzleFlashEnd = gameLocal.GetTime(); + muzzleFlash.lightRadius = spawnArgs.GetVector( "flashSize" ); + muzzleFlashDuration = SEC2MS( spawnArgs.GetFloat("flashTime") ); + idVec3 flashColor = spawnArgs.GetVector( "flashColor" ); + muzzleFlash.shaderParms[ SHADERPARM_RED ] = flashColor[0]; + muzzleFlash.shaderParms[ SHADERPARM_GREEN ] = flashColor[1]; + muzzleFlash.shaderParms[ SHADERPARM_BLUE ] = flashColor[2]; + muzzleFlash.shaderParms[ SHADERPARM_TIMESCALE ] = 1.0f; + + SpawnParts(); + + physicsObj.SetSelf( this ); + physicsObj.SetClipModel( new idClipModel( GetPhysics()->GetClipModel() ), 1.0f ); + physicsObj.SetContents( CONTENTS_SOLID ); + physicsObj.SetClipMask( GetPhysics()->GetContents() ); + physicsObj.SetOrigin( GetOrigin() ); + physicsObj.SetAxis( GetAxis() ); + SetPhysics( &physicsObj ); + + prevIdealLookAngle.Zero(); + + pitchController.Clear(); + pitchController.Setup( this, spawnArgs.GetString("bone_Hub"), idAngles(-90, 0, 0), idAngles(90, 0, 0), idAngles(90, 0, 0), idAngles(1,0,0) ); + + channelBody = ChannelName2Num( "body" ); + channelBarrel = ChannelName2Num( "barrel" ); + + PlayCycle( channelBody, "idle", 0 ); + + PVSArea = gameLocal.pvs.GetPVSArea( GetOrigin() ); + + PostEventMS( &EV_PostSpawn, 0 ); + + gunMode = GM_DORMANT; + team = spawnArgs.GetInt( "team", "1" ); + + PlayAnim( ANIMCHANNEL_ALL, "idle_close", 0 ); +} + +void hhMountedGun::Save(idSaveGame *savefile) const { + savefile->WriteInt( team ); + savefile->WriteInt( gunMode ); + pitchController.Save( savefile ); + trackMover.Save( savefile ); + targetingLaser.Save( savefile ); + boneHub.Save( savefile ); + boneGun.Save( savefile ); + boneExhaust.Save( savefile ); + boneOrigin.Save( savefile ); + enemy.Save( savefile ); + savefile->WriteFloat( enemyRange ); + savefile->WriteFloat( firingRange ); + savefile->WriteInt( nextFireTime ); + savefile->WriteFloat( burstRateOfFire ); + savefile->WriteInt( shotsPerBurst ); + savefile->WriteInt( shotsPerBurstCounter ); + savefile->WriteRenderLight( muzzleFlash ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->WriteInt( muzzleFlashHandle ); + savefile->WriteInt( muzzleFlashEnd ); + savefile->WriteInt( muzzleFlashDuration ); + savefile->WriteAngles( idealLookAngle ); + savefile->WriteAngles( prevIdealLookAngle ); + savefile->WriteStaticObject( physicsObj ); + savefile->WriteInt( animDoneTime ); + savefile->WriteInt( PVSArea ); + savefile->WriteInt( channelBody ); + savefile->WriteInt( channelBarrel ); + savefile->WriteFloat( yawVelocity ); + savefile->WriteInt( deathwalkDelay ); +} + +void hhMountedGun::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( team ); + savefile->ReadInt( reinterpret_cast ( gunMode ) ); + pitchController.Restore( savefile ); + trackMover.Restore( savefile ); + targetingLaser.Restore( savefile ); + boneHub.Restore( savefile ); + boneGun.Restore( savefile ); + boneExhaust.Restore( savefile ); + boneOrigin.Restore( savefile ); + enemy.Restore( savefile ); + savefile->ReadFloat( enemyRange ); + savefile->ReadFloat( firingRange ); + savefile->ReadInt( nextFireTime ); + savefile->ReadFloat( burstRateOfFire ); + savefile->ReadInt( shotsPerBurst ); + savefile->ReadInt( shotsPerBurstCounter ); + savefile->ReadRenderLight( muzzleFlash ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->ReadInt( muzzleFlashHandle ); + savefile->ReadInt( muzzleFlashEnd ); + savefile->ReadInt( muzzleFlashDuration ); + savefile->ReadAngles( idealLookAngle ); + savefile->ReadAngles( prevIdealLookAngle ); + savefile->ReadStaticObject( physicsObj ); + savefile->ReadInt( animDoneTime ); + savefile->ReadInt( PVSArea ); + savefile->ReadInt( channelBody ); + savefile->ReadInt( channelBarrel ); + savefile->ReadFloat( yawVelocity ); + savefile->ReadInt( deathwalkDelay ); + + projectileDict = gameLocal.FindEntityDefDict( spawnArgs.GetString( "def_projectile" ) ); + if ( !projectileDict ) { + gameLocal.Error( "Unknown def_projectile: %s\n", spawnArgs.GetString( "def_projectile" ) ); + } + RestorePhysics( &physicsObj ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + muzzleFlashHandle = -1; +} + +/* +================ +hhMountedGun::~hhMountedGun +================ +*/ +hhMountedGun::~hhMountedGun( void ) { + SAFE_REMOVE( targetingLaser ); + + SAFE_FREELIGHT( muzzleFlashHandle ); + + trackMover = NULL; +} + +/* +================ +hhMountedGun::SpawnParts +================ +*/ +void hhMountedGun::SpawnParts() { + boneHub.name = spawnArgs.GetString("bone_Hub"); + boneHub.handle = GetAnimator()->GetJointHandle( boneHub.name.c_str() ); + + boneGun.name = spawnArgs.GetString("bone_Gun"); + boneGun.handle = GetAnimator()->GetJointHandle( boneGun.name.c_str() ); + + boneOrigin.name = spawnArgs.GetString("bone_Origin"); + boneOrigin.handle = GetAnimator()->GetJointHandle( boneOrigin.name.c_str() ); + + UpdateBoneInfo(); + + targetingLaser = hhBeamSystem::SpawnBeam( boneGun.origin, spawnArgs.GetString("beam") ); + if( targetingLaser.IsValid() ) { + targetingLaser->MoveToJoint( this, boneGun.name.c_str() ); + targetingLaser->BindToJoint( this, boneGun.name.c_str(), false ); + targetingLaser->Activate( false ); + } +} + +/* +===================== +hhMountedGun::Hide +===================== +*/ +void hhMountedGun::Hide() { + hhAnimatedEntity::Hide(); + + if( targetingLaser.IsValid() ) { + targetingLaser->Hide(); + } +} + +/* +===================== +hhMountedGun::Show +===================== +*/ +void hhMountedGun::Show() { + hhAnimatedEntity::Show(); + + if( targetingLaser.IsValid() ) { + targetingLaser->Show(); + } +} + +/* +===================== +hhMountedGun::DetermineTargetingLaserEndPoint +===================== +*/ +idVec3 hhMountedGun::DetermineTargetingLaserEndPoint() { + trace_t traceInfo; + idVec3 start = boneGun.origin; + idVec3 dist = boneGun.axis[0] * CM_MAX_TRACE_DIST; + + gameLocal.clip.TracePoint( traceInfo, start, start + dist, MASK_SHOT_BOUNDINGBOX, this ); + + return (enemy.IsValid()) ? traceInfo.endpos + enemy->GetAxis()[2] * -10.0f : traceInfo.endpos; +} + +/* +===================== +hhMountedGun::Ticker +===================== +*/ +void hhMountedGun::Ticker( void ) { + if( !pitchController.IsFinishedMoving(PITCH) ) { + pitchController.Update( gameLocal.GetTime() ); + } + + UpdateBoneInfo(); + + if( targetingLaser.IsValid() && !targetingLaser->IsHidden() ) { + targetingLaser->SetTargetLocation( DetermineTargetingLaserEndPoint() ); + } + + UpdateMuzzleFlash(); +} + +/* +================ +hhMountedGun::UpdateOrientation +================ +*/ +void hhMountedGun::UpdateOrientation() { + SetAngles( idAngles( 0, idealLookAngle.yaw, 0 ) ); + pitchController.TurnTo( idealLookAngle ); +} + +/* +================ +hhMountedGun::UpdateBoneInfo +================ +*/ +void hhMountedGun::UpdateBoneInfo() { + idVec3 TempVec; + + GetJointWorldTransform( boneHub.handle, boneHub.origin, boneHub.axis ); + GetJointWorldTransform( boneGun.handle, boneGun.origin, boneGun.axis ); + + GetJointWorldTransform( boneOrigin.handle, boneOrigin.origin, boneOrigin.axis ); +} + +/* +================ +hhMountedGun::SetIdealLookAngle +================ +*/ +void hhMountedGun::SetIdealLookAngle( const idAngles& LookAngle ) { + idealLookAngle.pitch = -hhMath::AngleNormalize180( LookAngle.pitch ); + idealLookAngle.yaw = hhMath::AngleNormalize360( LookAngle.yaw ); + idealLookAngle.roll = 0.0f; + + if( prevIdealLookAngle == idealLookAngle ) { + return; + } + + prevIdealLookAngle = idealLookAngle; + + UpdateOrientation(); +} + +/* +================ +hhMountedGun::AttemptToRemoveMuzzleFlash +================ +*/ +void hhMountedGun::AttemptToRemoveMuzzleFlash() { + if( gameLocal.GetTime() >= muzzleFlashEnd ) { + SAFE_FREELIGHT( muzzleFlashHandle ); + } +} + +/* +================ +hhMountedGun::UpdateMuzzleFlash +================ +*/ +void hhMountedGun::UpdateMuzzleFlash() { + AttemptToRemoveMuzzleFlash(); + + if( muzzleFlashHandle != -1 ) { + UpdateMuzzleFlashPosition(); + gameRenderWorld->UpdateLightDef( muzzleFlashHandle, &muzzleFlash ); + } +} + +/* +================ +hhMountedGun::UpdateMuzzleFlashPosition +================ +*/ +void hhMountedGun::UpdateMuzzleFlashPosition() { + muzzleFlash.axis = boneGun.axis; + muzzleFlash.origin = boneGun.origin; + + //TEST + trace_t trace; + idVec3 flashSize = spawnArgs.GetVector( "flashSize" ); + if( gameLocal.clip.TracePoint(trace, muzzleFlash.origin, muzzleFlash.origin + muzzleFlash.axis[0] * flashSize[0], MASK_SHOT_BOUNDINGBOX, this) ) { + flashSize[0] *= trace.fraction; + } + muzzleFlash.lightRadius = flashSize; + muzzleFlash.origin += muzzleFlash.axis[0] * flashSize[0] * 0.5f; + muzzleFlash.lightCenter.Set( flashSize[0] * -0.45f, 0.0f, 0.0f ); + + hhUtils::Swap( muzzleFlash.lightRadius[0], muzzleFlash.lightRadius[2] ); + hhUtils::Swap( muzzleFlash.lightCenter[0], muzzleFlash.lightCenter[2] ); + muzzleFlash.axis = hhUtils::SwapXZ( muzzleFlash.axis ); + //TEST +} + +/* +================ +hhMountedGun::MuzzleFlash +================ +*/ +void hhMountedGun::MuzzleFlash() { + if( !muzzleFlash.lightRadius[0] ) { + return; + } + + UpdateMuzzleFlashPosition(); + + // these will be different each fire + muzzleFlash.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.GetTime() ); + muzzleFlash.shaderParms[ SHADERPARM_DIVERSITY ] = gameLocal.random.RandomFloat(); + + // the light will be removed at this time + muzzleFlashEnd = gameLocal.GetTime() + muzzleFlashDuration; + + if ( muzzleFlashHandle != -1 ) { + gameRenderWorld->UpdateLightDef( muzzleFlashHandle, &muzzleFlash ); + } else { + muzzleFlashHandle = gameRenderWorld->AddLightDef( &muzzleFlash ); + } +} + +/* +================ +hhMountedGun::TraceTargetVerified +================ +*/ +bool hhMountedGun::TraceTargetVerified( const idActor* target, int traceEntNum ) const { + int entNum = target->InVehicle() ? target->GetVehicleInterface()->GetVehicle()->entityNumber : target->entityNumber; + return entNum == traceEntNum; +} + +/* +================ +hhMountedGun::ScanForClosestTarget +================ +*/ +idActor *hhMountedGun::ScanForClosestTarget() { + trace_t TraceInfo; + idEntity *entity; + idActor *actor; + hhPlayer *player; + float firingRangeSquared = firingRange * firingRange; + float closestDistSqr = firingRangeSquared; + idActor *closestActor = NULL; + pvsHandle_t pvs; + + pvs = gameLocal.pvs.SetupCurrentPVS( GetPVSAreas(), GetNumPVSAreas() ); + + for ( entity = gameLocal.activeEntities.Next(); entity != NULL; entity = entity->activeNode.Next() ) { + if ( entity->fl.hidden || entity->fl.isDormant || !entity->IsType( idActor::Type ) ) { + continue; + } + + actor = static_cast( entity ); + + // Check if this actor is in the PVS (needed?) + if ( !gameLocal.pvs.InCurrentPVS( pvs, actor->GetPVSAreas(), actor->GetNumPVSAreas() ) ) { + continue; + } + + // Check if the actor can be damaged and should be targeted by the gun + // Also, check the actor's team, only player team (humans, player) should be targetted + if ( !actor->fl.takedamage || actor->GetHealth() <= 0 || actor->team != 0 ) { + if ( !actor->InVehicle() ) { //HUMANHEAD jsh target vehicles + continue; + } + } + + // Player-specific checks -- ignore the spirit player + if ( actor->IsType( hhPlayer::Type ) ) { + player = static_cast( actor ); + if ( !player || player->IsSpiritOrDeathwalking() ) { + continue; + } + } + + // Calculate the distance to the actor + float distSqr = (actor->GetEyePosition() - GetOrigin()).LengthSqr(); + + // Ignore actors beyond the max distance + if( distSqr > firingRangeSquared ) { + continue; + } + + // Only check if this distance is closer than the others + if( distSqr > closestDistSqr ) { + continue; + } + + // Check if this actor is visible from the gun + if( !gameLocal.clip.TracePoint(TraceInfo, GetOrigin(), actor->GetEyePosition(), MASK_SHOT_BOUNDINGBOX, this) ) { + continue; + } + + if( !TraceTargetVerified(actor, TraceInfo.c.entityNum) ) { + continue; + } + + if ( actor->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast(actor); + if ( player && player->GetLastResurrectTime() ) { + if ( gameLocal.time < player->GetLastResurrectTime() + deathwalkDelay ) { + continue; + } + } + } + + closestActor = actor; + closestDistSqr = distSqr; + } + + gameLocal.pvs.FreeCurrentPVS( pvs ); + + return closestActor; +} + +/* +================ +hhMountedGun::PlayAnim +================ +*/ +void hhMountedGun::PlayAnim( int iChannelNum, const char* pAnim, int iBlendTime ) { + PlayAnim( iChannelNum, GetAnimator()->GetAnim( pAnim ), iBlendTime ); +} + +/* +================ +hhMountedGun::PlayAnim +================ +*/ +void hhMountedGun::PlayAnim( int iChannelNum, int pAnim, int iBlendTime ) { + ClearAnims( iChannelNum, iBlendTime ); + + GetAnimator()->PlayAnim( iChannelNum, pAnim, gameLocal.GetTime(), iBlendTime ); + animDoneTime = GetAnimator()->CurrentAnim( iChannelNum )->Length(); + animDoneTime = (animDoneTime > iBlendTime) ? animDoneTime - iBlendTime : animDoneTime; + + animDoneTime += gameLocal.GetTime(); +} + +/* +================ +hhMountedGun::PlayCycle +================ +*/ +void hhMountedGun::PlayCycle( int iChannelNum, const char* pAnim, int iBlendTime ) { + PlayCycle( iChannelNum, GetAnimator()->GetAnim( pAnim ), iBlendTime ); +} + +/* +================ +hhMountedGun::NextCycleAnim + +// Cycles the next animation in after the current one completes +================ +*/ +void hhMountedGun::NextCycleAnim( int iChannelNum, const char* pAnim ) { + PostEventMS( &EV_NextCycleAnim, GetAnimDoneTime() - gameLocal.GetTime(), iChannelNum, pAnim ); +} + +void hhMountedGun::Event_NextCycleAnim( int iChannelNum, const char* pAnim ) { + PlayCycle( iChannelNum, pAnim, 0 ); +} + +/* +================ +hhMountedGun::PlayCycle +================ +*/ +void hhMountedGun::PlayCycle( int iChannelNum, int pAnim, int iBlendTime ) { + ClearAnims( iChannelNum, iBlendTime ); + + GetAnimator()->CycleAnim( iChannelNum, pAnim, gameLocal.GetTime(), iBlendTime ); +} + +/* +================ +hhMountedGun::ClearAnims +================ +*/ +void hhMountedGun::ClearAnims( int iChannelNum, int iBlendTime ) { + GetAnimator()->Clear( iChannelNum, gameLocal.GetTime(), iBlendTime ); + + animDoneTime = 0; +} + +/* +===================== +hhMountedGun::Event_PostSpawn +===================== +*/ +void hhMountedGun::Event_PostSpawn() { + // Find the trackmover if it exists + trackMover = gameLocal.FindEntity( spawnArgs.GetString("target") ); + if( trackMover.IsValid() && trackMover->IsType( hhTrackMover::Type ) ) { //HUMANHEAD PCF mdl 04/26/06 - Only bind to track movers, in case target is only for triggers + SetOrigin( trackMover->GetOrigin() ); + Bind( trackMover.GetEntity(), false ); + } + + SetIdealLookAngle( GetAxis().ToAngles() ); +} + + +/* +===================== +hhMountedGun::Event_Activate +===================== +*/ +void hhMountedGun::Event_Activate( idEntity* pActivator ) { + if ( gunMode == GM_DORMANT ) { + gunMode = GM_IDLE; + + BecomeActive( TH_THINK | TH_TICKER ); + + ProcessEvent( &EV_ScanForClosestTarget ); + } +} + +/* +===================== +hhMountedGun::Awaken +===================== +*/ + +void hhMountedGun::Awaken() { + if ( gunMode != GM_DEAD ) { + if( targetingLaser.IsValid() && !targetingLaser->IsActivated() ) { + targetingLaser->Activate( true ); + } + + PreAttack(); + fl.takedamage = true; // Gun can now be shot + + PlayAnim( ANIMCHANNEL_ALL, "open", 0 ); + NextCycleAnim( ANIMCHANNEL_ALL, "idle_open" ); + } +} + +/* +===================== +hhMountedGun::Event_Deactivate +===================== +*/ +void hhMountedGun::Event_Deactivate() { + if (gunMode != GM_DEAD) { + if( targetingLaser.IsValid() && targetingLaser->IsActivated() ) { + targetingLaser->Activate( false ); + } + + if( trackMover.IsValid() ) { + trackMover->ProcessEvent( &EV_Deactivate ); + } + + CancelEvents( &EV_ScanForClosestTarget ); + CancelEvents( &EV_StartAttack ); + + gunMode = GM_DORMANT; + fl.takedamage = false; + + StopSound( SND_CHANNEL_ANY, false ); + + PlayAnim( ANIMCHANNEL_ALL, "close", 0 ); + NextCycleAnim( ANIMCHANNEL_ALL, "idle_close" ); + } +} + +/* +===================== +hhMountedGun::Sleep + +// Very similar to dormant, except the gun continues to think and look for enemies while closed +===================== +*/ +void hhMountedGun::Sleep() { + if( targetingLaser.IsValid() && targetingLaser->IsActivated() ) { + targetingLaser->Activate( false ); + targetingLaser->SetShaderParm( SHADERPARM_MODE, 0 ); + } + + // Note: A sleeping gun doesn't disable the track mover + CancelEvents( &EV_StartAttack ); + + gunMode = GM_IDLE; + fl.takedamage = false; + + StopSound( SND_CHANNEL_ANY, false ); + + PlayAnim( ANIMCHANNEL_ALL, "close", 0 ); + NextCycleAnim( ANIMCHANNEL_ALL, "idle_close" ); +} + +//=============================================================================================================== +//=============================================================================================================== +//=============================================================================================================== +//=============================================================================================================== +//=============================================================================================================== + +void hhMountedGun::Think() { + hhAnimatedEntity::Think(); + + if (thinkFlags & TH_THINK) { + // Run the current state + switch ( gunMode ) { + case GM_DORMANT: + case GM_IDLE: + case GM_DEAD: + break; + case GM_PREATTACKING: + TurnTowardsEnemy( DEG2RAD( spawnArgs.GetFloat( "turnMax", "3" ) ), DEG2RAD( spawnArgs.GetFloat( "turnAccel", "0.2" ) ) ); + break; + case GM_ATTACKING: + TurnTowardsEnemy( DEG2RAD( spawnArgs.GetFloat( "attackTurnMax", "0.5" ) ), DEG2RAD( spawnArgs.GetFloat( "attackTurnAccel", "0.05" ) ) ); + Attack(); + break; + case GM_RELOADING: + // TODO: Handled by an event + if ( ReadyToFire() ) { // hack! + PreAttack(); + } + break; + } + } +} + +bool hhMountedGun::GetFacePosAngle( const idVec3 &sourceOrigin, float angle1, const idVec3 &targetOrigin, float &delta ) { + float diff; + float angle2; + + angle2 = hhUtils::PointToAngle( targetOrigin.x - sourceOrigin.x, targetOrigin.y - sourceOrigin.y ); + if(angle2 > angle1) { + diff = angle2 - angle1; + + if( diff > DEG2RAD(180.0f) ) { + delta = DEG2RAD(359.9f) - diff; + return false; + } + else { + delta = diff; + return true; + } + } + else { + diff = angle1 - angle2; + if( diff > DEG2RAD(180.0f) ) { + delta = DEG2RAD(359.9f) - diff; + return true; + } + else { + delta = diff; + return false; + } + } +} + +void hhMountedGun::TurnTowardsEnemy( float maxYawVelocity, float yawAccel ) { + idVec3 dirToEnemy; + idVec3 localDirToEnemy; + + bool dir; + float deltaYaw; + + if( !EnemyIsVisible() ) { // Enemy isn't valid or visible, so go to sleep until the enemy is available again + return; + } + + if ( !enemy->IsType( hhPlayer::Type ) ) { // Turn twice as fast if the enemy is not a player (to guarantee that the gun will kill other humans/creatures) + maxYawVelocity *= 2.0f; + yawAccel *= 2.0f; + } + + idVec3 currentVec = this->GetAxis()[0]; + float currentYaw = DEG2RAD( currentVec.ToYaw() ); + + // Face toward the enemy + dir = GetFacePosAngle( GetOrigin(), currentYaw, enemy->GetOrigin(), deltaYaw ); + + // Acceleration + if ( dir ) { + yawVelocity += yawAccel; + } else { + yawVelocity -= yawAccel; + } + + if ( yawVelocity > maxYawVelocity ) { + yawVelocity = maxYawVelocity; + } else if ( yawVelocity < -maxYawVelocity ) { + yawVelocity = -maxYawVelocity; + } + + deltaYaw = yawVelocity; + + // Compute the pitch to the enemy + dirToEnemy = ( ( enemy->GetEyePosition() + enemy->GetOrigin() ) * 0.5f ) - boneGun.origin; + dirToEnemy.Normalize(); + GetAxis().ProjectVector( dirToEnemy, localDirToEnemy ); + + SetIdealLookAngle( idAngles( localDirToEnemy.ToPitch(), RAD2DEG( currentYaw + deltaYaw ), 0.0f ) ); + + StartSound( "snd_rotate", SND_CHANNEL_BODY3 ); +} + +bool hhMountedGun::EnemyIsInRange() { + idVec3 dirToEnemy; + + if ( !enemy.IsValid() ) { + return false; + } + + dirToEnemy = enemy->GetEyePosition() - boneHub.origin; + if ( dirToEnemy.LengthSqr() > firingRange * firingRange ) { + return false; + } + + return true; +} + +bool hhMountedGun::EnemyCanBeAttacked() { + if ( !enemy.IsValid() ) { // No enemy + return false; + } + + // Check if the enemy is visible and is alive + if ( ClearLineOfFire() && enemy->health > 0 ) { + return true; + } + + return false; +} + +/* +===================== +hhMountedGun::Event_ScanForClosestTarget +===================== +*/ +void hhMountedGun::Event_ScanForClosestTarget() { + idActor *entity = ScanForClosestTarget(); + + enemy = entity; + if ( entity && gunMode == GM_IDLE ) { // Sleeping and found a target + Awaken(); + } + else if ( !entity && gunMode != GM_IDLE ) { // No target, so go to sleep if not already sleeping + Sleep(); + } + + PostEventSec( &EV_ScanForClosestTarget, spawnArgs.GetFloat("scanPeriod") ); +} + +void hhMountedGun::PreAttack() { + gunMode = GM_PREATTACKING; + + // Change the beam to something more threatening + targetingLaser->SetShaderParm( SHADERPARM_MODE, 1 ); + + // Play a preattack warning sound + StartSound( "snd_preattack", SND_CHANNEL_ANY ); + + // Delay before attacking + PostEventSec( &EV_StartAttack, spawnArgs.GetFloat( "attackDelay", "0.2" ) ); // Delay a bit before attacking +} + +void hhMountedGun::Event_StartAttack() { + gunMode = GM_ATTACKING; +} + +void hhMountedGun::Attack() { + hhPlayer *player = NULL; + + if ( !enemy.IsValid() ) { + return; + } + + if ( enemy->IsType( hhPlayer::Type ) ) { + player = static_cast( enemy.GetEntity() ); + if ( player->IsSpiritOrDeathwalking() ) { + return; + } + } + + // Actually fire the gun + if ( ReadyToFire() ) { + + Fire(); + + shotsPerBurstCounter++; + if ( shotsPerBurstCounter >= shotsPerBurst || enemy->GetHealth() <= 0 ) { + Reload(); + } + } +} + +void hhMountedGun::Reload() { + gunMode = GM_RELOADING; + PlayAnim( ANIMCHANNEL_ALL, "reload", 0 ); + + nextFireTime = gameLocal.GetTime() + SEC2MS( 2.0f ); // HACK: fake way of currently forcing a wait when reloading + shotsPerBurstCounter = 0; + + targetingLaser->SetShaderParm( SHADERPARM_MODE, 0 ); + + if ( enemy->GetHealth() <= 0 ) { // Enemy was killed, so clear it and wait for the gun to find a new enemy + enemy.Clear(); + } + + StopSound( SND_CHANNEL_BODY3 ); // Stop the rotation sound +} + +/* +===================== +hhMountedGun::DetermineNextFireTime +===================== +*/ +int hhMountedGun::DetermineNextFireTime() { + return gameLocal.GetTime() + burstRateOfFire; +} + +/* +===================== +hhMountedGun::ReadyToFire +===================== +*/ +bool hhMountedGun::ReadyToFire() { + return gameLocal.GetTime() > nextFireTime; +} + +/* +================ +hhMountedGun::Fire +================ +*/ +void hhMountedGun::Fire() { + idVec3 LaunchDir; + idMat3 LaunchAxis; + + PlayAnim( channelBarrel, "fireA", 0 ); + nextFireTime = DetermineNextFireTime(); + + MuzzleFlash(); + + hhProjectile* pProjectile = hhProjectile::SpawnProjectile( projectileDict ); + if( !pProjectile ) { + return; + } + + LaunchAxis = hhUtils::RandomSpreadDir( boneGun.axis, DEG2RAD(spawnArgs.GetFloat("spread"))).ToMat3(); + pProjectile->Create( this, boneGun.origin, LaunchAxis ); + pProjectile->Launch( boneGun.origin, LaunchAxis, vec3_zero ); +} + +/* +===================== +hhMountedGun::EnemyIsVisible + +// If the enemy is visible at any angle from the gun +===================== +*/ +bool hhMountedGun::EnemyIsVisible() { + trace_t traceInfo; + + if( !enemy.IsValid() || !EnemyIsInPVS() ) { + return false; + } + + if( !gameLocal.clip.TracePoint( traceInfo, GetOrigin(), enemy->GetEyePosition(), MASK_SHOT_BOUNDINGBOX, this ) ) { + return true; + } + + if( TraceTargetVerified( enemy.GetEntity(), traceInfo.c.entityNum ) ) { + return true; + } + + return false; +} + +/* +===================== +hhMountedGun::ClearLineOfFire + +// If the enemy can be shot (line of sight from the gun barrel to the enemy) +===================== +*/ +bool hhMountedGun::ClearLineOfFire() { + trace_t TraceInfo; + idVec3 BonePos; + idMat3 BoneAxis; + + if( !enemy.IsValid() || !EnemyIsInPVS() ) { + return false; + } + + if( !gameLocal.clip.TracePoint(TraceInfo, boneGun.origin, boneGun.origin + boneGun.axis[0] * firingRange, MASK_SHOT_BOUNDINGBOX, this) ) { + return false; + } + + if( !TraceTargetVerified(enemy.GetEntity(), TraceInfo.c.entityNum) ) { + return false; + } + + return true; +} + +/* +===================== +hhMountedGun::EnemyIsInPVS +===================== +*/ +bool hhMountedGun::EnemyIsInPVS() { + pvsHandle_t PVSHandle = gameLocal.pvs.SetupCurrentPVS( PVSArea ); + + bool bResult = gameLocal.pvs.InCurrentPVS( PVSHandle, enemy->GetPVSAreas(), enemy->GetNumPVSAreas() ); + + gameLocal.pvs.FreeCurrentPVS( PVSHandle ); + + return bResult; +} + +/* +================ +hhMountedGun::Killed +================ +*/ +void hhMountedGun::Killed( idEntity *pInflictor, idEntity *pAttacker, int iDamage, const idVec3 &Dir, int iLocation ) { + hhFxInfo fxInfo; + + if ( gunMode != GM_DEAD ) { + BecomeInactive( TH_THINK|TH_TICKER ); + CancelEvents( &EV_ScanForClosestTarget ); + CancelEvents( &EV_StartAttack ); + CancelEvents( &EV_NextCycleAnim ); + + fxInfo.SetNormal( -GetAxis()[2] ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfo( spawnArgs.GetString("fx_detonate"), boneHub.origin, GetAxis(), &fxInfo ); + + hhUtils::SpawnDebrisMass(spawnArgs.GetString("def_debrisspawner"), this); + + if( targetingLaser.IsValid() ) { + targetingLaser->Activate( false ); + } + SAFE_REMOVE( targetingLaser ); + SAFE_FREELIGHT( muzzleFlashHandle ); + + if( trackMover.IsValid() ) { + trackMover->ProcessEvent( &EV_Deactivate ); + } + + const char *killedModel = spawnArgs.GetString("model_killed", NULL); + if (killedModel) { + SetModel(killedModel); + UpdateVisuals(); + GetPhysics()->SetContents(0); + fl.takedamage = false; + } + + RemoveBinds(); + + StopSound( SND_CHANNEL_ANY, false ); + + StartSound( "snd_die", SND_CHANNEL_ANY); + + ActivateTargets( pAttacker ); + + gunMode = GM_DEAD; + } +} + +void hhMountedGun::jointInfo_t::Save( idSaveGame *savefile ) const { + savefile->WriteString( name ); + savefile->WriteJoint( handle ); + savefile->WriteVec3( origin ); + savefile->WriteMat3( axis ); +} + +void hhMountedGun::jointInfo_t::Restore( idRestoreGame *savefile ) { + savefile->ReadString( name ); + savefile->ReadJoint( handle ); + savefile->ReadVec3( origin ); + savefile->ReadMat3( axis ); +} + diff --git a/src/Prey/game_mountedgun.h b/src/Prey/game_mountedgun.h new file mode 100644 index 0000000..8bbae8a --- /dev/null +++ b/src/Prey/game_mountedgun.h @@ -0,0 +1,158 @@ +#ifndef __HH_MOUNTEDGUN_H +#define __HH_MOUNTEDGUN_H + +typedef enum { + GM_DORMANT, + GM_IDLE, + GM_PREATTACKING, + GM_ATTACKING, + GM_RELOADING, + GM_DEAD +} gunMode_t; + +/********************************************************************** + +hhMountedGun + +**********************************************************************/ +class hhMountedGun: public hhAnimatedEntity { + CLASS_PROTOTYPE( hhMountedGun ); + +public: + hhMountedGun(); + virtual ~hhMountedGun(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Hide(); + virtual void Show(); + +protected: + void SpawnParts(); + idVec3 DetermineTargetingLaserEndPoint(); + void Ticker(); + + virtual void Killed( idEntity *pInflictor, idEntity *pAttacker, int iDamage, const idVec3 &Dir, int iLocation ); + + int DetermineNextFireTime(); + bool ReadyToFire(); + void Fire(); + + + void MuzzleFlash(); + void UpdateMuzzleFlash(); + void AttemptToRemoveMuzzleFlash(); + void UpdateMuzzleFlashPosition(); + + void UpdateOrientation(); + void UpdateBoneInfo(); + + bool GetFacePosAngle( const idVec3 &sourceOrigin, float angle1, const idVec3 &targetOrigin, float &delta ); + void SetIdealLookAngle( const idAngles& LookAngle ); + + idActor *ScanForClosestTarget(); + + bool TraceTargetVerified( const idActor* target, int traceEntNum ) const; + +protected: + bool ClearLineOfFire(); + bool EnemyIsVisible(); + bool EnemyIsInPVS(); + + void PlayAnim( int iChannelNum, const char* pAnim, int iBlendTime ); + void PlayAnim( int iChannelNum, int pAnim, int iBlendTime ); + void PlayCycle( int iChannelNum, const char* pAnim, int iBlendTime ); + void PlayCycle( int iChannelNum, int pAnim, int iBlendTime ); + void ClearAnims( int iChannelNum, int iBlendTime ); + int GetAnimDoneTime() { return animDoneTime; } + + void NextCycleAnim( int iChannelNum, const char* pAnim ); + void Event_NextCycleAnim( int iChannelNum, const char* pAnim ); + +protected: + void Awaken(); + + void Sleep(); + + void Event_PostSpawn(); + void Event_Activate( idEntity* pActivator ); + void Event_Deactivate(); + void Event_StopRotating(); + + void Event_ScanForClosestTarget(); + void Event_StartAttack(); + +protected: + // CJR STUFF + int team; + gunMode_t gunMode; + void Think(); + void TurnTowardsEnemy( float maxYawVelocity, float yawAccel ); + + bool EnemyCanBeAttacked(); + bool EnemyIsInRange(); + void PreAttack(); + void Attack(); + void Reload(); + +protected: + hhBoneController pitchController; + + const idDict* projectileDict; + + idEntityPtr trackMover; + + idEntityPtr targetingLaser; + + struct jointInfo_t { + idStr name; + jointHandle_t handle; + idVec3 origin; + idMat3 axis; + + jointInfo_t() { Clear(); } + void Clear() { handle = INVALID_JOINT; origin.Zero(); axis = mat3_identity; } + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + }; + + jointInfo_t boneHub; + jointInfo_t boneGun; + jointInfo_t boneExhaust; + jointInfo_t boneOrigin; + + idEntityPtr enemy; + float enemyRange; + + float firingRange; + + int nextFireTime; + float burstRateOfFire; + int shotsPerBurst; + int shotsPerBurstCounter; + + renderLight_t muzzleFlash; + int muzzleFlashHandle; + int muzzleFlashEnd; + int muzzleFlashDuration; + + idAngles idealLookAngle; + idAngles prevIdealLookAngle; + + idPhysics_Parametric physicsObj; + + int animDoneTime; + + int PVSArea; + + // nla - Other vars + int channelBody; + int channelBarrel; + int deathwalkDelay; //time to wait in msecs after killing something + + // cjr vars + float yawVelocity; +}; + +#endif diff --git a/src/Prey/game_moveable.cpp b/src/Prey/game_moveable.cpp new file mode 100644 index 0000000..d6c6f1d --- /dev/null +++ b/src/Prey/game_moveable.cpp @@ -0,0 +1,492 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define PLAYER_COLLISION_PRINTF(e) if( (e)->IsType(hhPlayer::Type) ) gameLocal.Printf + +const idEventDef EV_HoverTo( "hoverTo", "v" ); +const idEventDef EV_HoverMove( "" ); +const idEventDef EV_Unhover( "unhover" ); +const idEventDef EV_FadeOutDebris( "", "f" ); + +CLASS_DECLARATION( idMoveable, hhMoveable ) + EVENT( EV_HoverTo, hhMoveable::Event_HoverTo ) + EVENT( EV_HoverMove, hhMoveable::Event_HoverMove ) + EVENT( EV_Unhover, hhMoveable::Event_Unhover ) + EVENT( EV_Touch, hhMoveable::Event_Touch ) + EVENT( EV_SpawnFxFlyLocal, hhMoveable::Event_SpawnFxFlyLocal ) + EVENT( EV_FadeOutDebris, hhMoveable::Event_StartFadingOut ) +END_CLASS + +/* +================ +hhMoveable::hhMoveable +================ +*/ +hhMoveable::hhMoveable() { +} + +hhMoveable::~hhMoveable() { + SAFE_REMOVE( fxFly ); +} + +/* +================ +hhMoveable::Spawn +================ +*/ +void hhMoveable::Spawn() { + + fl.takedamage = health > 0; + + hoverController = NULL; + nextDamageTime = 0; + + // idMoveable forces friction to (0.6f, 0.6f, friction) + float linearFriction = spawnArgs.GetFloat( "linearFriction", "0.6" ); + float angularFriction = spawnArgs.GetFloat( "angularFriction", "0.6" ); + float contactFriction = spawnArgs.GetFloat( "friction", "0.6" ); + physicsObj.SetFriction( linearFriction, angularFriction, contactFriction ); + + float gravityMagnitude = physicsObj.GetGravity().Length(); + collisionSpeed_min = hhUtils::DetermineFinalFallVelocityMagnitude( spawnArgs.GetFloat("collideDist_min"), gravityMagnitude ); + + currentChannel = SCHANNEL_ANY; + + if (spawnArgs.GetBool("walkthrough")) { + GetPhysics()->SetContents( CONTENTS_TRIGGER | CONTENTS_RENDERMODEL ); // CJR: Removed CONTENTS_CORPSE because ragdolls were colliding with these moveables + GetPhysics()->SetClipMask( MASK_SOLID | CONTENTS_MOVEABLECLIP ); + } + + // Check if the moveable wants to remove itself after a certain amount of time + float removeTime = spawnArgs.GetFloat("removeTime"); + removeOnCollision = spawnArgs.GetBool("removeOnCollision"); + + float duration = spawnArgs.GetFloat("duration"); // Ignore removeTime if duration is set -mdl + if (duration == 0.0f && !removeOnCollision && removeTime > 0.0f) { + PostEventSec(&EV_Remove, removeTime); + } + + if (!gameLocal.isClient) { + // CJR: spawn an optional flight fx system + const char *flyName = spawnArgs.GetString( "fx_fly" ); + if ( flyName && flyName[0] ) { + BroadcastFx( flyName, EV_SpawnFxFlyLocal ); + } + } + + fadeAlpha.Init(gameLocal.time, 0, 1.0f, 1.0f); + + float fadeTime = spawnArgs.GetFloat("fadeouttime"); + if (duration > 0.0f) { + if (fadeTime > duration) { + fadeTime = duration; + } + if (fadeTime > 0.0f) { + PostEventSec(&EV_FadeOutDebris, duration-fadeTime, fadeTime); + PostEventSec(&EV_Remove, duration); + } + } + + notPushableAI = spawnArgs.GetBool( "notPushableAI", "0" ); +} + +void hhMoveable::Save(idSaveGame *savefile) const { + savefile->WriteFloat( collisionSpeed_min ); + savefile->WriteInt( currentChannel ); + savefile->WriteObject( hoverController ); + savefile->WriteVec3( hoverPosition ); + savefile->WriteVec3( hoverAngle ); + savefile->WriteBool( removeOnCollision ); + savefile->WriteBool( notPushableAI ); + + savefile->WriteFloat( fadeAlpha.GetStartTime() ); // idInterpolate + savefile->WriteFloat( fadeAlpha.GetDuration() ); + savefile->WriteFloat( fadeAlpha.GetStartValue() ); + savefile->WriteFloat( fadeAlpha.GetEndValue() ); + + fxFly.Save( savefile ); + savefile->WriteInt( nextDamageTime ); +} + +void hhMoveable::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( collisionSpeed_min ); + savefile->ReadInt( currentChannel ); + savefile->ReadObject( reinterpret_cast(hoverController) ); + savefile->ReadVec3( hoverPosition ); + savefile->ReadVec3( hoverAngle ); + savefile->ReadBool( removeOnCollision ); + savefile->ReadBool( notPushableAI ); + + float set; + + savefile->ReadFloat( set ); // idInterpolate + fadeAlpha.SetStartTime( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetDuration( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetStartValue(set); + savefile->ReadFloat( set ); + fadeAlpha.SetEndValue( set ); + + fxFly.Restore( savefile ); + savefile->ReadInt( nextDamageTime ); +} + +/* +============ +hhMoveable::SquishedByDoor +============ +*/ +void hhMoveable::SquishedByDoor(idEntity *door) { + // Get rid of any moveables caught in doors + Killed(door, door, 0, vec3_origin, 0); +} + +/* +============ +hhMoveable::Killed +============ +*/ +void hhMoveable::Killed( idEntity *inflictor, idEntity *attacker, int damageAmt, const idVec3 &dir, int location ) { + fl.takedamage = false; + GetPhysics()->SetContents(0); // Turn collision off so other entities can spawn in + + if ( unbindOnDeath ) { + Unbind(); + } + + if ( renderEntity.gui[ 0 ] ) { + renderEntity.gui[ 0 ] = NULL; + } + + ActivateTargets(attacker); + + // nla - Taken from hhAI::Killed + const char *dropDef = spawnArgs.GetString( "def_drop", NULL); + if ( dropDef && *dropDef ) { + idDict args; + args.Set( "origin", physicsObj.GetOrigin().ToString() ); + gameLocal.SpawnObject( dropDef, &args ); + } + + const char *debrisDef = spawnArgs.GetString("def_debrisspawner", NULL); + if ( debrisDef && *debrisDef ) { + hhUtils::SpawnDebrisMass(debrisDef, this); + } + + PostEventMS( &EV_Remove, 0 ); +} + +/* +================ +hhMoveable::DetermineNextChannel +================ +*/ +s_channelType hhMoveable::DetermineNextChannel() { + static int NUM_CHANNELS = 6; + + currentChannel = (currentChannel + 1) % NUM_CHANNELS; + return (s_channelType)((currentChannel == SCHANNEL_ANY) ? ++currentChannel : currentChannel); +} + +/* +================ +hhMoveable::AttemptToPlayBounceSound +================ +*/ +void hhMoveable::AttemptToPlayBounceSound( const trace_t &collision, const idVec3 &velocity ) { + static const float minCollisionVelocity = 20.0f; + static const float maxCollisionVelocity = 650.0f; + s_channelType channel; + + float len = velocity * -collision.c.normal; + if( len > minCollisionVelocity && collision.c.material && !(collision.c.material->GetSurfaceFlags() & SURF_NOIMPACT) ) { + channel = DetermineNextChannel(); + if( StartSound("snd_bounce", channel) ) { + // Change volume only after we know the sound played + float volume = hhUtils::CalculateSoundVolume( len, minCollisionVelocity, maxCollisionVelocity ); + HH_SetSoundVolume( volume, channel ); + } + } +} + +/* +================ +hhMoveable::Collide +================ +*/ +bool hhMoveable::Collide( const trace_t &collision, const idVec3 &velocity ) { + idVec3 dir = velocity; + dir.Normalize(); + + if (removeOnCollision) { + StartSound( "snd_splat", SND_CHANNEL_ANY ); + PostEventMS(&EV_Remove, 0); + return true; + } + + AttemptToPlayBounceSound( collision, velocity ); + + idEntity* entity = ValidateEntity( collision.c.entityNum ); + if ( gameLocal.time > nextDamageTime && damage.Length() ) { + if ( entity ) { + if ( DetermineCollisionSpeed(entity, collision.c.point, entity->GetPhysics()->GetLinearVelocity(), GetOrigin(), GetPhysics()->GetLinearVelocity()) > collisionSpeed_min ) { + nextDamageTime = gameLocal.time + SEC2MS(0.5); //prevent multi-collision damage + entity->Damage( this, GetPhysics()->GetClipModel()->GetOwner(), dir, damage, 1.0f, INVALID_JOINT ); + } + } + } + + + if ( fxCollide.Length() && gameLocal.time > nextCollideFxTime ) { + float len = velocity * -collision.c.normal; + if (len > 50) { + hhFxInfo fxInfo; + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfo( fxCollide, collision.c.point, mat3_identity, &fxInfo ); + nextCollideFxTime = gameLocal.time + 1000; + } + } + + // CJR: Disable the flight fx effect the first time the moveable hits something + if ( fxFly.IsValid() ) { + fxFly->Nozzle( false ); + SAFE_REMOVE( fxFly ); + } + + return false; +} + +/* +================ +hhMoveable::ValidateEntity +================ +*/ +idEntity* hhMoveable::ValidateEntity( const int collisionEntityNum ) { + if( collisionEntityNum >= ENTITYNUM_WORLD ) { + return NULL; + } + + return gameLocal.entities[collisionEntityNum]; +} + +/* +================ +hhMoveable::DetermineCollisionSpeed + +Used so we only use linear velocity in our equations +================ +*/ +float hhMoveable::DetermineCollisionSpeed( const idEntity* entity, const idVec3& point1, const idVec3& velocity1, const idVec3& point2, const idVec3& velocity2 ) { + idVec3 originVector = point1 - point2; + originVector.Normalize(); + idVec3 reversedOriginVector = -originVector; + float vel1Dot = (velocity1 * reversedOriginVector); + float vel2Dot = (velocity2 * originVector); + + if( vel2Dot < 0.0f ) { + return 0.0f; + } + + idVec3 adjustedVel1 = vel1Dot * reversedOriginVector; + idVec3 adjustedVel2 = vel2Dot * originVector; + + //PLAYER_COLLISION_PRINTF(entity)( "Vel1: %s, Vel2: %s\n", adjustedVel1.ToString(), adjustedVel2.ToString() ); + + return (adjustedVel2 - adjustedVel1).Length(); +} + +/* +================ +hhMoveable::ApplyImpulse +================ +*/ +void hhMoveable::ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &impulse ) { + if ( notPushableAI && ent && ent->IsType( idAI::Type ) ) { + return; + } + GetPhysics()->ApplyImpulse( id, point, impulse ); +} + +void hhMoveable::AllowImpact( bool allow ) { + if ( allow ) { + physicsObj.EnableImpact(); + } else { + physicsObj.DisableImpact(); + } +} + +/* +======================== +hhMoveable::HoverTo +======================== +*/ +void hhMoveable::Event_HoverTo( const idVec3 &position ) { + + if ( !hoverController ) { + const char *controllerDef = spawnArgs.GetString( "def_hoverController", NULL ); + + if ( controllerDef ) { + hoverController = static_cast( gameLocal.SpawnObject( controllerDef ) ); + } + else { + hoverController = NULL; + } + } + + //gameLocal.Printf( "Hover to %s %.2f\n", position.ToString(), (float) GetPhysics()->GetMass() ); + if ( !hoverController ) { + gameLocal.Warning( "Event_HoverTo: Tried to hover with an invalid def_hoverController" ); + return; + } + + float tension; + tension = GetPhysics()->GetMass() / 64.0 * spawnArgs.GetFloat( "hover_tension", ".01" ); + + hoverPosition = position; + if ( ! spawnArgs.GetBool( "hover_gravity", "0" ) || gameLocal.GetGravityNormal().Compare(vec3_origin, VECTOR_EPSILON ) ) { + hoverAngle = GetPhysics()->GetOrigin() - position; + } + else { + hoverAngle = gameLocal.GetGravityNormal(); + } + hoverAngle.Normalize(); + + hoverController->SetTension( tension ); + hoverController->SetOrigin( hoverPosition ); + hoverController->Attach( this, true ); + + float rotation = spawnArgs.GetFloat( "hover_rotation", "0" ); + if ( rotation ) { + GetPhysics()->SetAngularVelocity( idVec3( rotation, rotation, rotation )); + } + + PostEventMS( &EV_HoverMove, 100 ); +} + + +/* +===================== +hhMoveable::HoverMove +===================== +*/ +void hhMoveable::Event_HoverMove( ) { + float dist; + float freq; // (in Hz) + float period; + float offset; + float normalizedT; + + + if ( !hoverController ) { + gameLocal.Warning( "Event_HoverMove: Tried to hover move with an invalid def_hoverController" ); + return; + } + + // Get the total distance to travel + //! Cache these + float height = GetPhysics()->GetBounds()[ 1 ][ 2 ] - GetPhysics()->GetBounds()[ 0 ][ 2 ]; + dist = spawnArgs.GetFloat( "hover_height_frac", ".1" ) * height; + freq = spawnArgs.GetFloat( "hover_freq", ".5" ); + + if ( !freq || !dist ) { + return; + } + + period = 1 / freq; + + // Find out what offset we should be at this point in time. + normalizedT = ( gameLocal.time % (int) ( period * 1000 ) ) / (float) ( period * 1000 ); + offset = idMath::Sin( normalizedT * idMath::TWO_PI ) * dist; + + // Set our new position + hoverController->SetOrigin( hoverPosition + hoverAngle * offset ); + + // Lets do it again in 1/10th of a second + PostEventMS( &EV_HoverMove, 100 ); +} + + +/* +===================== +hhMoveable::Unhover +===================== +*/ +void hhMoveable::Event_Unhover( ) { + + if ( !hoverController ) { + gameLocal.Warning( "Event_Unhover: Tried to unhover with an invalid def_hoverController" ); + return; + } + //gameLocal.Printf( "Unhover man" ); + + hoverController->Detach( ); + + SAFE_REMOVE( hoverController ); + CancelEvents( &EV_HoverMove ); +} + +/* +================ +hhMoveable::Event_Touch +================ +*/ +void hhMoveable::Event_Touch( idEntity *other, trace_t *trace ) { + if (spawnArgs.GetBool("walkthrough")) { + idVec3 otherVel = other->GetPhysics()->GetLinearVelocity(); + float otherSpeed = otherVel.NormalizeFast(); + if (otherSpeed > 50.0f && GetPhysics()->IsAtRest()) { + idVec3 toSide = hhUtils::RandomSign() * other->GetAxis()[1]; + idVec3 toMoveable = GetOrigin() - other->GetOrigin(); + toMoveable.NormalizeFast(); + idVec3 newVel = ( 3*otherVel + 6*toSide + hhUtils::RandomVector() ) * (1.0f/10.0f); + newVel.z = 0.1f; + newVel.NormalizeFast(); + newVel *= otherSpeed*1.5f; + GetPhysics()->SetLinearVelocity(newVel); + } + } +} + +/* +================ +hhMoveable::Event_SpawnFxFlyLocal + +CJR: Based upon the code in projectiles +================ +*/ +void hhMoveable::Event_SpawnFxFlyLocal( const char* defName ) { + if( !defName || !defName[0] ) { + return; + } + + hhFxInfo fxInfo; + + fxInfo.SetNormal( -GetAxis()[0] ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( false ); + fxFly = SpawnFxLocal( defName, GetOrigin(), GetAxis(), &fxInfo ); +} + +/* +============ +hhMoveable::Event_StartFadingOut() +============ +*/ +void hhMoveable::Event_StartFadingOut(float fadetime) { + float scale = renderEntity.shaderParms[SHADERPARM_ANY_DEFORM_PARM1]; + fadeAlpha.Init( gameLocal.time, SEC2MS(fadetime), scale > 0.0f ? scale : 1.0f, 0.01f ); + BecomeActive( TH_TICKER ); +} + +/* +============ +hhMoveable::Ticker() +============ +*/ +void hhMoveable::Ticker( void ) { + SetDeformation( DEFORMTYPE_SCALE, fadeAlpha.GetCurrentValue(gameLocal.time) ); +} + diff --git a/src/Prey/game_moveable.h b/src/Prey/game_moveable.h new file mode 100644 index 0000000..8fb69f6 --- /dev/null +++ b/src/Prey/game_moveable.h @@ -0,0 +1,56 @@ +#ifndef __HH_MOVEABLE_H +#define __HH_MOVEABLE_H + + +class hhMoveable: public idMoveable { + CLASS_PROTOTYPE( hhMoveable ); + +public: + hhMoveable(); + ~hhMoveable(); + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void SquishedByDoor(idEntity *door); + virtual bool Collide( const trace_t &collision, const idVec3 &velocity ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + void ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &impulse ); + void AllowImpact( bool allow ); + + virtual void Event_HoverTo( const idVec3& position ); + virtual void Event_HoverMove( ); + virtual void Event_Unhover( ); + void Event_Touch( idEntity *other, trace_t *trace ); + void Event_SpawnFxFlyLocal( const char* defName ); + + // HUMANHEAD mdl: For the keeper + ID_INLINE void SetDamageDef( const char *damageDef ) { damage = damageDef; } + +protected: + virtual idEntity* ValidateEntity( const int collisionEntityNum ); + virtual float DetermineCollisionSpeed( const idEntity* entity, const idVec3& point1, const idVec3& velocity1, const idVec3& point2, const idVec3& velocity2 ); + virtual s_channelType DetermineNextChannel(); + + virtual void AttemptToPlayBounceSound( const trace_t &collision, const idVec3 &velocity ); + virtual void Ticker( void ); + void Event_StartFadingOut(float fadetime); + +protected: + bool removeOnCollision; + bool notPushableAI; + + float collisionSpeed_min; + int currentChannel; + int nextDamageTime; // next time movable is allowed to cause collision damage + + hhBindController * hoverController; + idVec3 hoverPosition; + idVec3 hoverAngle; + + idEntityPtr fxFly; + idInterpolate fadeAlpha; +}; + + +#endif diff --git a/src/Prey/game_mover.cpp b/src/Prey/game_mover.cpp new file mode 100644 index 0000000..c2265e1 --- /dev/null +++ b/src/Prey/game_mover.cpp @@ -0,0 +1,241 @@ +// game_mover.cpp +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//============================================================================= +// hhMover +//============================================================================= + +CLASS_DECLARATION( idMover, hhMover ) +END_CLASS + + +void hhMover::Spawn(void) { + + if ( GetPhysics()->GetClipModel() && spawnArgs.GetBool( "unblockable", "0" ) ) { + // HUMANHEAD pdm: forcing unblockable off when noclipmodel is set, not valid to have push without clipmodel + //gameLocal.Printf( "Setting to nopushsupported" ); + physicsObj.SetPusher( 0 | PUSHFL_UNBLOCKABLE ); + } + + if ( spawnArgs.GetBool( "nonsolid" ) ) { + BecomeNonSolid(); + } + +} + +void hhMover::BecomeNonSolid() { + // Somewhat copied from idMoveable + physicsObj.SetContents( CONTENTS_RENDERMODEL ); + physicsObj.SetClipMask( 0 ); +} + + +//============================================================================= +// hhMoverWallwalk +// Mover capable of doing wallwalk fading +//============================================================================= + +CLASS_DECLARATION(hhMover, hhMoverWallwalk) + EVENT(EV_Activate, hhMoverWallwalk::Event_Activate) +END_CLASS + +void hhMoverWallwalk::Spawn(void) { + wallwalkOn = spawnArgs.GetBool("active"); + flicker = spawnArgs.GetBool("flicker"); + + // Get skin references (already precached) + onSkin = declManager->FindSkin( spawnArgs.GetString("skinOn") ); + offSkin = declManager->FindSkin( spawnArgs.GetString("skinOff") ); + + if (wallwalkOn) { + SetSkin( onSkin ); + alphaOn.Init(gameLocal.time, 0, 1.0f, 1.0f); + SetShaderParm(5, 1); + } + else { + SetSkin( offSkin ); + alphaOn.Init(gameLocal.time, 0, 0.0f, 0.0f); + SetShaderParm(5, 0); + } + alphaOn.SetHermiteParms(WALLWALK_HERM_S1, WALLWALK_HERM_S2); + SetShaderParm(4, alphaOn.GetCurrentValue(gameLocal.time)); + UpdateVisuals(); +} + +void hhMoverWallwalk::Save(idSaveGame *savefile) const { + savefile->WriteFloat( alphaOn.GetStartTime() ); // hhHermiteInterpolate + savefile->WriteFloat( alphaOn.GetDuration() ); + savefile->WriteFloat( alphaOn.GetStartValue() ); + savefile->WriteFloat( alphaOn.GetEndValue() ); + savefile->WriteFloat( alphaOn.GetS1() ); + savefile->WriteFloat( alphaOn.GetS2() ); + + savefile->WriteBool(wallwalkOn); + savefile->WriteBool(flicker); + savefile->WriteSkin(onSkin); + savefile->WriteSkin(offSkin); +} + +void hhMoverWallwalk::Restore( idRestoreGame *savefile ) { + float set, set2; + + savefile->ReadFloat( set ); // hhHermiteInterpolate + alphaOn.SetStartTime( set ); + savefile->ReadFloat( set ); + alphaOn.SetDuration( set ); + savefile->ReadFloat( set ); + alphaOn.SetStartValue(set); + savefile->ReadFloat( set ); + alphaOn.SetEndValue( set ); + savefile->ReadFloat( set ); + savefile->ReadFloat( set2 ); + alphaOn.SetHermiteParms(set, set2); + + savefile->ReadBool(wallwalkOn); + savefile->ReadBool(flicker); + savefile->ReadSkin(onSkin); + savefile->ReadSkin(offSkin); +} + +void hhMoverWallwalk::Think() { + hhMover::Think(); + if (thinkFlags & TH_THINK) { + SetShaderParm(4, alphaOn.GetCurrentValue(gameLocal.time)); + if (alphaOn.IsDone(gameLocal.time)) { + BecomeInactive(TH_THINK); + if (!wallwalkOn) { + SetSkin( offSkin ); + SetShaderParm(5, 0); // Not completely on + } + else { + SetShaderParm(5, 1); // Tell material to stay on + } + } + } +} + +void hhMoverWallwalk::SetWallWalkable(bool on) { + wallwalkOn = on; + + float curAlpha = alphaOn.GetCurrentValue(gameLocal.time); + + if (wallwalkOn) { // Turning on + BecomeActive(TH_THINK); + SetSkin( onSkin ); + StartSound( "snd_powerup", SND_CHANNEL_ANY ); + alphaOn.Init(gameLocal.time, WALLWALK_TRANSITION_TIME, curAlpha, 1.0f ); + alphaOn.SetHermiteParms(WALLWALK_HERM_S1, WALLWALK_HERM_S2); + SetShaderParm(5, 0); // Not completely on + } + else { // Turning off + BecomeActive(TH_THINK); + StartSound( "snd_powerdown", SND_CHANNEL_ANY ); + alphaOn.Init(gameLocal.time, WALLWALK_TRANSITION_TIME, curAlpha, 0.0f); + alphaOn.SetHermiteParms(WALLWALK_HERM_S1, 1.0f); // no overshoot + SetShaderParm(5, 0); // Not completely on + } +} + +void hhMoverWallwalk::Event_Activate(idEntity *activator) { + SetWallWalkable(!wallwalkOn); +} + + +//============================================================================= +//============================================================================= +// +// hhExplodeMover +// +// Mover explodes from a specific point to the destination when triggered +//============================================================================= +//============================================================================= + +CLASS_DECLARATION( hhMover, hhExplodeMover ) + EVENT( EV_PostSpawn, hhExplodeMover::Event_PostSpawn ) + EVENT( EV_Activate, hhExplodeMover::Event_Trigger ) +END_CLASS + +void hhExplodeMover::Spawn(void) { + moveDelay = spawnArgs.GetFloat( "moveDelay", "0" ); + PostEventMS( &EV_PostSpawn, 0 ); +} + +void hhExplodeMover::Event_PostSpawn( void ) { + // Set up the destination location after this object is triggered + dest_position = spawnArgs.GetVector( "destOrigin", "0 0 0" ); +} + +void hhExplodeMover::Save(idSaveGame *savefile) const { + savefile->WriteInt(oldContents); + savefile->WriteInt(oldClipMask); + savefile->WriteFloat(moveDelay); +} + +void hhExplodeMover::Restore( idRestoreGame *savefile ) { + savefile->ReadInt(oldContents); + savefile->ReadInt(oldClipMask); + savefile->ReadFloat(moveDelay); +} + +void hhExplodeMover::Event_Trigger( idEntity *activator ) { + // When triggered, blast self to the original position + oldContents = GetPhysics()->GetContents(); + GetPhysics()->SetContents( 0 ); + + oldClipMask = GetPhysics()->GetClipMask(); + GetPhysics()->SetClipMask( 0 ); + + BeginMove( NULL ); +} + +void hhExplodeMover::DoneMoving( void ) { + idMover::DoneMoving(); + GetPhysics()->SetContents( oldContents ); + GetPhysics()->SetClipMask( oldClipMask ); +} + +//============================================================================= +//============================================================================= +// +// hhExplodeMoverOrigin +// +// Origin object for exploding movers. +//============================================================================= +//============================================================================= + +CLASS_DECLARATION( idEntity, hhExplodeMoverOrigin ) + EVENT( EV_Activate, hhExplodeMoverOrigin::Event_Trigger ) +END_CLASS + +void hhExplodeMoverOrigin::Spawn(void) { +} + +void hhExplodeMoverOrigin::Event_Trigger( idEntity *activator ) { + int count = 0; + +/*FIXME: Should use this format instead for speed +for ( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if ( ent->IsType( idLight::Type ) ) { + idLight *light = static_cast(ent); + } +}*/ + // When triggered, locate explode movers than have this as their origin entity, and then trigger them + for (int i = 0; i < gameLocal.num_entities; i++ ) { + idEntity *ent = gameLocal.entities[i]; + if ( !ent || !ent->IsType( hhExplodeMover::Type ) ) { + continue; + } + + hhExplodeMover *mover = static_cast(ent); + if( mover->targets.Num() > 0 && mover->targets[0].GetEntity() == this ) { // Trigger all explode movers that have this origin as a target + mover->PostEventSec( &EV_Activate, mover->GetMoveDelay(), this ); + count++; + } + } +} diff --git a/src/Prey/game_mover.h b/src/Prey/game_mover.h new file mode 100644 index 0000000..9dc4557 --- /dev/null +++ b/src/Prey/game_mover.h @@ -0,0 +1,73 @@ +#ifndef __PREY_MOVER_H__ +#define __PREY_MOVER_H__ + + +class hhMover : public idMover { + CLASS_PROTOTYPE( hhMover ); + +public: + void Spawn(); + void BecomeNonSolid(); + void Save( idSaveGame *savefile ) const { } + void Restore( idRestoreGame *savefile ) { Spawn(); } +}; + + +class hhMoverWallwalk : public hhMover { + CLASS_PROTOTYPE(hhMoverWallwalk); + +public: + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void SetWallWalkable(bool on); + void Think(); + +protected: + void Event_Activate(idEntity *activator); + +protected: + hhHermiteInterpolate alphaOn; // Degree to which wallwalk is on + bool wallwalkOn; + bool flicker; + const idDeclSkin *onSkin; + const idDeclSkin *offSkin; +}; + + +//============================================================================= + +class hhExplodeMover : public hhMover { + CLASS_PROTOTYPE( hhExplodeMover ); + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void DoneMoving( void ); + + float GetMoveDelay( void ) { return moveDelay; } + +protected: + void Event_PostSpawn( void ); + void Event_Trigger( idEntity *activator ); + +protected: + int oldContents; // contents before moving (contents are none while moving) + int oldClipMask; + float moveDelay; +}; + +class hhExplodeMoverOrigin : public idEntity { + CLASS_PROTOTYPE( hhExplodeMoverOrigin ); + +public: + void Spawn(); +protected: + void Event_PostSpawn( void ); + void Event_Trigger( idEntity *activator ); +}; + +//============================================================================= + +#endif // __PREY_MOVER_H__ diff --git a/src/Prey/game_note.cpp b/src/Prey/game_note.cpp new file mode 100644 index 0000000..0514a6e --- /dev/null +++ b/src/Prey/game_note.cpp @@ -0,0 +1,29 @@ +// Game_note.cpp +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +CLASS_DECLARATION(hhConsole, hhNote) +END_CLASS + + +void hhNote::Spawn(void) { + + if ( renderEntity.gui[0] ) { + renderEntity.gui[0]->SetStateString("guitext", spawnArgs.GetString("text", "no text")); + + // For overriding the text that is defined in the Map file. Used when we put a note + // in as a placeholder for obsolete items. The text key may already be used (consoles), + // so use this to override the text. + idStr override; + if (spawnArgs.GetString("textoverride", "", override)) { + renderEntity.gui[0]->SetStateString("guitext", override.c_str()); + } + } +} + + diff --git a/src/Prey/game_note.h b/src/Prey/game_note.h new file mode 100644 index 0000000..8058036 --- /dev/null +++ b/src/Prey/game_note.h @@ -0,0 +1,13 @@ + +#ifndef __GAME_NOTE_H__ +#define __GAME_NOTE_H__ + +class hhNote : public hhConsole { +public: + CLASS_PROTOTYPE( hhNote ); + + void Spawn( void ); +}; + + +#endif /* __GAME_NOTE_H__ */ diff --git a/src/Prey/game_organtrigger.cpp b/src/Prey/game_organtrigger.cpp new file mode 100644 index 0000000..769429a --- /dev/null +++ b/src/Prey/game_organtrigger.cpp @@ -0,0 +1,190 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +/* +add particles to bone +make wait: -1 work (removes self now) +*/ + +#include "prey_local.h" + +const idEventDef EV_PlayPainIdle("", NULL); +const idEventDef EV_ResetOrgan("", NULL); + +CLASS_DECLARATION( hhAnimatedEntity, hhOrganTrigger ) + EVENT( EV_Enable, hhOrganTrigger::Event_Enable ) + EVENT( EV_Disable, hhOrganTrigger::Event_Disable ) + EVENT( EV_PlayIdle, hhOrganTrigger::Event_PlayIdle) + EVENT( EV_PlayPainIdle, hhOrganTrigger::Event_PlayPainIdle) + EVENT( EV_ResetOrgan, hhOrganTrigger::Event_ResetOrgan) + EVENT( EV_PostSpawn, hhOrganTrigger::Event_PostSpawn ) +END_CLASS + + +//-------------------------------- +// hhOrganTrigger::~hhOrganTrigger +//-------------------------------- +hhOrganTrigger::~hhOrganTrigger() { + SAFE_REMOVE( trigger ); +} + +//-------------------------------- +// hhOrganTrigger::Event_PostSpawn +//-------------------------------- +void hhOrganTrigger::Event_PostSpawn() { + SpawnTrigger(); +} + +//-------------------------------- +// hhOrganTrigger::Spawn +//-------------------------------- +void hhOrganTrigger::Spawn( void ) { + fl.takedamage = true; + GetPhysics()->SetContents( CONTENTS_SOLID ); + + idleAnim = GetAnimator()->GetAnim( "idle" ); + painAnim = GetAnimator()->GetAnim( "pain" ); + resetAnim = GetAnimator()->GetAnim( "reset" ); + painIdleAnim = GetAnimator()->GetAnim( "painidle" ); + + trigger = NULL; + if (!gameLocal.isClient) { + if (gameLocal.isMultiplayer) { + PostEventMS(&EV_PostSpawn, 0); + } else { //spawn right now. i don't want to break save/load stuff... + SpawnTrigger(); + } + } + + ProcessEvent( &EV_PlayIdle ); + + ProcessEvent( (spawnArgs.GetBool("enabled", "1")) ? &EV_Enable : &EV_Disable ); +} + +void hhOrganTrigger::Save(idSaveGame *savefile) const { + savefile->WriteInt( idleAnim ); + savefile->WriteInt( painAnim ); + savefile->WriteInt( resetAnim ); + savefile->WriteInt( painIdleAnim ); + trigger.Save(savefile); +} + +void hhOrganTrigger::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( idleAnim ); + savefile->ReadInt( painAnim ); + savefile->ReadInt( resetAnim ); + savefile->ReadInt( painIdleAnim ); + trigger.Restore(savefile); +} + +//-------------------------------- +// hhOrganTrigger::SpawnTrigger +//-------------------------------- +void hhOrganTrigger::SpawnTrigger() { + idDict args = spawnArgs; + + args.SetVector( "mins", idVec3(-1.0f, -1.0f, -1.0f) ); + args.SetVector( "maxs", idVec3(1.0f, 1.0f, 1.0f) ); + args.SetBool( "noTouch", true ); + args.SetBool( "enabled", false ); + args.Delete( "spawnclass" ); + args.Delete( "name" ); + args.Delete( "model" ); + trigger = gameLocal.SpawnObject( spawnArgs.GetString("def_trigger"), &args ); + if( trigger.IsValid() ) { + //Bound entities are removed when their master is removed + trigger->Bind( this, true ); + } +} + +//-------------------------------- +// hhOrganTrigger::Killed +//-------------------------------- +void hhOrganTrigger::Killed( idEntity* inflictor, idEntity* attacker, int damage, const idVec3& dir, int location ) { + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, painAnim, gameLocal.GetTime(), 100 ); + int opentime = GetAnimator()->GetAnim( painAnim )->Length(); + PostEventMS( &EV_PlayPainIdle, opentime ); + + if( !trigger.IsValid() ) { + return; + } + + trigger->TriggerAction( attacker ); + + Disable(); + + if( trigger->wait >= 0 ) { + PostEventMS( &EV_ResetOrgan, trigger->nextTriggerTime - gameLocal.GetTime() ); + } +} + +//-------------------------------- +// hhOrganTrigger::Enable +//-------------------------------- +void hhOrganTrigger::Enable() { + fl.takedamage = true; + + if( trigger.IsValid() ) { + trigger->Enable(); + } +} + +//-------------------------------- +// hhOrganTrigger::Disable +//-------------------------------- +void hhOrganTrigger::Disable() { + fl.takedamage = false; + + if( trigger.IsValid() ) { + trigger->Disable(); + } +} + +//-------------------------------- +// hhOrganTrigger::Event_Enable +//-------------------------------- +void hhOrganTrigger::Event_Enable() { + Enable(); +} + +//-------------------------------- +// hhOrganTrigger::Event_Disable +//-------------------------------- +void hhOrganTrigger::Event_Disable() { + Disable(); +} + +//-------------------------------- +// hhOrganTrigger::Event_PlayIdle +//-------------------------------- +void hhOrganTrigger::Event_PlayIdle( void ) { + Enable(); + + if( idleAnim ) { + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, idleAnim, gameLocal.GetTime(), 0 ); + } +} + +//-------------------------------- +// hhOrganTrigger::Event_PlayPainIdle +//-------------------------------- +void hhOrganTrigger::Event_PlayPainIdle( void ) { + if( painIdleAnim ) { + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, painIdleAnim, gameLocal.GetTime(), 100 ); + } +} + +//-------------------------------- +// hhOrganTrigger::Event_ResetOrgan +//-------------------------------- +void hhOrganTrigger::Event_ResetOrgan( void ) { + if( resetAnim ) { + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, resetAnim, gameLocal.GetTime(), 100 ); + int opentime = GetAnimator()->GetAnim( resetAnim )->Length(); + PostEventMS( &EV_PlayIdle, opentime ); + } +} \ No newline at end of file diff --git a/src/Prey/game_organtrigger.h b/src/Prey/game_organtrigger.h new file mode 100644 index 0000000..4d2d60e --- /dev/null +++ b/src/Prey/game_organtrigger.h @@ -0,0 +1,41 @@ +#ifndef __GAME_ORGANTRIGGER_H__ +#define __GAME_ORGANTRIGGER_H__ + +extern const idEventDef EV_ModelDoorOpen; +extern const idEventDef EV_ModelDoorClose; + +class hhOrganTrigger : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhOrganTrigger ); + + virtual ~hhOrganTrigger(); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Killed( idEntity* inflictor, idEntity* attacker, int damage, const idVec3& dir, int location ); + +protected: + void SpawnTrigger(); + + virtual void Enable(); + virtual void Disable(); + + void Event_Enable(); + void Event_Disable(); + void Event_PlayIdle( void ); + void Event_PlayPainIdle( void ); + void Event_ResetOrgan( void ); + virtual void Event_PostSpawn(); + +private: + int idleAnim; + int painAnim; + int resetAnim; + int painIdleAnim; + + idEntityPtr trigger; +}; + +#endif /* __GAME_ORGANTRIGGER_H__ */ diff --git a/src/Prey/game_player.cpp b/src/Prey/game_player.cpp new file mode 100644 index 0000000..6231228 --- /dev/null +++ b/src/Prey/game_player.cpp @@ -0,0 +1,7825 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define DAMAGE_INDICATOR_TIME 1100 // Update this in hud_damageindicator.guifragment too + +const idEventDef EV_PlayWeaponAnim( "playWeaponAnim", "sd" ); +const idEventDef EV_RechargeHealth( "", NULL ); +const idEventDef EV_RechargeRifleAmmo( "", NULL ); +const idEventDef EV_Cinematic( "cinematic", "dd" ); +const idEventDef EV_DialogStart( "dialogStart", "ddd" ); +const idEventDef EV_DialogStop( "dialogStop", NULL ); +const idEventDef EV_LotaTunnelMode( "lotaTunnelMode", "d" ); +const idEventDef EV_DrainSpiritPower( "", NULL ); +const idEventDef EV_SpawnDeathWraith( "", NULL ); +const idEventDef EV_PrepareToResurrect( "prepareToResurrect" ); +const idEventDef EV_ResurrectScreenFade( "" ); +const idEventDef EV_Resurrect( "resurrect" ); +const idEventDef EV_PrepareForDeathWorld( "", NULL ); +const idEventDef EV_EnterDeathWorld( "", NULL ); +const idEventDef EV_AdjustSpiritPowerDeathWalk( "" ); +const idEventDef EV_SetOverlayMaterial( "setOverlayMaterial", "sd" ); +const idEventDef EV_SetOverlayTime( "setOverlayTime", "fd" ); +const idEventDef EV_SetOverlayColor( "setOverlayColor", "ffff" ); +const idEventDef EV_DDAHeartbeat( "", NULL ); +const idEventDef EV_ShouldRemainAlignedToAxial( "shouldRemainAlignedToAxial", "d" ); +const idEventDef EV_StartHudTranslation( "" ); +const idEventDef EV_Unfreeze( "" ); +const idEventDef EV_GetSpiritPower( "getSpiritPower", "", 'f' ); //rww +const idEventDef EV_SetSpiritPower( "setSpiritPower", "f" ); //rww +const idEventDef EV_OnGround( "onGround", NULL, 'f' ); // bg +const idEventDef EV_BindUnfroze( "", "e" ); // mdl +const idEventDef EV_LockWeapon( "lockWeapon", "d" ); // mdl +const idEventDef EV_UnlockWeapon( "unlockWeapon", "d" ); // mdl +const idEventDef EV_SetPrivateCameraView( "setPrivateCameraView", "Ed" ); //rdr +const idEventDef EV_SetCinematicFOV( "setCinematicFOV", "ffff" ); //rdr +const idEventDef EV_StopSpiritWalk( "stopSpiritWalk" ); //rww +const idEventDef EV_DamagePlayer( "damagePlayer", "eevsfd" ); //rww +const idEventDef EV_GetSpiritProxy( "getSpiritProxy", "", 'e' ); //jsh +const idEventDef EV_IsSpiritWalking( "isSpiritWalking", "", 'd' ); // pdm +const idEventDef EV_IsDeathWalking( "isDeathWalking", "", 'd' ); // bjk +const idEventDef EV_GetDDAValue( "getDDAValue", "", 'f' ); // cjr +const idEventDef EV_AllowLighter( "allowLighter", "d" ); // jsh +const idEventDef EV_DisableSpirit( "disableSpirit" ); // mdl +const idEventDef EV_EnableSpirit( "enableSpirit" ); // mdl +const idEventDef EV_UpdateDDA( "" ); // cjr +const idEventDef EV_AllowDamage( "" ); // mdl +const idEventDef EV_IgnoreDamage( "" ); // mdl +const idEventDef EV_RespawnCleanup( "" ); //rww +const idEventDef EV_ReturnToWeapon( "returnToWeapon", "", 'd' ); //bjk +const idEventDef EV_CanAnimateTorso( "canAnimateTorso", "", 'd' ); //rww + +CLASS_DECLARATION( idPlayer, hhPlayer ) + EVENT( EV_PlayWeaponAnim, hhPlayer::Event_PlayWeaponAnim ) + EVENT( EV_RechargeHealth, hhPlayer::Event_RechargeHealth ) + EVENT( EV_RechargeRifleAmmo, hhPlayer::Event_RechargeRifleAmmo ) + EVENT( EV_Cinematic, hhPlayer::Event_Cinematic ) + EVENT( EV_DialogStart, hhPlayer::Event_DialogStart ) + EVENT( EV_DialogStop, hhPlayer::Event_DialogStop ) + EVENT( EV_LotaTunnelMode, hhPlayer::Event_LotaTunnelMode ) + EVENT( EV_DrainSpiritPower, hhPlayer::Event_DrainSpiritPower ) + EVENT( EV_SpawnDeathWraith, hhPlayer::Event_SpawnDeathWraith ) + EVENT( EV_PrepareToResurrect, hhPlayer::Event_PrepareToResurrect ) + EVENT( EV_ResurrectScreenFade, hhPlayer::Event_ResurrectScreenFade ) + EVENT( EV_Resurrect, hhPlayer::Event_Resurrect ) + EVENT( EV_EnterDeathWorld, hhPlayer::Event_EnterDeathWorld ) + EVENT( EV_PrepareForDeathWorld, hhPlayer::Event_PrepareForDeathWorld ) + EVENT( EV_AdjustSpiritPowerDeathWalk, hhPlayer::Event_AdjustSpiritPowerDeathWalk ) + EVENT( EV_ShouldRemainAlignedToAxial, hhPlayer::Event_ShouldRemainAlignedToAxial ) + EVENT( EV_OrientToGravity, hhPlayer::Event_OrientToGravity ) + EVENT( EV_ResetGravity, hhPlayer::Event_ResetGravity ) + EVENT( EV_SetOverlayMaterial, hhPlayer::Event_SetOverlayMaterial ) + EVENT( EV_SetOverlayTime, hhPlayer::Event_SetOverlayTime ) + EVENT( EV_SetOverlayColor, hhPlayer::Event_SetOverlayColor ) + EVENT( EV_DDAHeartbeat, hhPlayer::Event_DDAHeartBeat ) + EVENT( EV_StartHudTranslation, hhPlayer::Event_StartHUDTranslation) + EVENT( EV_Unfreeze, hhPlayer::Event_Unfreeze) + EVENT( EV_GetSpiritPower, hhPlayer::Event_GetSpiritPower) //rww + EVENT( EV_SetSpiritPower, hhPlayer::Event_SetSpiritPower) //rww + EVENT( EV_OnGround, hhPlayer::Event_OnGround ) // bg + EVENT( EV_LockWeapon, hhPlayer::Event_LockWeapon ) // mdl + EVENT( EV_UnlockWeapon, hhPlayer::Event_UnlockWeapon ) // mdl + EVENT( EV_SetPrivateCameraView, hhPlayer::Event_SetPrivateCameraView ) //rdr + EVENT( EV_SetCinematicFOV, hhPlayer::Event_SetCinematicFOV ) //rdr + EVENT( EV_StopSpiritWalk, hhPlayer::Event_StopSpiritWalk ) //rww + EVENT( EV_DamagePlayer, hhPlayer::Event_DamagePlayer ) //rww + EVENT( EV_GetSpiritProxy, hhPlayer::Event_GetSpiritProxy ) //rww + EVENT( EV_IsSpiritWalking, hhPlayer::Event_IsSpiritWalking ) //pdm + EVENT( EV_IsDeathWalking, hhPlayer::Event_IsDeathWalking ) //bjk + EVENT( EV_GetDDAValue, hhPlayer::Event_GetDDAValue ) // cjr + EVENT( EV_AllowLighter, hhPlayer::Event_AllowLighter ) // jsh + EVENT( EV_DisableSpirit, hhPlayer::Event_DisableSpirit ) // mdl + EVENT( EV_EnableSpirit, hhPlayer::Event_EnableSpirit ) // mdl + EVENT( EV_UpdateDDA, hhPlayer::Event_UpdateDDA ) // cjr + EVENT( EV_AllowDamage, hhPlayer::Event_AllowDamage ) // mdl + EVENT( EV_IgnoreDamage, hhPlayer::Event_IgnoreDamage ) // mdl + EVENT( EV_RespawnCleanup, hhPlayer::Event_RespawnCleanup ) //rww + EVENT( EV_ReturnToWeapon, hhPlayer::Event_ReturnToWeapon ) //bjk + EVENT( EV_CanAnimateTorso, hhPlayer::Event_CanAnimateTorso ) //rww +END_CLASS + +//rww +CLASS_DECLARATION( hhPlayer, hhArtificialPlayer ) +END_CLASS + +/* +================= +hhPlayer::hhPlayer +================= +*/ +hhPlayer::hhPlayer( void ) : + thirdPersonCameraClipBounds( idTraceModel(idBounds(idVec3(-4, -8, -8), idVec3(8, 8, 8))) ) { + + hand = NULL; // nla + spiritProxy = NULL; + possessedTommy = NULL; + talon = NULL; + handNext = NULL; + deathLookAtEntity = NULL; + guiWantsControls = NULL; + lighterHandle = -1; + nextTalonAttackCommentTime = 0; + bTalonAttackComment = false; + bSpiritWalk = false; + bDeathWalk = false; + bReallyDead = false; + bPossessed = false; + bInCinematic = false; + bFrozen = false; + bScopeView = false; //rww + bInDeathwalkTransition = false; + lastAppliedBobCycle = 0; +#if GAMEPAD_SUPPORT // VENOM BEGIN + lastAutoLevelTime = 0; + lastAccelTime = 0; + lastAccelFactor = 1.0f; +#endif // VENOM END + cinematicFOV.Init( gameLocal.time, 0.f, 0.f, 0.f, 90.f, 90.f ); //rdr + mpHitFeedbackTime = 0; //rww + bAllowSpirit = true; //mdl + bCollidingWithPortal = false; + bPlayingLowHealthSound = false; + bLotaTunnelMode = false; + + for (int ix=0; ix 8 ) { + // Go to the highest weapon available if no weapon is current selected (NOTE: This will be ignored if all weapons are locked) + NextBestWeapon(); //HUMANHEAD bjk + } + + //HUMANHEAD PCF mdl 04/26/06 - Made this a separate if block from above so we can disallow locked weapons + if ( ! ( weaponFlags & ( 1 << ( idealWeapon - 1 ) ) ) ) { + // If the current weapon is invalid now, try the next weapon + NextWeapon(); + if ( ! ( weaponFlags & ( 1 << ( idealWeapon - 1 ) ) ) ) { + // No weapons available + if ( weapon.GetEntity() ) { + weapon.GetEntity()->PutAway(); + weapon.GetEntity()->HideWeapon(); + } + idealWeapon = 0; + currentWeapon = -1; + } + } + + nextSpiritTime = 0; + + mpHitFeedbackTime = 0; //rww + + airAttackerTime = 0; + bDeathWalkStage2 = false; + + physicsObj.SetInwardGravity(-1); //rww +} + +void hhPlayer::RestorePersistantInfo( void ) { + int num; + + idPlayer::RestorePersistantInfo(); + + // Update persistent amoo maximums + num = GetWeaponNum("weaponobj_soulstripper"); + assert(num); + weaponInfo[ num ].ammoMax = spawnArgs.GetInt( "max_ammo_energy" ); + + num = GetWeaponNum("weaponobj_bow"); + assert(num); + weaponInfo[ num ].ammoMax = spawnArgs.GetInt( "max_ammo_spiritpower" ); +} + +/* +============== +hhPlayer::SpawnTalon() +============== +*/ +void hhPlayer::TrySpawnTalon() { + // TODO: Deal with co-op issues. Have multiple Talons in co-op, or just one? + if (inventory.requirements.bCanSummonTalon && !talon.IsValid()) { + talon = (hhTalon *)gameLocal.SpawnObject( spawnArgs.GetString( "def_hawkpower" ), NULL ); + if( talon.IsValid() ) { + talon->SetOwner( this ); + talon->SummonTalon(); + bTalonAttackComment = true; + nextTalonAttackCommentTime = 0; + } + } +} + +/* +============== +hhPlayer::TalonAttackComment + +Talon is attacking an enemy, so have Tommy make a comment +============== +*/ +void hhPlayer::TalonAttackComment() { + if ( IsDeathWalking() ) { + return; + } + + // Play a sound on the player to encourage Talon to attack + if ( bTalonAttackComment && gameLocal.GetTime() > nextTalonAttackCommentTime ) { // Player can make an attack comment + int num = spawnArgs.GetInt( "numAttackComments", "6" ); + + StartSound( va( "snd_talonattack%d", gameLocal.random.RandomInt( num ) ), SND_CHANNEL_VOICE, 0, false, NULL ); + nextTalonAttackCommentTime = gameLocal.GetTime() + spawnArgs.GetInt( "talonAttackCommentTime", "30000" ); + } +} + +/* +============== +hhPlayer::~hhPlayer() + +Release any resources used by the player. +============== +*/ +hhPlayer::~hhPlayer() { + //rww - remove me if i'm a pilot + if (InVehicle() && GetVehicleInterface() && GetVehicleInterface()->GetVehicle()) { + GetVehicleInterface()->GetVehicle()->EjectPilot(); + } + + StopSpiritWalk(); + + RemoveResources(); +} + + +void hhPlayer::RemoveResources() { + LighterOff(); + SAFE_REMOVE( hand ); //FIXME: Should this be something other than a safe remove? (ie, remove hand?) + SAFE_REMOVE( talon ); + SAFE_REMOVE( spiritProxy ); + SAFE_REMOVE( possessedTommy ); +} + + +/* +============== +hhPlayer::Init() + +Called every time a client is placed fresh in the world: +after the first ClientBegin, and after each respawn +Initializes all non-persistant parts of playerState +Called during SpawnToPoint +============== +*/ +void hhPlayer::Init() { + oldCmdAngles.Zero(); + + idPlayer::Init(); + + // initialize the script variables + AI_ASIDE = false; + + bInDeathwalkTransition = false; + bShowProgressBar = false; + progressBarValue = 0.0f; + progressBarState = 0; + progressBarGuiValue.Init(gameLocal.time, 0, 0.0f, 0.0f); + + LighterOff(); //rww - make sure lighter is removed first (primarily for mp) + + bobFrac = 0.0f; + lighterTemperature = 0; + lighterHandle = -1; + bPossessed = false; // cjr - player is unpossessed at the start + possessionTimer = 0; // cjr - player is unpossessed at the start + possessionFOV = 0; // cjr - no FOV change for possession at start + nextTalonAttackCommentTime = 0; + bTalonAttackComment = false; // cjr - If Tommy can make comments about talon attacking enemies + bSpiritWalk = false; // cjr - initialize spiritwalk variables + spiritProxy = NULL; // cjr - initialize spiritwalk variables + possessedTommy = NULL; // cjr - initialize possessed tommy + lastWeaponSpirit = 0; // cjr - initialize spiritwalk variables + guiWantsControls = NULL; // pdm - gui that controls should be routed to + bClampYaw = false; // pdm - whether do clamp yaw to master's axis (for rail rides) + maxRelativeYaw = 180.0f; // pdm - max deviation from master axis allowed when clamping yaw + maxRelativePitch = 0.0f; // rww - max deviation from master axis allowed when clamping pitch + bInCinematic = false; // pdm - our simple cinematic system (position locking) + lockView = false; + preCinematicWeapon = 0; + preCinematicWeaponFlags = 0; + guiOverlay = NULL; // pdm - current overlay gui + spiritWalkToggleTime = 0; + bLotaTunnelMode = false; + + StopSound(SND_CHANNEL_HEART, false); //rww - make sure this is stopped here, could conceivably still be playing on respawn + bPlayingLowHealthSound = false; + + // Set shuttle view off + if (entityNumber == gameLocal.localClientNum) { //rww + renderSystem->SetShuttleView( false ); + } + + if (gameLocal.isMultiplayer) { //rww - reset my really-deadness when i respawn in MP + bReallyDead = false; + SetSkinByName(NULL); //make sure we are not using spiritwalk skin. + } + + SetViewAnglesSensitivity(1.0f); + cameraInterpolator.SetSelf( this ); + + for (int ix=0; ixIsType(hhForceField::Type) || ent->IsType(hhDeathWraith::Type)) ) { + idPlayer::ApplyImpulse( ent, id, point, impulse ); + } +} + +/* +================ +hhPlayer::CL_UpdateProgress + Client function to update progress related variables + called after getting a snapshot, or directly if a local client +================ +*/ +void hhPlayer::CL_UpdateProgress(bool bBar, float value, int state) { + idUserInterface *_hud = NULL; + + if (InVehicle() && GetVehicleInterfaceLocal() && GetVehicleInterfaceLocal()->GetHUD()) { + _hud = GetVehicleInterfaceLocal()->GetHUD(); + } + else { + _hud = hud; + } + + // Update progress bar + if (bBar && !bShowProgressBar) { + _hud->HandleNamedEvent("ShowProgressBar"); + } + else if (!bBar && bShowProgressBar) { + _hud->HandleNamedEvent("HideProgressBar"); + } + + // See if we just entered a victory state + if (state != progressBarState) { + switch (state) { + case 0: + _hud->HandleNamedEvent("ProgressNone"); + break; + case 1: + _hud->HandleNamedEvent("ProgressVictory"); + break; + case 2: + _hud->HandleNamedEvent("ProgressFailure"); + break; + } + } + + // Interpolate gui value to target value + float curValue = progressBarGuiValue.GetCurrentValue(gameLocal.time); + progressBarGuiValue.Init(gameLocal.time, 500, curValue, value); + + // Update variables + bShowProgressBar = bBar; + progressBarValue = value; + progressBarState = state; +} + +// This called once per tick, put things in here that should be updated regardless of frame rate +// PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +void hhPlayer::UpdateHud( idUserInterface *_hud ) { + idPlayer *aimed; + + if ( !_hud ) { + return; + } + + if ( entityNumber != gameLocal.localClientNum ) { + return; + } + +#define PICKUPTIME_FADEIN_START 0 +#define PICKUPTIME_FADEIN_END 500 +#define PICKUPTIME_FADEOUT_START 1750 +#define PICKUPTIME_FADEOUT_END 2000 +#define MULTIPLEITEM_SPEEDUP 7 + int c = inventory.pickupItemNames.Num(); + int time; + int i; + int pickupTimeFadeInStart, pickupTimeFadeInStop, pickupTimeFadeOutStart, pickupTimeFadeOutStop; + float scale = 1.0f / (((c-1.0f)/9.0f) * (MULTIPLEITEM_SPEEDUP-1.0f) + 1.0f); + pickupTimeFadeInStart = PICKUPTIME_FADEIN_START; + pickupTimeFadeInStop = PICKUPTIME_FADEIN_END; + pickupTimeFadeOutStart = PICKUPTIME_FADEOUT_START * scale; + pickupTimeFadeOutStop = PICKUPTIME_FADEOUT_END * scale; + + // Determine fade in color for icons + if (c > 0) { + for ( i = 0; i < c; i++ ) { + inventory.pickupItemNames[i].time += gameLocal.msec; + time = inventory.pickupItemNames[i].time; + + if (time < pickupTimeFadeInStart) { + inventory.pickupItemNames[i].matcolorAlpha = 0.0f; + } + else if (time < pickupTimeFadeInStop) { + inventory.pickupItemNames[i].matcolorAlpha = ((float)(time-pickupTimeFadeInStart))/(pickupTimeFadeInStop-pickupTimeFadeInStart); + } + else { + inventory.pickupItemNames[i].matcolorAlpha = 1.0f; + } + } + + // Determine fadeout of slot zero icon + inventory.pickupItemNames[0].slotZeroTime += gameLocal.msec; + time = inventory.pickupItemNames[0].slotZeroTime; + if (time < pickupTimeFadeOutStart) { + inventory.pickupItemNames[0].matcolorAlpha = 1.0f; + } + else if (time < pickupTimeFadeOutStop) { + inventory.pickupItemNames[0].matcolorAlpha = 1.0f - ((float)(time-pickupTimeFadeOutStart))/(pickupTimeFadeOutStop-pickupTimeFadeOutStart); + } + else { + inventory.pickupItemNames[0].matcolorAlpha = 0.0f; + } + + // Remove any expired icons + time = inventory.pickupItemNames[0].slotZeroTime; + if (time > pickupTimeFadeOutStop) { + // icon has faded out, remove from list and slide them down + inventory.pickupItemNames.RemoveIndex(0); + } + } + + // HUMANHEAD pdm: Changed aim logic to always show color-coded aimee + if ( MPAim != -1 && gameLocal.entities[ MPAim ] + && gameLocal.entities[ MPAim ]->IsType( idPlayer::Type ) ) { + + aimed = static_cast< idPlayer * >( gameLocal.entities[ MPAim ] ); + _hud->SetStateString( "aim_text", gameLocal.userInfo[ MPAim ].GetString( "ui_name" ) ); + idVec4 teamColor = aimed->GetTeamColor(); + _hud->SetStateFloat( "aim_R", teamColor.x ); + _hud->SetStateFloat( "aim_G", teamColor.y ); + _hud->SetStateFloat( "aim_B", teamColor.z ); + + //HUMANHEAD rww - health display on hud for gameplay testing + if (g_showAimHealth.GetBool()) { + _hud->SetStateInt("aim_health", gameLocal.entities[MPAim]->health); + } + //HUMANHEAD END + } + else { + _hud->SetStateString( "aim_text", "" ); + + //HUMANHEAD rww + _hud->SetStateInt("aim_health", 0); + //HUMANHEAD END + } + +#if !GOLD + _hud->SetStateInt( "g_showProjectilePct", g_showProjectilePct.GetInteger() ); + if ( numProjectilesFired ) { + _hud->SetStateString( "projectilepct", va( "Hit %% %.1f", ( (float) numProjectileHits / numProjectilesFired ) * 100 ) ); + } else { + _hud->SetStateString( "projectilepct", "Hit % 0.0" ); + } +#endif + + if (gameLocal.isNewFrame) { //HUMANHEAD rww - only on newframe + // Update low health sound + if ( health > 0 && health < 25 && !IsSpiritOrDeathwalking() ) { + if (!bPlayingLowHealthSound) { + StartSound("snd_lowhealth", SND_CHANNEL_HEART, 0, false, NULL); + bPlayingLowHealthSound = true; + } + } + else { + if (bPlayingLowHealthSound) { + StopSound(SND_CHANNEL_HEART, false); + bPlayingLowHealthSound = false; + } + } + } + + // Handle damage indictors + idVec3 localDamageVector; + idVec3 toEnemy; + for (int ix=0; ixHandleNamedEvent( va("Attacked%i", ix) );//Notify hud we were attacked + _hud->SetStateBool("displayDamageIndicators", true); + lastAttackers[ix].displayed = true; + } + + toEnemy = lastAttackers[ix].attacker->GetOrigin() - GetOrigin(); + toEnemy.z = 0.0f; + GetAxis().ProjectVector( toEnemy, localDamageVector ); + float yawToEnemy = idMath::AngleNormalize180(localDamageVector.ToYaw()); + _hud->SetStateFloat( va("yawToEnemy%i", ix), yawToEnemy); + if (gameLocal.time > lastAttackers[ix].time + DAMAGE_INDICATOR_TIME) { + lastAttackers[ix].attacker = NULL; + + bool allExpired = true; + for (int jx=0; jxSetStateBool("displayDamageIndicators", false); + } + } + } + } + + if (gameLocal.isMultiplayer) { + bool bLagged = isLagged && gameLocal.isMultiplayer && (gameLocal.localClientNum == entityNumber); + _hud->SetStateBool("hudLag", bLagged); + } + // HUMANHEAD PCF pdm 06-26-06: Fix for multiplayer shuttle hud elements appearing in SP after a MP session + else { + _hud->SetStateBool( "ismultiplayer", false ); + } + // HUMANHEAD END +} + +// PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +void hhPlayer::UpdateHudWeapon(bool flashWeapon) { + idUserInterface *_hud = hhPlayer::hud; + + // if updating the hud of a followed client + if ( gameLocal.localClientNum >= 0 && gameLocal.entities[ gameLocal.localClientNum ] && gameLocal.entities[ gameLocal.localClientNum ]->IsType( idPlayer::Type ) ) { + idPlayer *p = static_cast< idPlayer * >( gameLocal.entities[ gameLocal.localClientNum ] ); + if ( p->spectating && p->spectator == entityNumber ) { + assert( p->hud ); + _hud = p->hud; + } + } + + if ( _hud ) { + // HUMANHEAD pdm: changed to suit our needs + _hud->SetStateInt( "currentweapon", GetCurrentWeapon() ); + _hud->SetStateInt( "idealweapon", GetIdealWeapon() ); + //HUMANHEAD PCF mdl 05/05/06 - Added !IsLocked( GetIdealWeapon() ) + if ( flashWeapon && !IsLocked( GetIdealWeapon() ) ) { + _hud->HandleNamedEvent( "weaponChange" ); + } + } +} + +// PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +void hhPlayer::UpdateHudAmmo(idUserInterface *_hud) { + assert( _hud ); + float ammoPct, altPct; + int ammoType, altAmmoType; + float ammo, altAmmo; + bool ammoLow, altAmmoLow; + + // HUMANHEAD pdm: Weapon switch overlay, using rover to spread the expense over frames + static int rover = 0; + if (++rover > 9) { + rover = 0; + } + + if (rover) { + bool bHeld = false; + ammoPct = 0.0f; + altPct = 0.0f; + ammoType = weaponInfo[rover].ammoType; + altAmmoType = altWeaponInfo[rover].ammoType; + ammo = inventory.ammo[ammoType]; + altAmmo = inventory.ammo[altAmmoType]; + ammoLow = false; + altAmmoLow = false; + + if ( inventory.weapons & ( 1 << rover ) ) { + // have this weapon + bHeld = true; + ammoPct = ammo / weaponInfo[rover].ammoMax; + altPct = altAmmo / altWeaponInfo[rover].ammoMax; + ammoLow = ammo > 0 && ammo <= weaponInfo[rover].ammoLow; + altAmmoLow = altAmmo > 0 && altAmmo <= altWeaponInfo[rover].ammoLow; + } + + // Spirit ammo doesn't display in a bar + if ( ammoType == 1 ) { + ammoType = 0; + altAmmoType = 0; + } + + // Zero out alt-ammo bar if appropriate + if (altAmmoType == 0 || altAmmoType == ammoType) { + altPct = 0.0f; + } + + _hud->SetStateBool (va("weapon%d_held", rover), bHeld); + _hud->SetStateBool (va("weapon%d_ammolow", rover), ammoLow); + _hud->SetStateBool (va("weapon%d_altammolow", rover), altAmmoLow); + _hud->SetStateFloat(va("weapon%d_ammo", rover), ammoPct); + _hud->SetStateFloat(va("weapon%d_altammo", rover), altPct); + _hud->SetStateBool (va("weapon%d_ammoempty", rover), ammoType != 0 && ammo == 0 && altAmmo == 0); + } + + // Update the current weapon's ammo status + bool bDisallowAmmoBars = false; + ammoPct = 0.0f; + altPct = 0.0f; + ammoLow = false; + altAmmoLow = false; + ammoType = 0; + altAmmoType = 0; + ammo = 0; + altAmmo = 0; + + if( bLotaTunnelMode || privateCameraView || IsLocked(idealWeapon) || !weapon.IsValid() || !weapon->IsLinked() || currentWeapon == -1) { + // Don't display ammo bar for invalid weapons, or when weapons are locked + bDisallowAmmoBars = true; + } + else { + if (currentWeapon >= 0 && currentWeapon < 15) { + ammoType = weaponInfo[currentWeapon].ammoType; + altAmmoType = altWeaponInfo[currentWeapon].ammoType; + ammo = inventory.ammo[ammoType]; + altAmmo = inventory.ammo[altAmmoType]; + ammoPct = ammo / weaponInfo[currentWeapon].ammoMax; + altPct = altAmmo / altWeaponInfo[currentWeapon].ammoMax; + ammoLow = ammo > 0 && ammo <= weaponInfo[currentWeapon].ammoLow; + altAmmoLow = altAmmo > 0 && altAmmo <= altWeaponInfo[currentWeapon].ammoLow; + } + + if (ammoType == 1) { + bDisallowAmmoBars = true; + } + } + + if ( bDisallowAmmoBars ) { + _hud->SetStateBool( "player_ammobar", false); + _hud->SetStateBool( "player_altammobar", false ); + } + else { + _hud->SetStateBool( "player_ammobar", ammoType != 0); + _hud->SetStateBool( "player_altammobar", altAmmoType != 0 && altAmmoType != ammoType ); + + _hud->SetStateFloat( "player_ammopercent", ammoPct ); + _hud->SetStateFloat( "player_altammopercent", altPct ); + _hud->SetStateString( "player_ammoamounttext", ammo<0 ? "" : va("%d", ammo) ); + _hud->SetStateString( "player_altammoamounttext", altAmmo<0 ? "" : va("%d", altAmmo) ); + _hud->SetStateBool( "player_ammolow", ammoLow ); + _hud->SetStateBool( "player_altammolow", altAmmoLow ); + } + +} + +/* +================ +hhPlayer::UpdateHudStats + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +================ +*/ +void hhPlayer::UpdateHudStats( idUserInterface *_hud ) { + + assert( _hud ); + + int spiritPower = GetSpiritPower(); + + // Disallow health if wrench is locked on a non-lota map --or-- if in a private camera + bool disallowHealth = bLotaTunnelMode || (IsLocked(1) && !gameLocal.IsLOTA()) || privateCameraView != NULL; + bool disallowProgress = bLotaTunnelMode || privateCameraView != NULL || IsDeathWalking(); + + if (disallowHealth) { + int blah = 0; + } + else { + int blah2 = 0; + } + + _hud->SetStateBool("invehicle", InVehicle()); + _hud->SetStateBool("deathwalking", IsDeathWalking()); + _hud->SetStateBool("spiritwalking", IsSpiritWalking()); + _hud->SetStateBool("lighter", IsLighterOn() ); + _hud->SetStateBool("showhealth", !disallowHealth); + _hud->SetStateBool("allowprogress", !disallowProgress); + _hud->SetStateBool("showspiritpower", (!disallowHealth && !InVehicle() && (inventory.requirements.bCanSpiritWalk || IsDeathWalking()))); + _hud->SetStateFloat("lightertemp", lighterTemperature); + _hud->SetStateFloat( "player_spiritpercent", ((float)spiritPower)/inventory.maxSpirit ); + _hud->SetStateFloat( "player_healthpercent", ((float)health)/inventory.maxHealth ); + _hud->SetStateFloat( "player_healthR", inventory.maxHealth > 100 ? 0.75f : 0.6f ); + _hud->SetStateFloat( "player_healthG", inventory.maxHealth > 100 ? 0.85f : 0.0f ); + _hud->SetStateFloat( "player_healthB", inventory.maxHealth > 100 ? 1.0f : 0.0f ); + _hud->SetStateFloat( "player_healthPulseR", inventory.maxHealth > 100 ? 1.0f : 1.0f ); + _hud->SetStateFloat( "player_healthPulseG", inventory.maxHealth > 100 ? 1.0f : 0.0f ); + _hud->SetStateFloat( "player_healthPulseB", inventory.maxHealth > 100 ? 1.0f : 0.0f ); + _hud->SetStateInt( "player_health", health ); + _hud->SetStateInt( "player_maxhealth", inventory.maxHealth ); //rww + _hud->SetStateString( "player_spirit", spiritPower<0 ? "0" : va("%d", spiritPower) ); + _hud->SetStateFloat( "progress", progressBarGuiValue.GetCurrentValue(gameLocal.time) ); + + if ( healthPulse ) { + _hud->HandleNamedEvent( "healthPulse" ); + StartSound( "snd_healthpulse", SND_CHANNEL_ITEM, 0, false, NULL ); + healthPulse = false; + } + if ( spiritPulse ) { + _hud->HandleNamedEvent( "spiritPulse" ); + StartSound( "snd_spiritpulse", SND_CHANNEL_ITEM, 0, false, NULL ); + spiritPulse = false; + } + + if ( inventory.ammoPulse ) { + _hud->HandleNamedEvent( "ammoPulse" ); + inventory.ammoPulse = false; + } + if ( inventory.weaponPulse ) { + // We need to update the weapon hud manually, but not + // the armor/ammo/health because they are updated every + // frame no matter what + UpdateHudWeapon(); + _hud->HandleNamedEvent( "weaponPulse" ); + inventory.weaponPulse = false; + } + + UpdateHudAmmo( _hud ); + + // Determine slots for our pickup icons + int c = inventory.pickupItemNames.Num(); + for ( int i=0; i<10; i++ ) { + if (iSetStateString( va( "itemicon%i", i ), inventory.pickupItemNames[i].icon ); + _hud->SetStateFloat( va( "itemalpha%i", i ), inventory.pickupItemNames[i].matcolorAlpha ); + _hud->SetStateBool( va( "itemwide%i", i), inventory.pickupItemNames[i].bDoubleWide ); + } + else { + _hud->SetStateString( va( "itemicon%i", i ), "" ); + _hud->SetStateFloat( va( "itemalpha%i", i ), 0.0f ); + _hud->SetStateBool( va( "itemwide%i", i), false ); + } + } +} + +/* +=============== +hhPlayer::DrawHUD + HUMANHEAD: This run only for local players + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +=============== +*/ +void hhPlayer::DrawHUD( idUserInterface *_hud ) { + + if (guiOverlay) { + guiOverlay->Redraw(gameLocal.realClientTime); + sysEvent_t ev; + const char *command; + ev = sys->GenerateMouseMoveEvent( -2000, -2000 ); + command = guiOverlay->HandleEvent( &ev, gameLocal.time ); +// HandleGuiCommands( this, command ); + return; + } + + // HUMANHEAD pdm: removed weapon ptr check here since ours is invalid when in a vehicle + // Also removed privateCameraView, since we want subtitles when they are on + if ( gameLocal.GetCamera() || !_hud || !g_showHud.GetBool() || pm_thirdPerson.GetBool() ) { + return; + } + + UpdateHudStats( _hud ); + + + if(weapon.IsValid()) { // HUMANHEAD + bool allowGuiUpdate = true; + //rww - update the weapon gui only if the owner is being spectated by this client, or is this client + if ( gameLocal.localClientNum != entityNumber ) { + // if updating the hud for a followed client + if ( gameLocal.localClientNum >= 0 && gameLocal.entities[ gameLocal.localClientNum ] && gameLocal.entities[ gameLocal.localClientNum ]->IsType( idPlayer::Type ) ) { + idPlayer *p = static_cast< idPlayer * >( gameLocal.entities[ gameLocal.localClientNum ] ); + if ( !p->spectating || p->spectator != entityNumber ) { + allowGuiUpdate = false; + } + } else { + allowGuiUpdate = false; + } + } + + if (allowGuiUpdate) { + weapon->UpdateGUI(); + } + } + + _hud->SetStateInt( "s_debug", cvarSystem->GetCVarInteger( "s_showLevelMeter" ) ); + + //HUMANHEAD aob: vehicle logic + if ( InVehicle() ) { + DrawHUDVehicle( _hud ); + } + else { + _hud->Redraw( gameLocal.realClientTime ); + } + //HUMANHEAD END + + // weapon targeting crosshair + if ( !GuiActive() ) { + if ( cursor ) { + UpdateCrosshairs(); + cursor->Redraw( gameLocal.realClientTime ); + } + } +} + +/* +=============== +hhPlayer::DrawHUDVehicle + HUMANHEAD: This run only for local players +=============== +*/ +void hhPlayer::DrawHUDVehicle( idUserInterface* _hud ) { + if (GetVehicleInterfaceLocal()) { + GetVehicleInterfaceLocal()->DrawHUD( _hud ); + } +} + +/* +=============== +hhPlayer::FireWeapon +=============== +*/ +void hhPlayer::FireWeapon( void ) { + idMat3 axis; + idVec3 muzzle; + + if ( privateCameraView ) { + return; + } + + if ( g_editEntityMode.GetInteger() ) { + GetViewPos( muzzle, axis ); + if ( gameLocal.editEntities->SelectEntity( muzzle, axis[0], this ) ) { + return; + } + } + + //HUMANHEAD: aob - removed ammo check because we allow weapons to change modes + if( weapon.IsValid() ) { + AI_ATTACK_HELD = true; + weapon->BeginAttack(); + } + //HUMANEHAD END +} + +//========================================================================= +// +// hhPlayer::FireWeaponAlt +// +//========================================================================= + +void hhPlayer::FireWeaponAlt( void ) { + idMat3 axis; + idVec3 muzzle; + + if ( privateCameraView ) { + return; + } + + if ( g_editEntityMode.GetBool() ) { + GetViewPos( muzzle, axis ); + if ( gameLocal.editEntities->SelectEntity( muzzle, axis[0], NULL ) ) { + return; + } + } + + if ( gameLocal.isMultiplayer && spectating ) { + static int lastSpectatorSwitch = 0; + if ( gameLocal.time > lastSpectatorSwitch + 500 ) { + spectator = gameLocal.GetNextClientNum( spectator ); + idPlayer *player = gameLocal.GetClientByNum( spectator ); + while ( player->spectating && player != this ) { + player = gameLocal.GetClientByNum(spectator); + } + lastSpectatorSwitch = gameLocal.time; + } + return; + } + + //HUMANHEAD: aob - removed ammo check because we allow weapons to change modes + if( weapon.IsValid() ) { + AI_ATTACK_HELD = true; + weapon->BeginAltAttack(); + } + //HUMANEHAD END + + if ( hud ) { + hud->HandleNamedEvent( "closeObjective" ); + } +} + +void hhPlayer::StopFiring( void ) { + idPlayer::StopFiring(); + if ( weapon.GetEntity() && weapon->IsType( hhWeaponRifle::Type ) ) { + static_cast( weapon.GetEntity() )->ZoomOut(); + } +} + +/* +=============== +hhPlayer::HasAmmo +=============== +*/ +int hhPlayer::HasAmmo( ammo_t type, int amount ) { + return inventory.HasAmmo( type, amount ); +} + +/* +=============== +hhPlayer::UseAmmo +=============== +*/ +bool hhPlayer::UseAmmo( ammo_t type, int amount ) { + return inventory.UseAmmo( type, amount ); +} + +/* +=============== +hhPlayer::NextBestWeapon +=============== +*/ +void hhPlayer::NextBestWeapon( void ) { + if ( ActiveGui() || IsSpiritOrDeathwalking() ) { + return; + } + idPlayer::NextBestWeapon(); +} + +/* +=============== +hhPlayer::SelectWeapon +=============== +*/ +void hhPlayer::SelectWeapon( int num, bool force ) { + if ( ! ( weaponFlags & ( 1 << ( num - 1 ) ) ) ) { + return; + } + if ( bFrozen || ActiveGui() || ( SkipWeapon(num) || IsSpiritOrDeathwalking() ) ) { + return; + } + idPlayer::SelectWeapon( num, force ); +} + +/* +=============== +hhPlayer::NextWeapon +=============== +*/ +void hhPlayer::NextWeapon( void ) { + if ( ActiveGui() || IsSpiritOrDeathwalking() || bFrozen ) { + // NOTE: Spirit weapon is disallowed from being cycled to using the weapon*_cycle key + return; + } + //idPlayer::NextWeapon(); + if ( !weaponEnabled || spectating || hiddenWeapon || gameLocal.inCinematic || privateCameraView || gameLocal.world->spawnArgs.GetBool( "no_Weapons" ) || health < 0 ) { + return; + } + + if ( gameLocal.isClient ) { + return; + } + + // check if we have any weapons + if ( !inventory.weapons ) { + return; + } + + const char *weap; + int w, start; + + w = idealWeapon; + start = w; + while( 1 ) { + w++; + if ( w >= MAX_WEAPONS ) { + // No weapon selected and nothing to select + if ( start == -1 ) { + idealWeapon = 0; + currentWeapon = 0; + return; + } + w = 0; + } + // Keep us from an infinite loop if no weapons are valid + if ( w == start ) { + if ( ! ( weaponFlags & ( 1 << ( w - 1 ) ) ) ) { + idealWeapon = 0; + currentWeapon = 0; + } + return; + } + weap = spawnArgs.GetString( va( "def_weapon%d", w ) ); + if ( !spawnArgs.GetBool( va( "weapon%d_cycle", w ) ) ) { + continue; + } + if ( !weap[ 0 ] ) { + continue; + } + if ( ( inventory.weapons & ( 1 << w ) ) == 0 ) { + continue; + } + // Make sure the weapon is valid for this level + if ( ! ( weaponFlags & ( 1 << ( w - 1 ) ) ) ) { + continue; + } + if ( inventory.HasAmmo( weap ) || inventory.HasAltAmmo( weap ) || spawnArgs.GetBool( va( "weapon%d_allowempty", w ) ) ) { + break; + } + } + + if ( ( w != currentWeapon ) && ( w != idealWeapon ) ) { + idealWeapon = w; + weaponSwitchTime = gameLocal.time + WEAPON_SWITCH_DELAY; + UpdateHudWeapon(); + } +} + +/* +=============== +hhPlayer::PrevWeapon +=============== +*/ +void hhPlayer::PrevWeapon( void ) { + if ( ActiveGui() || IsSpiritOrDeathwalking() || bFrozen ) { + // NOTE: Spirit weapon is disallowed from being cycled to using the weapon*_cycle key + return; + } + //idPlayer::PrevWeapon(); + if ( !weaponEnabled || spectating || hiddenWeapon || gameLocal.inCinematic || privateCameraView || gameLocal.world->spawnArgs.GetBool( "no_Weapons" ) || health < 0 ) { + return; + } + + if ( gameLocal.isClient ) { + return; + } + + // check if we have any weapons + if ( !inventory.weapons ) { + return; + } + + const char *weap; + int w = idealWeapon, start = w; + if (w == -1) { + w = MAX_WEAPONS - 1; + } + while( 1 ) { + w--; + if ( w < 0 ) { + // No weapon selected and nothing to select + if ( start == -1 ) { + idealWeapon = 0; + currentWeapon = 0; + return; + } + + w = MAX_WEAPONS - 1; + } + // Keep us from an infinite loop if no weapons are valid + if ( w == start ) { + if ( ! ( weaponFlags & ( 1 << ( w - 1 ) ) ) ) { + idealWeapon = 0; + currentWeapon = 0; + } + return; + } + weap = spawnArgs.GetString( va( "def_weapon%d", w ) ); + if ( !spawnArgs.GetBool( va( "weapon%d_cycle", w ) ) ) { + continue; + } + if ( !weap[ 0 ] ) { + continue; + } + if ( ( inventory.weapons & ( 1 << w ) ) == 0 ) { + continue; + } + // Make sure the weapon is valid for this level + if ( ! ( weaponFlags & ( 1 << ( w - 1 ) ) ) ) { + continue; + } + if ( inventory.HasAmmo( weap ) || inventory.HasAltAmmo( weap ) || spawnArgs.GetBool( va( "weapon%d_allowempty", w ) ) ) { + break; + } + } + + if ( ( w != currentWeapon ) && ( w != idealWeapon ) ) { + idealWeapon = w; + weaponSwitchTime = gameLocal.time + WEAPON_SWITCH_DELAY; + UpdateHudWeapon(); + } +} + +/* +=============== +hhPlayer::SkipWeapon +=============== +*/ +bool hhPlayer::SkipWeapon( int weaponNum ) const { + //No bow if not in spirit mode + return !IsSpiritOrDeathwalking() && weaponNum == spawnArgs.GetInt("spirit_weapon"); +} + +/* +=============== +hhPlayer::SnapDownCurrentWeapon + +HUMANHEAD: aob +=============== +*/ +void hhPlayer::SnapDownCurrentWeapon() { + if( weapon.IsValid() ) { + weapon->SnapDown(); + } +} + +/* +=============== +hhPlayer::SnapUpCurrentWeapon + +HUMANHEAD: aob +=============== +*/ +void hhPlayer::SnapUpCurrentWeapon() { + if( weapon.IsValid() ) { + weapon->SnapUp(); + } +} + +/* +=============== +hhPlayer::SelectEtherealWeapon + +HUMANHEAD: aob +=============== +*/ +void hhPlayer::SelectEtherealWeapon() { + if ( GetCurrentWeapon() != 0 ) { + lastWeaponSpirit = GetCurrentWeapon(); + } + + weaponHandState.SetPlayer( this ); + if (!gameLocal.isClient) { + int spiritWeaponIndex = spawnArgs.GetInt("spirit_weapon"); + const char *spiritWeaponName = NULL; + if (bDeathWalk || (inventory.weapons & (1 << spiritWeaponIndex)) ) { + spiritWeaponName = GetWeaponName(spiritWeaponIndex); + } + weaponHandState.Archive( spiritWeaponName, 0, (InGUIMode()) ? "guihand_normal" : NULL ); + } + + if ( weapon.IsValid() ) { + weapon->SetShaderParm( SHADERPARM_MODE, MS2SEC( gameLocal.time ) ); // Glow in the bow + weapon->SetShaderParm( SHADERPARM_MISC, 1.0f ); // Turn on the arrow + weapon->SetShaderParm( SHADERPARM_DIVERSITY, MS2SEC( gameLocal.time ) ); // Glow in the arrow + } +} + +/* +=============== +hhPlayer::PutawayEtherealWeapon + +HUMANHEAD: aob +=============== +*/ +void hhPlayer::PutawayEtherealWeapon() { + //If we haven't actually changed weapons yet, put current weapon up + if( GetCurrentWeapon() != lastWeaponSpirit ) { + SnapDownCurrentWeapon(); + } else { + SnapUpCurrentWeapon(); + } + + weaponHandState.RestoreFromArchive(); +} + +/* +=============== +hhPlayer::UpdateWeapon + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +=============== +*/ +void hhPlayer::UpdateWeapon( void ) { + + if ( IsDead() ) { // HUMANHEAD cjr: Replaced health <= 0 with IsDead() call for deathwalk override + return; + } + + assert( !spectating ); + + if ( gameLocal.isClient ) { + // clients need to wait till the weapon and it's world model entity + // are present and synchronized ( weapon.worldModel idEntityPtr to idAnimatedEntity ) + if ( !weapon.GetEntity()->IsWorldModelReady() ) { + return; + } + } + else if (gameLocal.isMultiplayer) { //rww - projectile deferring + if (weapon.IsValid()) { + weapon->CheckDeferredProjectiles(); + } + } + + // always make sure the weapon is correctly setup before accessing it + if ( weapon.GetEntity() && !weapon.GetEntity()->IsLinked() ) { + if ( idealWeapon != -1 ) { + animPrefix = spawnArgs.GetString( va( "def_weapon%d", idealWeapon ) ); + weapon.GetEntity()->GetWeaponDef( animPrefix, inventory.clip[ idealWeapon ] ); + animPrefix.Strip( "weaponobj_" ); //HUMANHEAD rww + assert( weapon.GetEntity()->IsLinked() ); + } else { + return; + } + } + + //HUMANEHAD rww + if (weapon.IsValid() && weapon->IsType(hhWeaponSoulStripper::Type)) { + hhWeaponSoulStripper *leechGun = static_cast(weapon.GetEntity()); + leechGun->CheckCans(); + } + //HUMANHEAD END + + if (!InVehicle()) { + if ( g_dragEntity.GetBool() ) { + StopFiring(); + dragEntity.Update( this ); + if ( weapon.IsValid() ) { + weapon.GetEntity()->FreeModelDef(); + } + return; + } else if (ActiveGui()) { + // gui handling overrides weapon use + Weapon_GUI(); + } else { + Weapon_Combat(); + } + + // Determine whether we are in aside state + if ( weapon.IsValid() && weapon->IsAside() ) { + if ( !AI_ASIDE ) { + AI_ASIDE = true; + SetState( "AsideWeapon" ); + UpdateScript(); + } + } else { + AI_ASIDE = false; + } + } + + //HUMANHEAD: aob - added weapon validity check + if( weapon.IsValid() ) { + // update weapon state, particles, dlights, etc + weapon->PresentWeapon( showWeaponViewModel ); + } + + // nla + if ( hand.IsValid() ) { + hand->Present(); + } +} + +/* +=============== +hhPlayer::InGUIMode +nla - Used for the GUI hand to check if it should be up. (Coming back from spiritwalk) +=============== +*/ +bool hhPlayer::InGUIMode() { + + // Logic adapted from UpdateWeapon + if ( ActiveGui() && !InVehicle() ) { + return( true ); + } + + return( false ); +} + + +/* +=============== +hhPlayer::Weapon_Combat + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +=============== +*/ +void hhPlayer::Weapon_Combat( void ) { + idMat3 axis; + idVec3 muzzle; + idDict args; + + if ( !weaponEnabled || gameLocal.inCinematic || privateCameraView ) { + return; + } + + // HUMANHEAD nla + if ( hand.IsValid() && hand->IsType( hhGuiHand::Type ) && hand->IsAttached() && !hand->IsLowering() ) { + hand->RemoveHand(); + } + // HUMANHEAD END + + if( idealWeapon != 0 && IsLocked(idealWeapon) ) { //HUMANHEAD bjk PCF (4-30-06) - fix wrench up in roadhouse + NextWeapon(); + } + + if ( idealWeapon == 0 ) { + return; + } + + RaiseWeapon(); + if ( weapon.IsValid() ) { + weapon.GetEntity()->PutUpright(); + } + + if ( weapon.IsValid() && weapon->IsReloading() ) { + if ( !AI_RELOAD ) { + AI_RELOAD = true; + SetState( "ReloadWeapon" ); + UpdateScript(); + } + } else { + AI_RELOAD = false; + } + + if ( idealWeapon != currentWeapon && (!gameLocal.isMultiplayer || inventory.lastShot[idealWeapon] < gameLocal.time) ) { //HUMANHEAD bjk PATCH 7-27-06 + if ( weaponCatchup ) { + assert( gameLocal.isClient ); +#ifndef HUMANHEAD //HUMANHEAD rww FIXME!!!! this also produces horrible memory leaks because of the dirty fire controller stuff + //HUMANHEAD rww - our crazy weapon system does not work ok with this. did we change the weapon dictionary parsing? + //it seems that it is very much dependant on the base class of weapon at the moment and that of course is going to be + //that of currentWeapon and not idealWeapon. + //currentWeapon = idealWeapon; + //HUMANHEAD END + weaponGone = false; + animPrefix = spawnArgs.GetString( va( "def_weapon%d", currentWeapon ) ); + weapon.GetEntity()->GetWeaponDef( animPrefix, inventory.clip[ currentWeapon ] ); + animPrefix.Strip( "weaponobj_" ); //HUMANHEAD pdm: changed from weapon_ to weaponobj_ per our naming convention + + weapon.GetEntity()->NetCatchup(); + const function_t *newstate = GetScriptFunction( "NetCatchup" ); + if ( newstate ) { + SetState( newstate ); + UpdateScript(); + } +#else + assert( idealWeapon >= 0 ); + assert( idealWeapon < MAX_WEAPONS ); + + animPrefix = spawnArgs.GetString( va( "def_weapon%d", idealWeapon ) ); + const char *currentWeaponName = weapon->spawnArgs.GetString("classname", ""); + //make sure the weapon i have from my snapshot, and the weapon i have selected are the same. + if (currentWeaponName && !idStr::Cmp(animPrefix, currentWeaponName)) { + weaponGone = false; + currentWeapon = idealWeapon; + + weapon->GetWeaponDef( animPrefix, inventory.clip[ currentWeapon ] ); + animPrefix.Strip( "weaponobj_" ); + weapon->Raise(); +#endif //HUMANHEAD END + weaponCatchup = false; + } + } else { + if ( weapon.IsValid() && (weapon->IsReady() || weapon->IsRising()) ) { + InvalidateCurrentWeapon();//Needed incase we change weapons quickly, we can go back to old weapon + weapon->PutAway(); + } + + if ( ( !weapon.IsValid() || weapon->IsHolstered() ) && !bDeathWalk && ! bReallyDead ) { + assert( idealWeapon >= 0 ); + assert( idealWeapon < MAX_WEAPONS ); + + if ( currentWeapon > 0 && !spawnArgs.GetBool( va( "weapon%d_toggle", currentWeapon ) ) ) { //HUMANHEAD bjk + previousWeapon = currentWeapon; + } + //HUMANHEAD PCF rww 05/03/06 - the local client might get skippiness between switching weapons if we + //attempt to raise the weapon again before validating that the weapon ent is of the type that we + //already desire to switch to. this bug is introduced by the concept of switching out entities when + //changing weapons (not client-friendly). + if (gameLocal.isClient && entityNumber == gameLocal.localClientNum) { + if (weapon.IsValid() && weapon->GetDict() && idStr::Icmp(weapon->GetDict()->GetString("classname"), GetWeaponName( idealWeapon )) == 0) { + currentWeapon = idealWeapon; + weaponGone = false; + animPrefix = GetWeaponName( currentWeapon ); + animPrefix.Strip( "weaponobj_" ); //HUMANHEAD pdm: changed from weapon_ to weaponobj_ per our naming convention + weapon.GetEntity()->Raise(); + } + } + //HUMANHEAD END + else { + currentWeapon = idealWeapon; + weaponGone = false; + + //HUMANHEAD: aob + animPrefix = GetWeaponName( currentWeapon ); + if (!gameLocal.isClient) { + SAFE_REMOVE( weapon ); + weapon = SpawnWeapon( animPrefix.c_str() ); + } + //HUMANHEAD END + + animPrefix.Strip( "weaponobj_" ); //HUMANHEAD pdm: changed from weapon_ to weaponobj_ per our naming convention + + //HUMANHEAD PCF rww 05/03/06 - safety check, make sure weapon is valid particularly for the client + if (weapon.IsValid()) { + weapon.GetEntity()->Raise(); + } + } + } + } + } else if (!bDeathWalk && !bReallyDead) { + weaponGone = false; // if you drop and re-get weap, you may miss the = false above + if ( weapon.IsValid() && weapon.GetEntity()->IsHolstered() ) { + if ( !weapon.GetEntity()->AmmoAvailable() ) { + // weapons can switch automatically if they have no more ammo + NextBestWeapon(); + } else if( !gameLocal.isMultiplayer || inventory.lastShot[idealWeapon] < gameLocal.time ) { //HUMANHEAD bjk PATCH 9-11-06 + weapon.GetEntity()->Raise(); + state = GetScriptFunction( "RaiseWeapon" ); + if ( state ) { + SetState( state ); + } + } + } + } + + if ( weapon.IsValid() ) { + weapon->PrecomputeTraceInfo(); + } + + // check for attack + AI_WEAPON_FIRED = false; + if ( ( usercmd.buttons & BUTTON_ATTACK ) && !weaponGone ) { + FireWeapon(); + } else if ( oldButtons & BUTTON_ATTACK ) { + AI_ATTACK_HELD = false; + if( weapon.IsValid() ) { + weapon.GetEntity()->EndAttack(); + } + } + + // HUMANHEAD + if ( ( usercmd.buttons & BUTTON_ATTACK_ALT ) && !weaponGone ) { + FireWeaponAlt(); + } else if ( oldButtons & BUTTON_ATTACK_ALT ) { + AI_ATTACK_HELD = false; + if( weapon.IsValid() ) { + weapon.GetEntity()->EndAltAttack(); + } + } + // HUMANHEAD END + + // update our ammo clip in our inventory + if ( weapon.IsValid() && ( currentWeapon >= 0 ) && ( currentWeapon < MAX_WEAPONS ) ) { + inventory.clip[ currentWeapon ] = weapon->AmmoInClip(); + inventory.altMode[ currentWeapon ] = weapon->GetAltMode(); + } +} + +/* +=============== +hhPlayer::SpawnWeapon +=============== +*/ +hhWeapon* hhPlayer::SpawnWeapon( const char* name ) { + hhWeapon* weaponPtr = NULL; + idDict args; + + args.SetVector( "origin", GetEyePosition() ); + args.SetMatrix( "rotation", viewAngles.ToMat3() ); + weaponPtr = static_cast( gameLocal.SpawnObject(name, &args) ); + weaponPtr->SetOwner( this ); + weaponPtr->Bind( this, true ); + + //HUMANHEAD: aob - removed last two params + weaponPtr->GetWeaponDef( name ); + + return weaponPtr; +} + +/* +===================== +hhPlayer::GetWeaponNum + HUMANHEAD +===================== +*/ +int hhPlayer::GetWeaponNum( const char* weaponName ) const { + for( int i = 1; i < MAX_WEAPONS; ++i ) { + if( !idStr::Icmp(GetWeaponName(i), weaponName) ) { + return i; + } + } + + return 0; +} + +/* +===================== +hhPlayer::GetWeaponName + HUMANHEAD +===================== +*/ +const char* hhPlayer::GetWeaponName( int num ) const { + if( num < 1 || num >= MAX_WEAPONS ) { + return NULL; + } + + return spawnArgs.GetString( va("def_weapon%d", num) ); +} + +/* +=============== +hhPlayer::Weapon_GUI + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +=============== +*/ +void hhPlayer::Weapon_GUI( void ) { + + StopFiring(); + weapon.GetEntity()->LowerWeapon(); + + // NLANOTE - Same here + // disable click prediction for the GUIs. handy to check the state sync does the right thing + if ( gameLocal.isClient && !net_clientPredictGUI.GetBool() ) { + return; + } + + if ( health <= 0 || bDeathWalk ) { //HUMANHEAD bjk PCF (5-5-06) - no hand stuff when dead + return; + } + + // HUMANHEAD nla + if ( !bDeathWalk ) { // mdl - Don't do hand stuff while deathwalking + if ( hand.IsValid() && hand->IsType( hhGuiHand::Type ) ) { + //if ( !hand->IsReady() && !hand->IsRaising() ) { + if ( !hand->IsReady() && !hand->IsRaising() && !hand->IsLowering() ) { + hand->Reraise(); + } + } + else if ( (!hand.IsValid() && weapon.IsValid() && !weapon->IsAside() ) || + (hand.IsValid() && !hand->IsType( hhGuiHand::Type ) && !hand->IsLowering() ) || + (!hand.IsValid() && idealWeapon == 0 ) ) { + if (!gameLocal.isClient) { //rww + hhHand::AddHand( this, GetGuiHandInfo() ); + } + } + } + // HUMANHEAD END + + if ( hand.IsValid() && ( oldButtons ^ usercmd.buttons ) & BUTTON_ATTACK ) { //HUMANHEAD bjk: no waiting for ready + sysEvent_t ev; + const char *command = NULL; + bool updateVisuals = false; + + idUserInterface *ui = ActiveGui(); + if ( ui ) { + ev = sys->GenerateMouseButtonEvent( 1, ( usercmd.buttons & BUTTON_ATTACK ) != 0 ); + command = ui->HandleEvent( &ev, gameLocal.time, &updateVisuals ); + if ( updateVisuals && focusGUIent && ui == focusUI ) { + focusGUIent->UpdateVisuals(); + } + } + if ( gameLocal.isClient ) { + // we predict enough, but don't want to execute commands + if (focusGUIent) { + if ( hand.IsValid() && hand->IsType( hhGuiHand::Type ) && ev.evValue2) { //rww - still predict hand + hand->SetAction( focusGUIent->spawnArgs.GetString( "pressAnim", "press" ) ); //HUMANHEAD bjk + hand->Action(); + } + } + return; + } + if ( focusGUIent ) { + HandleGuiCommands( focusGUIent, command ); + // HUMANHEAD nla - Added to handle the hand stuff. = ) + if ( hand.IsValid() && hand->IsType( hhGuiHand::Type ) && ev.evValue2) { + hand->SetAction( focusGUIent->spawnArgs.GetString( "pressAnim", "press" ) ); //HUMANHEAD bjk + hand->Action(); + } + // HUMANHEAD END + } else { + HandleGuiCommands( this, command ); + } + } +} + +/* +================ +hhPlayer::UpdateCrosshairs +HUMANHEAD PDM +================ +*/ +void hhPlayer::UpdateCrosshairs() { + bool combatCrosshair = false; + bool targeting = false; + int crosshair = 0; + + if ( !privateCameraView && !IsLocked(idealWeapon) && (!hand.IsValid() || hand->IsLowered()) && !InCinematic() && weapon.IsValid() && g_crosshair.GetInteger() ) { + weapon->UpdateCrosshairs(combatCrosshair, targeting); + crosshair = g_crosshair.GetInteger(); + } + + cursor->SetStateBool( "combatcursor", targeting ? false : combatCrosshair ); + cursor->SetStateBool( "activecombatcursor", targeting ); + cursor->SetStateInt( "crosshair", crosshair ); +} + + +/* +================ +hhPlayer::UpdateFocus + +Searches nearby entities for interactive guis, possibly making one of them +the focus and sending it a mouse move event + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +================ +*/ +void hhPlayer::UpdateFocus( void ) { + idClipModel *clipModelList[ MAX_GENTITIES ]; + idClipModel *clip; + int listedClipModels; + idEntity *oldFocus; + idEntity *ent; + idUserInterface *oldUI; + int i, j; + idVec3 start, end; + const char *command; + trace_t trace; + guiPoint_t pt; + const idKeyValue *kv; + sysEvent_t ev; + idUserInterface *ui; + + if ( gameLocal.inCinematic ) { + return; + } + + oldFocus = focusGUIent; + oldUI = focusUI; + + if ( focusTime <= gameLocal.time ) { + ClearFocus(); + } + + // don't let spectators interact with GUIs + if ( spectating ) { + return; + } + + start = GetEyePosition(); + + //HUMANHEAD rww - the actual viewAngles do not seem to be properly transformed to at this point, + //so just get a transformed direction now. + idVec3 viewDir = (untransformedViewAngles.ToMat3()*GetEyeAxis())[0]; + + end = start + viewDir * 70.0f; // was 50, doom=80 + + // HUMANHEAD pdm: Changed aim logic a bit + // player identification -> names to the hud + if ( gameLocal.isMultiplayer && entityNumber == gameLocal.localClientNum ) { + idVec3 end = start + viewDir * 768.0f; + gameLocal.clip.TracePoint( trace, start, end, MASK_SHOT_BOUNDINGBOX, this ); + if ( ( trace.fraction < 1.0f ) && ( trace.c.entityNum < MAX_CLIENTS ) ) { + MPAim = trace.c.entityNum; + } + else { + MPAim = -1; + + //HUMANHEAD rww - if we're hitting a shuttle with a pilot, set the pilot to the MPAim ent + if (trace.c.entityNum >= MAX_CLIENTS && trace.c.entityNum < MAX_GENTITIES) { + idEntity *traceEnt = gameLocal.entities[trace.c.entityNum]; + if (traceEnt && traceEnt->IsType(hhVehicle::Type)) { //is it a vehicle? + hhVehicle *traceVeh = static_cast(traceEnt); + if (traceVeh->GetPilot() && traceVeh->GetPilot()->IsType(hhPlayer::Type)) { //if it's a vehicle, does it have a player pilot? + MPAim = traceVeh->GetPilot()->entityNumber; + } + } + } + //HUMANHEAD END + } + } + + idBounds bounds( start ); + bounds.AddPoint( end ); + + listedClipModels = gameLocal.clip.ClipModelsTouchingBounds( bounds, -1, clipModelList, MAX_GENTITIES ); + + // no pretense at sorting here, just assume that there will only be one active + // gui within range along the trace + for ( i = 0; i < listedClipModels; i++ ) { + clip = clipModelList[ i ]; + ent = clip->GetEntity(); + + if ( ent->IsHidden() ) { + continue; + } + + // HUMANHEAD pdm: added support here for all guis, not just gui1 + int interactiveMask = 0; + renderEntity_t *renderEnt = ent->GetRenderEntity(); + if ( renderEnt ) { + for (int ix=0; ixgui[ix] && renderEnt->gui[ix]->IsInteractive()) { + interactiveMask |= (1<GuiTrace( ent->GetModelDefHandle(), start, end, interactiveMask ); + + if ( ent->fl.accurateGuiTrace ) { + trace_t tr; + gameLocal.clip.TracePoint(tr, start, end, CONTENTS_SOLID, this); + if (tr.fraction < pt.frac) { + continue; + } + } + + if ( pt.x != -1 ) { + // we have a hit + renderEntity_t *focusGUIrenderEntity = ent->GetRenderEntity(); + if ( !focusGUIrenderEntity ) { + continue; + } + + if ( pt.guiId == 1 ) { + ui = focusGUIrenderEntity->gui[ 0 ]; + } else if ( pt.guiId == 2 ) { + ui = focusGUIrenderEntity->gui[ 1 ]; + } else { + ui = focusGUIrenderEntity->gui[ 2 ]; + } + + if ( ui == NULL ) { + continue; + } + + ClearFocus(); + focusGUIent = ent; + focusUI = ui; + + if ( oldFocus != ent ) { + // new activation + // going to see if we have anything in inventory a gui might be interested in + // need to enumerate inventory items + focusUI->SetStateInt( "inv_count", inventory.items.Num() ); + for ( j = 0; j < inventory.items.Num(); j++ ) { + idDict *item = inventory.items[ j ]; + const char *iname = item->GetString( "inv_name" ); + const char *iicon = item->GetString( "inv_icon" ); + const char *itext = item->GetString( "inv_text" ); + + focusUI->SetStateString( va( "inv_name_%i", j), iname ); + focusUI->SetStateString( va( "inv_icon_%i", j), iicon ); + focusUI->SetStateString( va( "inv_text_%i", j), itext ); + kv = item->MatchPrefix("inv_id", NULL); + if ( kv ) { + focusUI->SetStateString( va( "inv_id_%i", j ), kv->GetValue() ); + } + // HUMANHEAD nla - Changed to pass all "passtogui_" keys to the gui + kv = item->MatchPrefix("passtogui_", NULL); + if ( kv ) { + focusUI->SetStateString( kv->GetKey(), kv->GetValue() ); + kv = item->MatchPrefix( "passtogui_", kv ); + } + // HUMANHEAD END + focusUI->SetStateInt( iname, 1 ); + } + + focusUI->SetStateString( "player_health", va("%i", health ) ); + focusUI->SetStateBool( "player_spiritwalking", IsSpiritWalking() ); // for hunterhand gui + + kv = focusGUIent->spawnArgs.MatchPrefix( "gui_parm", NULL ); + while ( kv ) { + focusUI->SetStateString( kv->GetKey(), kv->GetValue() ); + kv = focusGUIent->spawnArgs.MatchPrefix( "gui_parm", kv ); + } + } + + // clamp the mouse to the corner + ev = sys->GenerateMouseMoveEvent( -2000, -2000 ); + command = focusUI->HandleEvent( &ev, gameLocal.time ); + HandleGuiCommands( focusGUIent, command ); + + // move to an absolute position + ev = sys->GenerateMouseMoveEvent( pt.x * SCREEN_WIDTH, pt.y * SCREEN_HEIGHT ); + command = focusUI->HandleEvent( &ev, gameLocal.time ); + HandleGuiCommands( focusGUIent, command ); + focusTime = gameLocal.time + FOCUS_GUI_TIME; + break; + } + } + + if ( focusGUIent && focusUI ) { + if ( !oldFocus || oldFocus != focusGUIent ) { + command = focusUI->Activate( true, gameLocal.time ); + HandleGuiCommands( focusGUIent, command ); + //StartSound( "snd_guienter", SND_CHANNEL_ANY, 0, false, NULL ); + } + } else if ( oldFocus && oldUI ) { + command = oldUI->Activate( false, gameLocal.time ); + HandleGuiCommands( oldFocus, command ); + //StartSound( "snd_guiexit", SND_CHANNEL_ANY, 0, false, NULL ); + } +} + + +/* +=============== +hhPlayer::ApplyLandDeflect + +HUMANHEAD: aob +=============== +*/ +idVec3 hhPlayer::ApplyLandDeflect( const idVec3& pos, float scale ) { + idVec3 localPos( pos ); + int delta = 0; + float fraction = 0.0f; + + // add fall height + delta = gameLocal.time - landTime; + if ( delta < LAND_DEFLECT_TIME ) { + fraction = (float)delta / LAND_DEFLECT_TIME; + //HUMANHEAD: aob + fraction *= scale; + //HUMANHEAD END + localPos += cameraInterpolator.GetCurrentUpVector() * landChange * fraction; + } else if ( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME ) { + fraction = (float)(LAND_DEFLECT_TIME + LAND_RETURN_TIME - delta) / LAND_RETURN_TIME; + //HUMANHEAD: aob + fraction *= scale; + //HUMANHEAD END + localPos += cameraInterpolator.GetCurrentUpVector() * landChange * fraction; + } + + return localPos; +} + +/* +================= +hhPlayer::CrashLand + +Check for hard landings that generate sound events + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +================= +*/ +void hhPlayer::CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ) { + //HUMANHEAD: aob - removed unused vars + idVec3 origin; + idVec3 gravityNormal; + float delta; + waterLevel_t waterLevel; + bool noDamage; + //HUMANHEAD END + + //HUMANHEAD: aob + const trace_t& trace = physicsObj.GetGroundTrace(); + //HUMANHEAD END + + AI_SOFTLANDING = false; + AI_HARDLANDING = false; + + //HUMANHEAD: aob - added IsBound and trace check + // if the player is not on the ground + if ( (!physicsObj.HasGroundContacts() || trace.fraction == 1.0f) && !IsBound() ) { + return; + } + + //HUMANHEAD: aob - only check when we land on the ground + //If we get here we can assume we currently have ground contacts + if( physicsObj.HadGroundContacts() ) { + return; + } + //HUMANHEAD END + + gravityNormal = physicsObj.GetGravityNormal(); + + // if the player wasn't going down + if ( ( oldVelocity * -gravityNormal ) >= 0.0f ) { + return; + } + + waterLevel = physicsObj.GetWaterLevel(); + + // never take falling damage if completely underwater + if ( waterLevel == WATERLEVEL_HEAD ) { + return; + } + + // no falling damage if touching a nodamage surface + noDamage = false; + for ( int i = 0; i < physicsObj.GetNumContacts(); i++ ) { + const contactInfo_t &contact = physicsObj.GetContact( i ); + if ( contact.material->GetSurfaceFlags() & SURF_NODAMAGE ) { + noDamage = true; + break; + } + } + + //HUMANHEAD: aob - removed velocity calculations + + //HUMANHEAD: aob + idVec3 deltaVelocity = DetermineDeltaCollisionVelocity( oldVelocity, trace ); + delta = (IsBound()) ? deltaVelocity.Length() : deltaVelocity * gravityNormal; + //HUMANHEAD END + + // reduce falling damage if there is standing water + if ( waterLevel == WATERLEVEL_WAIST ) { + delta *= 0.25f; + } + if ( waterLevel == WATERLEVEL_FEET ) { + delta *= 0.5f; + } + + if ( delta < crashlandSpeed_jump || IsSpiritOrDeathwalking() ) { + return; // Early out + } + + // Calculate landing sound volume + float soundScale = hhUtils::CalculateScale( delta, crashlandSpeed_jump, crashlandSpeed_fatal ); + landChange = -32 * soundScale; + landTime = gameLocal.time; + float min = hhMath::dB2Scale( spawnArgs.GetInt("minCrashLandVolumedB", "-20") ); + float max = hhMath::dB2Scale( spawnArgs.GetInt("maxCrashLandVolumedB", "-4") ); + soundScale = min + (max - min) * soundScale; + + idVec3 fallDir = oldVelocity; + idVec3 reverseContactNormal = -physicsObj.GetGroundContactNormal(); + idEntity* entity = NULL; + float damageScale = hhUtils::CalculateScale( delta, crashlandSpeed_soft, crashlandSpeed_fatal ); + + fallDir.Normalize(); + + if( trace.fraction == 1.0f ) { + return; + } + + if( !IsBound() ) { + PlayCrashLandSound( trace, soundScale ); + gameLocal.AlertAI( this, spawnArgs.GetFloat( "land_alert_radius", "400" ) ); + } + + // Determine damage to what you're landing on + entity = gameLocal.GetTraceEntity( trace ); + if( entity && trace.c.entityNum != ENTITYNUM_WORLD ) { + entity->ApplyImpulse( this, 0, trace.c.point, (oldVelocity * reverseContactNormal) * reverseContactNormal );//Not sure if this impulse is large enough + + const char* entityDamageName = spawnArgs.GetString( "def_damageFellOnto" ); + if( *entityDamageName && damageScale > 0.0f) { + entity->Damage( this, this, fallDir, entityDamageName, damageScale, INVALID_JOINT ); + } + } + + // Calculate damage to self + const char* selfDamageName = NULL; + if ( delta < crashlandSpeed_soft ) { // Soft Fall + AI_SOFTLANDING = true; + selfDamageName = spawnArgs.GetString( "def_damageSoftFall" ); + } + else if ( delta < crashlandSpeed_fatal ) { // Hard Fall + AI_HARDLANDING = true; + selfDamageName = spawnArgs.GetString( "def_damageHardFall" ); + } + else { // Fatal Fall + AI_HARDLANDING = true; + selfDamageName = spawnArgs.GetString( "def_damageFatalFall" ); + } + + if( *selfDamageName && damageScale > 0.0f && !noDamage ) { + pain_debounce_time = gameLocal.time + pain_delay + 1; // ignore pain since we'll play our landing anim + Damage( NULL, NULL, fallDir, selfDamageName, damageScale, INVALID_JOINT ); + } +} + +/* +=============== +hhPlayer::ApplyBobCycle + +HUMANHEAD: aob +=============== +*/ +#define BOB_TO_NOBOB_RETURN_TIME 3000.0f +idVec3 hhPlayer::ApplyBobCycle( const idVec3& pos, const idVec3& velocity ) { + float delta = 0.0f; + idVec3 localViewBob( pos ); + const float maxBob = 6.0f; + + if( bob > 0.0f && !usercmd.forwardmove && !usercmd.rightmove ) {//smoothly goto bob of zero if not already there + delta = gameLocal.time - lastAppliedBobCycle; + bob *= ( 1.0f - (delta / BOB_TO_NOBOB_RETURN_TIME) ); + if( bob < 0.0f ) { + bob = 0.0f; + } + } else { + // add bob height after any movement smoothing + lastAppliedBobCycle = gameLocal.time; + bob = bobfracsin * xyspeed * pm_bobup.GetFloat(); + if( bob > maxBob ) { + bob = maxBob; + } + } + + localViewBob -= cameraInterpolator.GetCurrentUpVector() * bob; + + return localViewBob; +} + + +/* +=============== +hhPlayer::BobCycle + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +=============== +*/ +void hhPlayer::BobCycle( const idVec3 &pushVelocity ) { + float bobmove; + int old; //, deltaTime; + idVec3 vel, gravityDir, velocity; + idMat3 viewaxis; +// float bob; + float delta; + float speed; +// float f; + + // + // calculate speed and cycle to be used for + // all cyclic walking effects + // + velocity = physicsObj.GetLinearVelocity() - pushVelocity; + + //HUMANHEAD: aob - changed GetGravityNormal to GetCurrentUpVector to smooth out wallwalk + gravityDir = -cameraInterpolator.GetCurrentUpVector(); + //HUMANHEAD END + vel = velocity - ( velocity * gravityDir ) * gravityDir; + xyspeed = vel.LengthFast(); + + // do not evaluate the bob for other clients + // when doing a spectate follow, don't do any weapon bobbing + if ( gameLocal.isClient && entityNumber != gameLocal.localClientNum ) { + //HUMANHEAD rww - allow bob when following players in spectator mode + bool canBob = false; + if (gameLocal.localClientNum != -1 && gameLocal.entities[gameLocal.localClientNum] && gameLocal.entities[gameLocal.localClientNum]->IsType(hhPlayer::Type)) { + hhPlayer *pl = static_cast(gameLocal.entities[gameLocal.localClientNum]); + if (pl->spectating && pl->spectator == entityNumber) { + canBob = true; + } + } + + if (!canBob) { + //HUMANHEAD END + viewBobAngles.Zero(); + viewBob.Zero(); + return; + } + } + + if ( !physicsObj.HasGroundContacts() || influenceActive == INFLUENCE_LEVEL2 || ( gameLocal.isMultiplayer && spectating ) ) { + // airborne + bobCycle = 0; + bobFoot = 0; + bobfracsin = 0; + } else if ( ( !usercmd.forwardmove && !usercmd.rightmove ) || ( xyspeed <= MIN_BOB_SPEED ) ) { + // start at beginning of cycle again + bobCycle = 0; + bobFoot = 0; + bobfracsin = 0; + } else { + if ( physicsObj.IsCrouching() ) { + bobmove = pm_crouchbob.GetFloat(); + // ducked characters never play footsteps + } else { + // vary the bobbing based on the speed of the player + bobmove = pm_walkbob.GetFloat() * ( 1.0f - bobFrac ) + pm_runbob.GetFloat() * bobFrac; + } + + // check for footstep / splash sounds + old = bobCycle; + bobCycle = (int)( old + bobmove * gameLocal.msec ) & 255; + bobFoot = ( bobCycle & 128 ) >> 7; + bobfracsin = idMath::Fabs( sin( ( bobCycle & 127 ) / 127.0 * idMath::PI ) ); + } + + // calculate angles for view bobbing + viewBobAngles.Zero(); + + viewaxis = viewAngles.ToMat3(); + + // add angles based on velocity + delta = velocity * viewaxis[0]; + viewBobAngles.pitch += delta * pm_runpitch.GetFloat(); + + delta = velocity * viewaxis[1]; + viewBobAngles.roll -= delta * pm_runroll.GetFloat(); + + // add angles based on bob + // make sure the bob is visible even at low speeds + speed = xyspeed > 200 ? xyspeed : 200; + + delta = bobfracsin * pm_bobpitch.GetFloat() * speed; + if ( physicsObj.IsCrouching() ) { + delta *= 3; // crouching + } + viewBobAngles.pitch += delta; + delta = bobfracsin * pm_bobroll.GetFloat() * speed; + if ( physicsObj.IsCrouching() ) { + delta *= 3; // crouching accentuates roll + } + if ( bobFoot & 1 ) { + delta = -delta; + } + viewBobAngles.roll += delta; + + //HUMANHEAD: aob +#if HUMANHEAD + viewBob = ApplyBobCycle( ApplyLandDeflect(vec3_zero, 1.0f), velocity ); +#else + // calculate position for view bobbing + viewBob.Zero(); + + if ( physicsObj.HasSteppedUp() ) { + + // check for stepping up before a previous step is completed + deltaTime = gameLocal.time - stepUpTime; + if ( deltaTime < STEPUP_TIME ) { + stepUpDelta = stepUpDelta * ( STEPUP_TIME - deltaTime ) / STEPUP_TIME + physicsObj.GetStepUp(); + } else { + stepUpDelta = physicsObj.GetStepUp(); + } + if ( stepUpDelta > 2.0f * pm_stepsize.GetFloat() ) { + stepUpDelta = 2.0f * pm_stepsize.GetFloat(); + } + stepUpTime = gameLocal.time; + } + + idVec3 gravity = physicsObj.GetGravityNormal(); + + // if the player stepped up recently + deltaTime = gameLocal.time - stepUpTime; + if ( deltaTime < STEPUP_TIME ) { + viewBob += gravity * ( stepUpDelta * ( STEPUP_TIME - deltaTime ) / STEPUP_TIME ); + } + + // add bob height after any movement smoothing + bob = bobfracsin * xyspeed * pm_bobup.GetFloat(); + if ( bob > 6 ) { + bob = 6; + } + viewBob[2] += bob; + + // add fall height + delta = gameLocal.time - landTime; + if ( delta < LAND_DEFLECT_TIME ) { + f = delta / LAND_DEFLECT_TIME; + viewBob -= gravity * ( landChange * f ); + } else if ( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME ) { + delta -= LAND_DEFLECT_TIME; + f = 1.0 - ( delta / LAND_RETURN_TIME ); + viewBob -= gravity * ( landChange * f ); + } +#endif //HUMANHEAD END +} + +/* +================ +hhPlayer::UpdateDeltaViewAngles +================ +*/ +void hhPlayer::UpdateDeltaViewAngles( const idAngles &angles ) { + // set the delta angle + idAngles delta; + for( int i = 0; i < 3; i++ ) { +#if 1 //rww - revert to id's method + //delta[i] = ((angles[i] - GetUntransformedViewAngles()[i]) / GetViewAnglesSensitivity()) + GetUntransformedViewAngles()[i] - SHORT2ANGLE(usercmd.angles[i]); + delta[ i ] = angles[ i ] - SHORT2ANGLE( usercmd.angles[ i ] ); +#else // HUMANHEAD mdl: If you enable this, comment out the SetOrientation() call for vehicles in hhPlayer::Restore() + delta[i] = (angles[i] - idMath::AngleNormalize180( SHORT2ANGLE(usercmd.angles[ i ]) - oldCmdAngles[i]) * GetViewAnglesSensitivity()); +#endif + } + SetDeltaViewAngles( delta ); +} + +/* +================ +hhPlayer::DetermineViewAngles +================ +*/ +idAngles hhPlayer::DetermineViewAngles( const usercmd_t& cmd, idAngles& cmdAngles ) { + int i; + idAngles localViewAngles( GetUntransformedViewAngles() ); + idAngles deltaCmdAngles; + + if ( !noclip && ( gameLocal.inCinematic || privateCameraView || gameLocal.GetCamera() || influenceActive == INFLUENCE_LEVEL2 ) ) { + // no view changes at all, but we still want to update the deltas or else when + // we get out of this mode, our view will snap to a kind of random angle + return GetUntransformedViewAngles(); + } + + //HUMANHEAD rww - testing + /* + if (gameLoacl.isClient && gameLocal.localClientNum != entityNumber) { + idQuat a = viewAngles.ToQuat(); + idQuat b = GetUntransformedViewAngles().ToQuat(); + idQuat c; + c.Slerp(a, b, 0.3f); + return c.ToAngles(); + } + */ + //HUMANHEAD END + + // if dead + if ( IsDead() ) { +#if 0 + if ( DoThirdPersonDeath() ) { + localViewAngles.roll = 0.0f; + localViewAngles.pitch = 30.0f; + } + else { + localViewAngles.roll = 40.0f; + localViewAngles.pitch = -15.0f; + } + return localViewAngles; +#else //HUMANHEAD PCF rww 04/26/06 - look freely while dead in MP + if ( !DoThirdPersonDeath() ) { + localViewAngles.roll = 40.0f; + localViewAngles.pitch = -15.0f; + return localViewAngles; + } +#endif //HUMANHEAD END + } + + //JSHTODO this messes up multiplayer input. remerge sensitivity code + // circularly clamp the angles with deltas + if ( usercmd.buttons & BUTTON_MLOOK ) { + for ( i = 0; i < 3; i++ ) { + cmdAngles[i] = SHORT2ANGLE( usercmd.angles[i] ); +#if 1 //rww - revert to id's method + //localViewAngles[i] = idMath::AngleNormalize180( cmdAngles[i] + deltaViewAngles[i] - GetUntransformedViewAngles()[i] ) * GetViewAnglesSensitivity() + GetUntransformedViewAngles()[i]; + localViewAngles[i] = idMath::AngleNormalize180( SHORT2ANGLE( usercmd.angles[i]) + deltaViewAngles[i] ); +#else + deltaCmdAngles[i] = idMath::AngleNormalize180( cmdAngles[i] - oldCmdAngles[i] ) * GetViewAnglesSensitivity(); + oldCmdAngles[i] = cmdAngles[i]; + localViewAngles[i] = deltaCmdAngles[i] + deltaViewAngles[i]; +#endif + } + } else { +#if 1 //rww - revert to id's method + //localViewAngles.yaw = idMath::AngleNormalize180( SHORT2ANGLE( usercmd.angles[YAW] ) + deltaViewAngles[YAW] - GetUntransformedViewAngles()[YAW] ) * GetViewAnglesSensitivity() + GetUntransformedViewAngles()[YAW]; + localViewAngles.yaw = idMath::AngleNormalize180( SHORT2ANGLE( usercmd.angles[YAW]) + deltaViewAngles[YAW] ); +#if GAMEPAD_SUPPORT // VENOM BEGIN + oldCmdAngles[YAW] = SHORT2ANGLE( usercmd.angles[YAW] ); +#endif // VENOM END +#else + deltaCmdAngles[YAW] = idMath::AngleNormalize180( SHORT2ANGLE(usercmd.angles[YAW]) - oldCmdAngles[YAW] ) * GetViewAnglesSensitivity(); + oldCmdAngles[YAW] = SHORT2ANGLE( usercmd.angles[YAW] ); + localViewAngles.yaw = deltaCmdAngles[YAW] + deltaViewAngles[YAW]; +#endif + + if (!centerView.IsDone(gameLocal.time)) { + localViewAngles.pitch = centerView.GetCurrentValue(gameLocal.time); + } + } + +#if GAMEPAD_SUPPORT // VENOM BEGIN + float fpitch = idMath::Fabs(localViewAngles.pitch); + if( idMath::Abs(usercmd.rightmove) < 10 && + idMath::Abs(usercmd.forwardmove) > 80 && + fpitch > 1.f && + idMath::Fabs(oldCmdAngles[YAW] - cmdAngles[YAW]) < 1.0f && + idMath::Fabs(oldCmdAngles[PITCH] - cmdAngles[PITCH]) < 1.0f + ) + { + // dont start the leveling until we have .5 sec of valid movement + if((gameLocal.time - lastAutoLevelTime) > 500 ) { + float fadd = 0.6f; + if(fpitch < 10.f) { + fadd *= (fpitch/10.f ); + fadd+= 0.05f; + } + + if(localViewAngles.pitch < 0 ) { + localViewAngles.pitch +=fadd; + } + else { + localViewAngles.pitch -=fadd; + } + } + } + else { + lastAutoLevelTime = gameLocal.time; + } + + oldCmdAngles[YAW] = cmdAngles[YAW]; + oldCmdAngles[PITCH] = cmdAngles[PITCH]; +#endif // VENOM END + + // clamp the pitch + if ( noclip ) { + localViewAngles.pitch = hhMath::ClampFloat( -89.0f, 89.0f, localViewAngles.pitch ); + } else { + //HUMANHEAD PCF rww 04/26/06 - look freely while dead in MP + if (IsDead() && DoThirdPersonDeath()) { + localViewAngles.pitch = hhMath::ClampFloat( -45.0f, 45.0f, localViewAngles.pitch ); + } + else { + //HUMANHEAD END + localViewAngles.pitch = hhMath::ClampFloat( pm_minviewpitch.GetFloat(), pm_maxviewpitch.GetFloat(), localViewAngles.pitch ); + } + } + + // HUMANHEAD pdm: clamp the yaw (only used for bindControllers) + if ( !noclip && bClampYaw ) { + float idealYaw=0.0f; + idVec3 masterOrigin; + idMat3 masterAxis; + + if (GetMasterPosition(masterOrigin, masterAxis)) { + idealYaw = masterAxis[0].ToYaw(); + idealYaw = idMath::AngleNormalize180( idealYaw ); + } + + // Transpose the everything to "zero" space before clamping, so idealYaw is at zero in a (-180..180) normalized space + float zeroSpaceYaw = idMath::AngleNormalize180( localViewAngles.yaw - idealYaw ); + zeroSpaceYaw = idMath::ClampFloat(-maxRelativeYaw, maxRelativeYaw, zeroSpaceYaw); + localViewAngles.yaw = idMath::AngleNormalize180( zeroSpaceYaw + idealYaw ); + } + // HUMANHEAD END + + UpdateDeltaViewAngles( localViewAngles ); + + // save in the log for analyzing weapon angle offsets + loggedViewAngles[ gameLocal.framenum & (NUM_LOGGED_VIEW_ANGLES-1) ] = localViewAngles; + + return localViewAngles; +} + +/* +================ +hhPlayer::SetViewAnglesSensitivity +================ +*/ +void hhPlayer::SetViewAnglesSensitivity( float factor ) { + viewAnglesSensitivity = factor; + if (gameLocal.localClientNum == entityNumber) { + common->SetGameSensitivityFactor(factor); + } +} + +/* +================ +hhPlayer::GetViewAnglesSensitivity +================ +*/ +float hhPlayer::GetViewAnglesSensitivity() const { + return viewAnglesSensitivity; +} + +/* +================ +hhPlayer::UpdateViewAngles +================ +*/ +void hhPlayer::UpdateViewAngles( void ) { + if( InVehicle() ) { + return; + } + + if (!noclip && (InCinematic() && lockView)) { + // no view changes at all, but we still want to update the deltas or else when + // we get out of this mode, our view will snap to a kind of random angle + UpdateDeltaViewAngles( viewAngles ); + return; + } + + //HUMANHEAD: aob - moved logic into helper function so we can call this somewhere else while in a vehicle + viewAngles = DetermineViewAngles( usercmd, cmdAngles ); + + //HUMANHEAD rww - moved angle smoothing code here + // update the smoothed view angles + if (gameLocal.isClient) { + if ( gameLocal.framenum >= smoothedFrame && entityNumber != gameLocal.localClientNum ) { + idAngles anglesDiff = viewAngles - smoothedAngles; + anglesDiff.Normalize180(); + if ( idMath::Fabs( anglesDiff.yaw ) < 90.0f && idMath::Fabs( anglesDiff.pitch ) < 90.0f ) { + // smoothen by pushing back to the previous angles + viewAngles -= gameLocal.clientSmoothing * anglesDiff; + viewAngles.Normalize180(); + } + smoothedAngles = viewAngles; + } + smoothedOriginUpdated = false; + } + + // orient the model towards the direction we're looking + UpdateOrientation( viewAngles ); + //HUMANHEAD END +} + +/* +===================== +hhPlayer::UpdateOrientation + +HUMANHEAD: aob +===================== +*/ +void hhPlayer::UpdateOrientation( const idAngles& newUntransformedViewAngles ) { + idAngles angles; + //This is where the camera interpolator transforms the viewAngles to work with wallwalk. + //We also store the original untransformed view angles for use in some other code. + //idPlayer called SetAxis, but only used the yaw but because we are transforming our view angles + //we do something similier by taking the untransformed yaw and rotating by the camera interpolator's axis. + //This becomes our new axis, the camera interpolator axis plus the untransformed view angles yaw + SetUntransformedViewAngles( newUntransformedViewAngles ); + + angles = GetUntransformedViewAngles(); + if( IsWallWalking() ) { + angles.pitch = 0.0f; + } + SetUntransformedViewAxis( idAngles( 0.0f, angles.yaw, 0.0f ).ToMat3() ); + + viewAngles = cameraInterpolator.UpdateViewAngles( angles ); + SetAxis( TransformToPlayerSpace(GetUntransformedViewAxis()) ); +} + +//============================================================================= +// +// hhPlayer::CheckFOV +// +// Similar to idActor::CheckFOV, but isn't infinite along the vertical plane +// +// CJR +//============================================================================= + +bool hhPlayer::CheckFOV( const idVec3 &pos ) { + if ( fovDot == 1.0f ) { + return true; + } + + float dot; + idVec3 delta; + + delta = pos - GetEyePosition(); + delta.Normalize(); + + dot = delta * viewAngles.ToForward(); + + return ( dot >= fovDot ); +} + +//============================================================================= +// hhPlayer::CheckFOV +// jsh - Similar to CheckFOV, but only checks yaw +//============================================================================= +bool hhPlayer::CheckYawFOV( const idVec3 &pos ) { + if ( fovDot == 1.0f ) { + return true; + } + + float dot; + idVec3 delta; + idVec3 viewAng; + + delta = pos - GetEyePosition(); + delta.z = 0; + delta.Normalize(); + viewAng = viewAngles.ToForward(); + viewAng.z = 0; + dot = delta * viewAng; + + return ( dot >= fovDot ); +} + + +/* +===================== +hhPlayer::SetOrientation + +HUMANHEAD: aob +===================== +*/ +void hhPlayer::SetOrientation( const idVec3& origin, const idMat3& bboxAxis, const idVec3& lookDir, const idAngles& newUntransformedViewAngles ) { + //never let the untransformed view angles have a roll value + idAngles modifiedUntransViewAngles = newUntransformedViewAngles; + modifiedUntransViewAngles.roll = 0.0f; + + physicsObj.SetOrigin( GetLocalCoordinates(origin) ); + physicsObj.SetAxis( bboxAxis ); + physicsObj.CheckWallWalk( true ); + + SetViewAngles( modifiedUntransViewAngles ); + BufferLoggedViewAngles( modifiedUntransViewAngles ); + SetUntransformedViewAngles( modifiedUntransViewAngles ); + + SetAxis( lookDir.ToMat3() ); + SetUntransformedViewAxis( idAngles(0.0f, modifiedUntransViewAngles.yaw, 0.0f).ToMat3() ); + + cameraInterpolator.Reset( origin, bboxAxis[2], EyeHeightIdeal() ); + + UpdateViewAngles(); + UpdateVisuals(); +} +/* +===================== +hhPlayer::RestoreOrientation + +HUMANHEAD: mdl +Same as SetOrientation, except doesn't check wallwalk, since the materials may not be set before the first frame. +===================== +*/ +void hhPlayer::RestoreOrientation( const idVec3& origin, const idMat3& bboxAxis, const idVec3& lookDir, const idAngles& newUntransformedViewAngles ) { + physicsObj.SetOrigin( GetLocalCoordinates(origin) ); + physicsObj.SetAxis( bboxAxis ); + + SetViewAngles( newUntransformedViewAngles ); + BufferLoggedViewAngles( newUntransformedViewAngles ); + SetUntransformedViewAngles( newUntransformedViewAngles ); + + SetAxis( lookDir.ToMat3() ); + SetUntransformedViewAxis( idAngles(0.0f, newUntransformedViewAngles.yaw, 0.0f).ToMat3() ); + cameraInterpolator.Reset( origin, bboxAxis[2], EyeHeightIdeal() ); + + UpdateViewAngles(); + UpdateVisuals(); +} + +/* +===================== +hhPlayer::BufferLoggedViewAngles + +HUMANHEAD: aob - used when teleporting and using portals +===================== +*/ +void hhPlayer::BufferLoggedViewAngles( const idAngles& newUntransformedViewAngles ) { + idAngles deltaAngles = newUntransformedViewAngles - GetUntransformedViewAngles(); + + for( int ix = 0; ix < NUM_LOGGED_VIEW_ANGLES; ++ix ) { + loggedViewAngles[ix] += deltaAngles; + } +} + +/* +================ +hhPlayer::UpdateFromPhysics + +AOBMERGE - PERSISTANT MERGE +================ +*/ +void hhPlayer::UpdateFromPhysics( bool moveBack ) { + + // set master delta angles for actors + if ( GetBindMaster() ) { + if( !InVehicle() ) { + idAngles delta = GetDeltaViewAngles(); + if ( moveBack ) { + delta.yaw -= physicsObj.GetMasterDeltaYaw(); + } else { + delta.yaw += physicsObj.GetMasterDeltaYaw(); + } + + SetDeltaViewAngles( delta ); + } else { + SetUntransformedViewAxis( mat3_identity ); + SetUntransformedViewAngles( GetUntransformedViewAxis().ToAngles() ); + viewAngles = GetUntransformedViewAngles(); + SetAxis( GetBindMaster()->GetAxis() ); + + //AOB - now that we have an updated viewAxis we need to update the + //camera. Feels like a hack! + cameraInterpolator.SetTargetAxis( GetAxis(), INTERPOLATE_NONE ); + } + } + + UpdateVisuals(); +} + +/* +============== +hhPlayer::PerformImpulse +============== +*/ + +void hhPlayer::PerformImpulse( int impulse ) { + if( InVehicle() && GetVehicleInterface()->InvalidVehicleImpulse(impulse) ) { + return; + } + + idPlayer::PerformImpulse( impulse ); + + switch( impulse ) { + case IMPULSE_14: { + if ( weapon.IsValid() && weapon->IsType( hhWeaponZoomable::Type ) ) { + hhWeaponZoomable *weap = static_cast(weapon.GetEntity()); + if ( weap && weap->GetAltMode() ) { + weap->ZoomInStep(); + } else { + NextWeapon(); + } + } else { + NextWeapon(); + } + break; + } + case IMPULSE_15: { + if ( weapon.IsValid() && weapon->IsType( hhWeaponZoomable::Type ) ) { + hhWeaponZoomable *weap = static_cast(weapon.GetEntity()); + if ( weap && weap->GetAltMode() ) { + weap->ZoomOutStep(); + } else { + PrevWeapon(); + } + } else { + PrevWeapon(); + } + break; + } + case IMPULSE_16: + // Toggle Lighter + if (inventory.requirements.bCanUseLighter && weaponFlags != 0) { // mdl: Disable lighter if all weapons are disabled + if (!gameLocal.isClient) { + ToggleLighter(); + } + } + break; + case IMPULSE_25: + // Throw grenade + if ( weaponFlags != 0 && !ActiveGui()) { // mdl: Disable if all weapons are disabled + if (!gameLocal.isClient) { + ThrowGrenade(); + } + } + break; + case IMPULSE_54: + // Spirit Power key + if ( IsDeathWalking() ) { // CJR: Developer-only ability to quickly return from DeathWalk by hitting the spirit key + if ( developer.GetBool() && ( gameLocal.time - deathWalkTime > spawnArgs.GetInt( "deathWalkMinTime", "4000" ) ) ) { // Force the player to stay in deathwalk for a short period of time + deathWalkFlash = 0.0f; + PostEventMS( &EV_PrepareToResurrect, 0 ); + } + } + else if (inventory.requirements.bCanSpiritWalk) { + ToggleSpiritWalk(); + } + break; + + case IMPULSE_18: { +#if GAMEPAD_SUPPORT // VENOM BEGIN + idAngles localViewAngles( GetUntransformedViewAngles() ); + centerView.Init(gameLocal.time, 360, localViewAngles.pitch, 0); +#else + centerView.Init(gameLocal.time, 200, viewAngles.pitch, 0); +#endif // VENOM END + break; + } + } +} + +void hhPlayer::Present() { + idPlayer::Present(); + + if ( lighterHandle != -1 ) { + // Update oscillation position + idVec3 oscillation; + oscillation.x = 0.0f; + oscillation.y = idMath::Cos( MS2SEC(gameLocal.time) * spawnArgs.GetFloat("lighter_yspeed") ) * spawnArgs.GetFloat("lighter_ysize"); + oscillation.z = idMath::Sin( MS2SEC(gameLocal.time) * spawnArgs.GetFloat("lighter_zspeed") ) * spawnArgs.GetFloat("lighter_zsize"); + + idVec3 offset; + offset = spawnArgs.GetVector("offset_lighter"); + offset.z = EyeHeight(); + lighter.origin = GetOrigin() + GetAxis() * (offset + oscillation); + lighter.axis = mat3_identity; + + lighter.shaderParms[ SHADERPARM_DIVERSITY ] = renderEntity.shaderParms[ SHADERPARM_DIVERSITY ]; + + gameRenderWorld->UpdateLightDef( lighterHandle, &lighter ); + } +} + +void hhPlayer::LighterOn() { + if (IsSpiritOrDeathwalking() || InVehicle() || spectating || bReallyDead) { // No lighter in spirit mode, deathwalk, or when in vehicles (or when spectating or really dead -rww) + return; + } + + if (lighterHandle == -1) { + // Add the dynamic light + memset( &lighter, 0, sizeof( lighter ) ); + lighter.lightId = LIGHTID_VIEW_MUZZLE_FLASH + entityNumber; + lighter.pointLight = true; // false; ? + lighter.shader = declManager->FindMaterial( spawnArgs.GetString( "mtr_lighter" ) ); + lighter.shaderParms[ SHADERPARM_RED ] = spawnArgs.GetFloat( "lighterColorR" ); + lighter.shaderParms[ SHADERPARM_GREEN ] = spawnArgs.GetFloat( "lighterColorG" ); + lighter.shaderParms[ SHADERPARM_BLUE ] = spawnArgs.GetFloat( "lighterColorB" ); + lighter.shaderParms[ SHADERPARM_ALPHA ] = 1.0f; + lighter.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.time ); + lighter.lightRadius = spawnArgs.GetVector( "lighter_radius" ); + + lighter.origin = GetOrigin() + GetAxis() * spawnArgs.GetVector("offset_lighter"); + lighter.axis = mat3_identity; + + lighterHandle = gameRenderWorld->AddLightDef( &lighter ); + + StartSound("snd_lighter_on", SND_CHANNEL_ITEM); + } +} + +void hhPlayer::LighterOff() { + if (lighterHandle != -1) { + gameRenderWorld->FreeLightDef( lighterHandle ); + lighterHandle = -1; + StartSound("snd_lighter_off", SND_CHANNEL_ITEM); + } +} + +bool hhPlayer::IsLighterOn() const { //HUMANHEAD PCF mdl 05/04/06 - Made const + return (lighterHandle != -1); +} + +void hhPlayer::ToggleLighter() { + if (IsLighterOn()) { + LighterOff(); + } + else { + LighterOn(); + } +} + +void hhPlayer::UpdateLighter() { + if (IsLighterOn()) { + // Increase the lighter's temperature until it overheats + if (!gameLocal.isMultiplayer) { //rww - don't bother overheating the lighter in mp + lighterTemperature += spawnArgs.GetFloat( "lighterHeatRate", "0.025" ) * MS2SEC( USERCMD_MSEC ); + if (lighterTemperature >= 1.0f) { + // Too hot, turn the lighter off + StartSound("snd_lighter_toohot", SND_CHANNEL_ANY); + lighterTemperature = 1.0f; + + if (!(godmode || noclip)) { + LighterOff(); + } + } + } + } + else { + // Lighter is off, so decrease the lighter's temperature + if (lighterTemperature > 0) { + lighterTemperature -= spawnArgs.GetFloat( "lighterCoolRate", "0.05") * MS2SEC( USERCMD_MSEC ); + } + } +} + +/* +================== +hhPlayer::PlayFootstepSound +================== +*/ +void hhPlayer::PlayFootstepSound() { + if ( IsSpiritOrDeathwalking() ) { + return; // No footstep sounds in spiritwalk mode + } + + if ( IsWallWalking() ) { + // Special case wallwalk since contacts includes some non-wallwalk types + const char* soundKey = gameLocal.MatterTypeToMatterKey( "snd_footstep", SURFTYPE_WALLWALK ); + StartSound( soundKey, SND_CHANNEL_BODY3, 0, false, NULL ); + return; + } + + idPlayer::PlayFootstepSound(); +} + +/* +===================== +hhPlayer::PlayPainSound + +HUMANHEAD: aob +===================== +*/ +void hhPlayer::PlayPainSound() { + if( IsSpiritOrDeathwalking() ) { + return; + } + + //HUMANHEAD PCF rww 09/15/06 - female mp sounds + if (IsFemale()) { + StartSound( "snd_pain_small_female", SND_CHANNEL_VOICE ); + return; + } + //HUMANHEAD END + idPlayer::PlayPainSound(); +} + +/* +=============== +hhPlayer::Give +=============== +*/ +bool hhPlayer::Give( const char *statname, const char *value ) { + if( IsDead() ) { + return false; + } + + return inventory.Give( this, spawnArgs, statname, value, &idealWeapon, true ); +} + +/* +=============== +hhPlayer::ReportAttack +=============== +*/ +void hhPlayer::ReportAttack(idEntity *attacker) { + if (gameLocal.isServer && attacker) { //rww - broadcast event for this on the server + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + msg.Init(msgBuf, sizeof(msgBuf)); + msg.WriteBits(gameLocal.GetSpawnId(attacker), 32); + //unreliable since it's not super-important, and only send to this player. + ServerSendEvent(EVENT_REPORTATTACK, &msg, false, -1, entityNumber, true); + } + + int ix; + + for (ix=0; ix lastAttackers[ix].time + DAMAGE_INDICATOR_TIME) { + lastAttackers[ix].attacker = NULL; + } + } + + // Add this attacker to first free slot, if any + for (ix=0; ix 0) { + lastAttackers[oldSlot].attacker = attacker; + lastAttackers[oldSlot].time = gameLocal.time; + lastAttackers[oldSlot].displayed = false; + } +} + +/* +================== +hhPlayer::Damage +================== +*/ +void hhPlayer::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, + const char *damageDefName, const float damageScale, const int location ) { + + if( IsSpiritOrDeathwalking() ) { //Player is spirit-walking, so check for special immunities + const idKeyValue *kv = spawnArgs.MatchPrefix("immunityspirit"); + while( kv && kv->GetValue().Length() ) { + if ( !kv->GetValue().Icmp(damageDefName) ) { + return; + } + kv = spawnArgs.MatchPrefix("immunityspirit", kv); + } + + // If the player is spiritwalking, then any damage takes away spirit power instead of health + ammo_t ammo_spiritpower = idWeapon::GetAmmoNumForName( "ammo_spiritpower" ); + if ( IsSpiritWalking() ) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef ) { + idPlayer *player = (attacker && attacker->IsType( idPlayer::Type )) ? static_cast(attacker) : NULL; + if ( !(gameLocal.gameType == GAME_TDM + && !gameLocal.serverInfo.GetBool( "si_teamDamage" ) + && !damageDef->GetBool( "noTeam" ) + && player + && player != this // you get self damage no matter what + && player->team == team) ) { + //rww - don't damage teammates' spirit when ff off + int damageWhenSpirit = damageDef->GetInt("damageWhenSpirit", "0"); //rww - special damage for knocking players back to body in mp when shot by other things (namely spirit arrows) + int oldSpirit = inventory.ammo[ ammo_spiritpower ]; + + int spiritDamage = (damageDef->GetInt( "damage", "1" ) * spawnArgs.GetFloat( "damageScaleInSpirit" ) * damageScale )+damageWhenSpirit; + if ( spiritDamage <= 0 && damageScale > 0 ) { + spiritDamage = 1; + } + + if ( !UseAmmo( ammo_spiritpower, spiritDamage ) ) { + inventory.ammo[ ammo_spiritpower ] = 0; // Clear spiritpower amount when returning from excessive damage + } + } + } + + lastDamagedTime = gameLocal.time; // Save the damage time for the health recharge code + + // Track last attacker for use in displaying HUD hit indicator + if (!gameLocal.isClient) { + ReportAttack(attacker); + } + + //HUMANHEAD rww - damage feedback for hitting spirit players in mp + if (gameLocal.isMultiplayer && attacker && !gameLocal.isClient) { + hhPlayer *killer = NULL; + if (attacker->IsType(idPlayer::Type)) { + killer = static_cast(attacker); + } + else if (attacker->IsType(hhVehicle::Type)) { + hhVehicle *veh = static_cast(attacker); + if (veh->GetPilot() && veh->GetPilot()->IsType(idPlayer::Type)) { + killer = static_cast(veh->GetPilot()); + } + } + + if (killer && killer->entityNumber != entityNumber && killer->mpHitFeedbackTime <= gameLocal.time) { + if (killer == gameLocal.GetLocalPlayer()) { + assert(IsSpiritOrDeathwalking()); + if (gameLocal.gameType == GAME_TDM && team == killer->team) { + killer->StartSound( "snd_hitTeamFeedback", SND_CHANNEL_ITEM, 0, false, NULL ); + } + else { + killer->StartSound( "snd_hitSpiritFeedback", SND_CHANNEL_ITEM, 0, false, NULL ); + + //hardcoded health ranges for the various flash colors + float h; + if (health > 100) { + h = 0.0f; + } + else if (health > 75) { + h = 0.25f; + } + else if (health > 25) { + h = 0.50f; + } + else { + h = 0.75f; + } + SetShaderParm(3, h); + SetShaderParm(5, -MS2SEC(gameLocal.time)*2); + } + } + else { + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + msg.Init(msgBuf, sizeof(msgBuf)); + msg.WriteBits(gameLocal.GetSpawnId(this), 32); + killer->ServerSendEvent(EVENT_HITNOTIFICATION, &msg, false, -1, killer->entityNumber); + } + } + } + //HUMANHEAD END + + return; + } + } + + lastDamagedTime = gameLocal.time; // Save the damage time for the health recharge code + + // Track last attacker for use in displaying HUD hit indicator + if (!gameLocal.isClient) { + ReportAttack(attacker); + } + + // Check if the damage should "really kill" the player (when in deathwalk mode) + if ( IsDeathWalking() ) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef ) { + if ( damageDef->GetBool( "reallyKill", "0" ) ) { // Truly kill the player + ReallyKilled( inflictor, attacker, damageDef->GetInt( "damage", "9999" ), dir, location ); + return; + } else if ( damageDef->GetBool( "spiritDamage", "0" ) ) { // Drain spirit power + ammo_t ammo_spiritpower = idWeapon::GetAmmoNumForName( "ammo_spiritpower" ); + UseAmmo( ammo_spiritpower, damageDef->GetInt( "damage", "1" ) * damageScale ); + return; + } + } + } + + //HUMANHEAD rww - keep track of last person to attack me, debounce time will vary based on ground contact + if (attacker && attacker->IsType(idPlayer::Type)) { + airAttacker = attacker; + if (!AI_ONGROUND) { + airAttackerTime = gameLocal.time + 300; + } + else { //if they are on the ground do a 100ms debounce in case they fall off a ledge or something as a result of leftover attack velocity + airAttackerTime = gameLocal.time + 100; + } + } + //HUMANHEAD END + + // CJR: DDA: Only in single player + if ( !gameLocal.isMultiplayer ) { + float newDamageScale = gameLocal.GetDDAValue() * 2.0f; // Scale damage from easy to hard + newDamageScale *= damageScale; + + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef ) { + bool noGod = damageDef->GetBool("noGod", "0"); + if ( noGod ) { + // Make sure fatal damage bypasses post-deathwalk invulnerability + fl.takedamage = true; + } + } + + int oldHealth = health; + idPlayer::Damage( inflictor, attacker, dir, damageDefName, (const float)newDamageScale, location ); // CJR DDA TEST + + if ( attacker && attacker->IsType( hhMonsterAI::Type ) ) { // CJR DDA: Damaged by a monster, add the damage to the monster count + float delta = oldHealth - health; + + hhMonsterAI *monster = static_cast( attacker ); + + if ( monster ) { + monster->DamagedPlayer( delta ); + } + } + } else { + idPlayer::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + } + + //HUMANHEAD rww - hit feedback + if (gameLocal.isMultiplayer && attacker && !gameLocal.isClient) { //let's broadcast from the server only, so hit feedback is always reliable + hhPlayer *killer = NULL; + if (attacker->IsType(idPlayer::Type)) { + killer = static_cast(attacker); + } + else if (attacker->IsType(hhVehicle::Type)) { + hhVehicle *veh = static_cast(attacker); + if (veh->GetPilot() && veh->GetPilot()->IsType(idPlayer::Type)) { + killer = static_cast(veh->GetPilot()); + } + } + + if (killer && killer->entityNumber != entityNumber && killer->mpHitFeedbackTime <= gameLocal.time) { + if (killer == gameLocal.GetLocalPlayer()) { + //don't provide visual indicator when shooting a teammate + if (gameLocal.gameType == GAME_TDM && team == killer->team) { + killer->StartSound( "snd_hitTeamFeedback", SND_CHANNEL_ITEM, 0, false, NULL ); + } + else { + if (IsSpiritOrDeathwalking()) { + killer->StartSound( "snd_hitSpiritFeedback", SND_CHANNEL_ITEM, 0, false, NULL ); + } + else { + killer->StartSound( "snd_hitFeedback", SND_CHANNEL_ITEM, 0, false, NULL ); + } + + //hardcoded health ranges for the various flash colors + float h; + if (health > 100) { + h = 0.0f; + } + else if (health > 75) { + h = 0.25f; + } + else if (health > 25) { + h = 0.50f; + } + else { + h = 0.75f; + } + SetShaderParm(3, h); + SetShaderParm(5, -MS2SEC(gameLocal.time)*2); + } + } + else { + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + msg.Init(msgBuf, sizeof(msgBuf)); + msg.WriteBits(gameLocal.GetSpawnId(this), 32); + killer->ServerSendEvent(EVENT_HITNOTIFICATION, &msg, false, -1, killer->entityNumber); + } + + //see how this feels (and more importantly how destructive it is toward bandwidth) + //killer->mpHitFeedbackTime = gameLocal.time + USERCMD_MSEC; + } + } + //HUMANHEAD END + + if ( bFrozen ) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( damageDef && damageDef->GetInt( "free_cocoon", "0" ) ) { + Event_Unfreeze(); + } + } +} + +void hhPlayer::DoDeathDrop() { + // General dropping (for monsters, souls, etc.) + const idKeyValue *kv = NULL; + kv = spawnArgs.MatchPrefix( "def_drops", NULL ); + while ( kv ) { + + idStr drops = kv->GetValue(); + idDict args; + + idStr last5 = kv->GetKey().Right(5); + if ( drops.Length() && idStr::Icmp( last5, "Joint" ) != 0) { + + args.Set( "classname", drops ); + + // HUMANHEAD pdm: specify monster so souls can call back to remove body when picked up + args.Set("monsterSpawnedBy", name.c_str()); + + idVec3 origin; + idMat3 axis; + idStr jointKey = kv->GetKey() + idStr("Joint"); + idStr jointName = spawnArgs.GetString( jointKey ); + idStr joint2JointKey = kv->GetKey() + idStr("Joint2Joint"); + idStr j2jName = spawnArgs.GetString( joint2JointKey ); + + idEntity *newEnt = NULL; + gameLocal.SpawnEntityDef( args, &newEnt ); + HH_ASSERT(newEnt != NULL); + + if(jointName.Length()) { + jointHandle_t joint = GetAnimator()->GetJointHandle( jointName ); + if (!GetAnimator()->GetJointTransform( joint, gameLocal.time, origin, axis ) ) { + gameLocal.Printf( "%s refers to invalid joint '%s' on entity '%s'\n", (const char*)jointKey.c_str(), (const char*)jointName, (const char*)name ); + origin = renderEntity.origin; + axis = renderEntity.axis; + } + axis *= renderEntity.axis; + origin = renderEntity.origin + origin * renderEntity.axis; + newEnt->SetAxis(axis); + newEnt->SetOrigin(origin); + } + else { + newEnt->SetAxis(viewAxis); + newEnt->SetOrigin(GetOrigin()); + } + } + + kv = spawnArgs.MatchPrefix( "def_drops", kv ); + } +} + +//HUMANHEAD rww +/* +================== +hhPlayer::Event_RespawnCleanup +performs any necessary operations on death to prepare for a clean spawn. +only cosmetic things should be placed here, this function will not be called +for sure on death, at least for the client. +================== +*/ +void hhPlayer::Event_RespawnCleanup(void) { + //remove any fx entities which are bound to the player + idEntity *ent; + idEntity *next; + + for( ent = teamChain; ent != NULL; ent = next ) { + next = ent->GetTeamChain(); + if ( ent->GetBindMaster() == this && ent->IsType(hhEntityFx::Type) ) { + ent->Unbind(); + if( !ent->fl.noRemoveWhenUnbound ) { + ent->PostEventMS( &EV_Remove, 0 ); + if (gameLocal.isClient) { + ent->Hide(); + } + } + next = teamChain; + } + } +} +//HUMANHEAD END + +/* +================== +hhPlayer::Killed +================== +*/ +void hhPlayer::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + //you only die once + if( AI_DEAD ) { + return; + } + + CancelEvents(&EV_DamagePlayer); //rww - don't do any more posted damage events once dead + + if (gameLocal.isMultiplayer) { //rww - this was not handled at all, i guess because there are different circumstances for "death" in sp + wallwalkSoundController.StopSound( SND_CHANNEL_WALLWALK, SND_CHANNEL_WALLWALK2, false ); + spiritwalkSoundController.StopSound( SND_CHANNEL_BODY, SND_CHANNEL_BODY2 ); + + LighterOff(); + + if (!gameLocal.isClient && weapon.IsValid() && weapon->IsType(hhWeapon::Type) && weapon->CanDrop()) { //when dying in mp, toss my weapon out. + idVec3 forward, up; + + viewAngles.ToVectors( &forward, NULL, &up ); + //rww - hackishness to keep the type of ammo in the leechgun that it was dropped with + idEntity *dropped = weapon->DropItem( 50.0f * forward + 50.0f * up, 0, 60000, true ); + if (dropped && weapon->IsType(hhWeaponSoulStripper::Type)) { + dropped->spawnArgs.Set("def_droppedEnergyType", inventory.energyType); + } + } + } + + if (!gameLocal.isClient) { + DoDeathDrop(); + } + + if ( gameLocal.isMultiplayer && !gameLocal.IsCooperative() ) { + bDeathWalk = false; + bSpiritWalk = false; + bReallyDead = true; + } + else if ( !inventory.requirements.bCanDeathWalk || !gameLocal.DeathwalkMapLoaded() ) { + bDeathWalk = false; + bSpiritWalk = false; + bReallyDead = true; + } + + //First thing we do is get out of vehicle + if (InVehicle()) { + GetVehicleInterface()->GetVehicle()->EjectPilot(); + } + + // HUMANHEAD cjr: Update DDA + if ( !AI_DEAD && !gameLocal.isMultiplayer ) { + gameLocal.GetDDA()->DDA_AddDeath( this, attacker ); + } + // HUMANHEAD END + + if ( attacker && attacker->spawnArgs.GetBool("death_look_at", "0")) { + attacker->spawnArgs.GetString("death_look_at_bone", "", deathLookAtBone); + attacker->spawnArgs.GetString("death_camera_bone", "", deathCameraBone ); + deathLookAtEntity = attacker; + } + + if ( weapon.IsValid() ) { + //HUMANHEAD bjk 04/26/06 - no sniper scope when dead + if ( weapon->IsType( hhWeaponRifle::Type ) ) { + static_cast( weapon.GetEntity() )->ZoomOut(); + } + weapon->PutAway(); + } + + // HUMANHEAD nla + if ( hand.IsValid() ) { + hand->PutAway(); + } + // HUMANHEAD END + + if ( !bReallyDead ) { // Only go into deathwalk mode if the player is in the transitional death state + idVec3 origin; + idMat3 axis; + idVec3 viewDir; + idAngles angles; + idMat3 eyeAxis; + GetResurrectionPoint( origin, axis, viewDir, angles, eyeAxis, GetPhysics()->GetAbsBounds(), GetOrigin(), GetPhysics()->GetAxis(), GetAxis()[0], viewAngles ); + DeathWalk( origin, axis, viewDir.ToMat3(), angles, eyeAxis ); + } else { + if (gameLocal.isMultiplayer) { //HUMANHEAD rww + PostEventMS(&EV_RespawnCleanup, 32); + if (!gameLocal.isClient) { + StartRagdoll(); //start to rag on the player so that the proxy copies off proper af status + + GetPhysics()->SetContents( 0 ); //make non-solid + + hhMPDeathProxy *prox = (hhMPDeathProxy *)hhSpiritProxy::CreateProxy( spawnArgs.GetString("def_deathProxy_mp"), this, GetOrigin(), GetPhysics()->GetAxis(), viewAxis, viewAngles, GetEyeAxis() ); + assert(((hhSpiritProxy *)prox)->IsType(hhMPDeathProxy::Type)); + if (prox) { + idVec3 flingForce; + float capDmg = (float)damage; + if (capDmg > 200.0f) { + capDmg = 200.0f; + } + + flingForce = dir; + flingForce.Normalize(); + flingForce *= capDmg; + + prox->GetPhysics()->AddForce(0, prox->GetPhysics()->GetOrigin(0), flingForce*256.0f*256.0f); + prox->SetFling(prox->GetPhysics()->GetOrigin(0), flingForce); + } + + StopRagdoll(); //stop again, as we don't need to be ragging on the actual player + } + + Hide(); //hide player + + minRespawnTime = gameLocal.time + RAGDOLL_DEATH_TIME; + maxRespawnTime = minRespawnTime + 10000; + } else { + GetPhysics()->SetContents(0); + Hide(); + + minRespawnTime = gameLocal.time + 2000; + maxRespawnTime = minRespawnTime + 5000; + } //HUMANHEAD END + + physicsObj.SetMovementType( PM_DEAD ); + SAFE_REMOVE( weapon ); + } + + AI_DEAD = true; + + //HUMANHEAD rww + if (gameLocal.isMultiplayer) { + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 4 ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 4 ); + SetWaitState( "" ); + } + //HUMANHEAD END + + //HUMANHEAD PCF rww 09/15/06 - female mp sounds + if (IsFemale()) { + StartSound( "snd_death_female", SND_CHANNEL_VOICE ); + } + else { + //HUMANHEAD END + StartSound( "snd_death", SND_CHANNEL_VOICE ); + } + + if ( gameLocal.isMultiplayer && !gameLocal.isCoop ) { + idPlayer *killer = NULL; + + if ( attacker->IsType( idPlayer::Type ) ) { + killer = static_cast(attacker); + } + else { //rww - otherwise try to credit airAttacker + if (airAttacker.IsValid() && + airAttacker.GetEntity() && + airAttackerTime > gameLocal.time) { + + if (airAttacker->IsType(idPlayer::Type)) { + killer = static_cast(airAttacker.GetEntity()); + } + else if (airAttacker->IsType(hhVehicle::Type)) { + hhVehicle *veh = static_cast(airAttacker.GetEntity()); + if (veh->GetPilot() && veh->GetPilot()->IsType(idPlayer::Type)) { + killer = static_cast(veh->GetPilot()); + } + } + } + } + gameLocal.mpGame.PlayerDeath( this, killer, inflictor, false ); //HUMANHEAD rww - pass inflictor + } + + UpdateVisuals(); + + airAttackerTime = 0; //HUMANHEAD rww - reset air attacker time once dead +} + +/* +============== +hhPlayer::Kill +============== +*/ +void hhPlayer::Kill( bool delayRespawn, bool nodamage ) { + if (noclip) { // HUMANHEAD pdm: Because of deathwalk, this shouldn't be allowed + return; + } + + if ( health > 0 ) { + //HUMANHEAD rww + if (IsSpiritOrDeathwalking()) { + if (!IsSpiritWalking()) { //don't "kill" when dead. + return; + } + StopSpiritWalk(true); + } + //HUMANHEAD END + + godmode = false; + health = 0; + //HUMANHEAD rww - if in a vehicle, eject now + if (InVehicle()) { + if (vehicleInterfaceLocal.GetVehicle()) { + vehicleInterfaceLocal.GetVehicle()->EjectPilot(); + } + } + //HUMANHEAD END + Damage( NULL, NULL, vec3_origin, "damage_suicide", 1.0f, INVALID_JOINT ); + } +} + +/* +=============== +hhPlayer::DetermineOwnerPosition + +HUMANHEAD: aob +=============== +*/ +void hhPlayer::DetermineOwnerPosition( idVec3 &ownerOrigin, idMat3 &ownerAxis ) { + ownerAxis = TransformToPlayerSpace( GetUntransformedViewAxis() ); + ownerOrigin = cameraInterpolator.GetCurrentPosition() + idVec3(g_gun_x.GetFloat(), g_gun_y.GetFloat(), g_gun_z.GetFloat()) * GetAxis(); +} + +/* +=============== +hhPlayer::GetViewPos +=============== +*/ +//HUMANHEAD bjk +void hhPlayer::GetViewPos( idVec3 &origin, idMat3 &axis ) { + idAngles angles; + + // if dead, fix the angle and don't add any kick + // HUMANHEAD cjr: Replaced health <= 0 with IsDead() call for deathwalk override + if ( IsDead() && !gameLocal.isMultiplayer ) { //rww - don't want this in mp. + // HUMANHEAD END + angles.yaw = viewAngles.yaw; + angles.roll = 40; + angles.pitch = -15; + axis = angles.ToMat3(); + origin = GetEyePosition(); + } else { + assert(kickSpring < 500.0f && kickSpring > 0.0f); + assert(kickDamping < 500.0f && kickDamping > 0.0f); + origin = viewBob + TransformToPlayerSpace( idVec3(g_viewNodalX.GetFloat(), g_viewNodalZ.GetFloat(), g_viewNodalZ.GetFloat() + EyeHeight()) ); + axis = TransformToPlayerSpace( (GetUntransformedViewAngles() + viewBobAngles + playerView.AngleOffset(kickSpring, kickDamping)).ToMat3() ); + } +} + +/* +=============== +hhPlayer::CalculateRenderView +=============== +*/ +void hhPlayer::CalculateRenderView( void ) { + idPlayer::CalculateRenderView(); + + // HUMANHEAD cjr + if ( IsSpiritOrDeathwalking() ) { // If spiritwalking, then allow the player to see special objects + renderView->viewSpiritEntities = true; + } +} + + +/* +=============== +hhPlayer::OffsetThirdPersonView + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +=============== +*/ +void hhPlayer::OffsetThirdPersonView( float angle, float range, float height, bool clip ) { + idVec3 view; + trace_t trace; + idMat3 lookAxis; + idVec3 origin; + idAngles angles( GetUntransformedViewAngles() ); + + if ( angle ) { + angles.pitch = 0.0f; + } + + if ( angles.pitch > 45.0f && !InVehicle() ) { + angles.pitch = 45.0f; // don't go too far overhead + } + + //HUMANHEAD: aob + lookAxis = TransformToPlayerSpace( angles.ToMat3() * idAngles(0.0f, angle, 0.0f).ToMat3() ); + if( InVehicle() ) { + origin = GetOrigin() + lookAxis[2] * (height + EyeHeight()); + } else { + origin = GetEyePosition() + GetEyeAxis()[2] * height; + } + view = origin + lookAxis[0] * -range; + // trace a ray from the origin to the viewpoint to make sure the view isn't + // in a solid block. Use an 16 by 16 block to prevent the view from near clipping anything + if( !noclip ) { + int mask = MASK_SHOT_BOUNDINGBOX; + + if (gameLocal.isMultiplayer) { //rww - don't want this hitting corpses. also, shouldn't we pay attention to "clip"? + mask = MASK_PLAYERSOLID; + } + + thirdPersonCameraClipBounds.SetOwner( this ); + idEntity* ignore = (InVehicle()) ? GetVehicleInterface()->GetVehicle() : (idEntity*)this; + gameLocal.clip.Translation( trace, origin, view, &thirdPersonCameraClipBounds, lookAxis, mask, ignore ); + range *= trace.fraction; + } + + renderView->vieworg = origin + lookAxis[0] * -range; + renderView->viewaxis = lookAxis; + //HUMANHEAD END + renderView->viewID = 0; +} + + +/* +============================== +hhPlayer::GetGuiHandInfo + Retrns the proper gui hand info for a given gui +============================== +*/ +const char *hhPlayer::GetGuiHandInfo() { + const char *attrib; + idDict *item = NULL; + const char *handString; + + // Set the gui to: + // "required_attribute" "Hunter Hand" + // If item exists in player inventory, player uses guihand specified by item + if (!IsSpiritWalking() && focusGUIent && focusGUIent->spawnArgs.GetString("required_attribute", NULL, &attrib)) { + item = FindInventoryItem( attrib ); + if ( item ) { + if (item->GetString("def_guihand", NULL, &handString)) { + return handString; + } + } + } + + // If no required attribute/player doesn't have the item, check for a def_guihand string + if ( focusGUIent && focusGUIent->spawnArgs.GetString( "def_guihand", NULL, &handString ) ) { + return( handString ); + } + + // Otherwise, use the default player hand + return spawnArgs.GetString("def_guihand"); + +} + + +//============================================================================= +// +// Spirit walk functions +// +//============================================================================= + +void hhPlayer::StartSpiritWalk( const bool bThrust, bool force ) { + hhFxInfo fxInfo; + idVec3 origin; + idMat3 axis; + + //rww - don't do anything on the client + if (gameLocal.isClient) { + return; + } + + if ( bReallyDead ) { // Don't allow spiritwalking if truly dead + if (force) { + gameLocal.Error("Attempted to force spirit walk when dead.\n"); + } + return; + } + + // Make sure spirit walk isn't disabled + if ( !bAllowSpirit ) { + StartSound("snd_spiritWalkDenied", SND_CHANNEL_ANY); + return; + } + + spiritWalkToggleTime = gameLocal.time; + + if ( gameLocal.isMultiplayer ) { // CJR: Don't allow spiritwalking in MP unless the player has spirit power + if ( GetSpiritPower() <= 0 ) { + StartSound("snd_spiritWalkDenied", SND_CHANNEL_ANY, 0, true); + return; + } + } + + if ( !force && nextSpiritTime > gameLocal.time ) { // mdl: Make sure they didn't just get knocked back into their body by a wraith + StartSound("snd_spiritWalkDenied", SND_CHANNEL_ANY); + return; + } + + if (!IsSpiritOrDeathwalking()) { + if ( !gameLocal.IsLOTA() ) { // In LOTA, spirit power never drains + PostEventMS( &EV_DrainSpiritPower, spiritDrainHeartbeatMS ); + } + + if ( !bThrust ) { // Normal spiritwalking + EnableEthereal( spawnArgs.GetString( "def_spiritProxy" ), GetOrigin(), GetPhysics()->GetAxis(), viewAxis, viewAngles, GetEyeAxis() ); + } else { // Knocked out by a wraith + EnableEthereal( spawnArgs.GetString( "def_possessedProxy" ), GetOrigin(), GetPhysics()->GetAxis(), viewAxis, viewAngles, GetEyeAxis() ); + } + + SelectEtherealWeapon(); + + spiritwalkSoundController.StartSound( SND_CHANNEL_BODY, SND_CHANNEL_BODY2 ); + + // Update HUD + if (hud) { + hud->HandleNamedEvent("SwitchToEthereal"); + } + + // Set the player's skin to a glowy effect + SetSkinByName( spawnArgs.GetString("skin_Spiritwalk") ); + SetShaderParm( SHADERPARM_TIMEOFFSET, 1.0f ); // TEMP: cjr - Required by the forcefield material. Can remove when a proper spiritwalk texture is made + + // Spawn in a flash + GetViewPos( origin, axis ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( true ); + fxInfo.SetBindBone( "origin" ); + BroadcastFxInfoPrefixed( "fx_spiritWalkFlash", origin, axis, &fxInfo ); + + // Thrust the player backwards out of the body + if ( bThrust ) { + idVec3 vec = GetPhysics()->GetAxis()[0] * -200.0f + GetPhysics()->GetAxis()[2] * 50.0f; + + physicsObj.SetLinearVelocity( physicsObj.GetLinearVelocity() + vec ); + // set the timer so that the player can't cancel out the movement immediately + physicsObj.SetKnockBack( 100 ); + } + + // bg - trigger map entity, provides a simple hook for map/scripts + idEntity *swTrig = gameLocal.FindEntity( "sw_spiritWalkEntered" ); + if( swTrig ) { + swTrig->PostEventMS( &EV_Activate, 0, this ); + } + } +} + +void hhPlayer::StopSpiritWalk(bool forceAllowance) { + hhFxInfo fxInfo; + idVec3 origin; + idMat3 axis; + + //rww - don't do anything on the client + if (gameLocal.isClient) { + return; + } + + if ( IsPossessed() ) { // If possessed and the player stops spiritwalking, they die + PossessKilled(); + return; + } + + spiritWalkToggleTime = gameLocal.time; + + if ( bReallyDead ) { // Don't allow spiritwalking if truly dead + return; + } + + CancelEvents(&EV_SetOverlayMaterial); //HUMANHEAD rww - if being forced back out of spirit mode quickly enough, don't set overlay afterward + + if (IsSpiritOrDeathwalking()) { + CancelEvents(&EV_DrainSpiritPower); + if( weapon.IsValid() && weapon->IsType(hhWeaponSpiritBow::Type) ) { + hhWeaponSpiritBow *bow = static_cast( weapon.GetEntity() ); + if( bow->BowVisionIsEnabled() ) { + bow->StopBowVision(); + } + } + DisableEthereal(); + spiritwalkSoundController.StopSound( SND_CHANNEL_BODY, SND_CHANNEL_BODY2 ); + SetSkin( NULL ); + + // bg - trigger map entity, provides a simple hook for map/scripts + idEntity *swTrig = gameLocal.FindEntity( "sw_spiritWalkExited" ); + if( swTrig ) { + swTrig->PostEventMS( &EV_Activate, 0, this ); + } + + // Update HUD + if (hud) { + hud->HandleNamedEvent("SwitchFromEthereal"); + } + buttonMask |= BUTTON_ATTACK; //HUMANHEAD bjk + + // Spawn in a flash + GetViewPos( origin, axis ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( true ); + fxInfo.SetBindBone( "origin" ); + BroadcastFxInfoPrefixed( "fx_spiritWalkFlash", origin, axis, &fxInfo ); + } +} + +void hhPlayer::ToggleSpiritWalk( void ) { + if (spectating) { //rww - do not allow spectators to spirit walk. + return; + } + + if ( bPossessed ) { // Cannot toggle away from spiritwalk when possessed + StartSound("snd_spiritWalkDenied", SND_CHANNEL_ANY); + return; + } + + // spiritwalk time check -- force a delay between spiritwalking and not + if (gameLocal.time < spiritWalkToggleTime + 250) { + return; + } + + if( IsSpiritOrDeathwalking() ) { + StopSpiritWalk(); + } else { + StartSpiritWalk( false ); + } +} + +//HUMANHEAD rww +void hhPlayer::RestorePlayerLocationFromDeathwalk( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& angles ) { + idVec3 newOrigin; + idMat3 newAxis; + idVec3 newViewDir; + idAngles newAngles; + + if( deathwalkLastCrouching ) { + ForceCrouching(); + } + + SetEyeAxis(deathwalkLastEyeAxis); + GetResurrectionPoint( newOrigin, newAxis, newViewDir, newAngles, deathwalkLastEyeAxis, GetPhysics()->GetAbsBounds(), deathwalkLastOrigin, deathwalkLastBBoxAxis, viewDir, angles ); + Teleport( newOrigin, newAxis, newViewDir, (newAngles.ToMat3() * deathwalkLastEyeAxis.Transpose()).ToAngles(), NULL ); +} +//HUMANHEAD END + +//============================================================================= +// +// hhPlayer::EnableEthereal +// +// Sets the player into an ethereal state (used for both spiritwalk and deathwalk) +// This function does the following: +// - Fade volume down on all class 0 sounds +// - Sets bSpiritWalk (since this is true for both spirit and death walks) +// - Spawns a proxy +// - Sets the new collision +// - Disables the weapons (the bow is enabled seperately for spiritwalk) +// - Disables the lighter +// - Unpossesses the player (if possessed) +// - Updates bindings +//============================================================================= + +void hhPlayer::EnableEthereal( const char *proxyName, const idVec3& origin, const idMat3& bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ) { + //rww - don't do anything on the client - DO NOT PUT ANYTHING ABOVE HERE + if (gameLocal.isClient) { + return; + } + + // Turn on low-pass effect + if (gameLocal.localClientNum == entityNumber) { //rww - don't do this for anyone but the local client on listen servers + gameLocal.SpiritWalkSoundMode( true ); + } + + // Spawn proxy Tommy actor + spiritProxy = hhSpiritProxy::CreateProxy( proxyName, this, origin, bboxAxis, newViewAxis, newViewAngles, newEyeAxis ); + + // Change player's collision type (to allow walking through forcefields, etc) + GetPhysics()->SetClipMask( MASK_SPIRITPLAYER ); + fl.acceptsWounds = false; + spawnArgs.Set("produces_splats", "0"); + + // HUMANHEAD mdl: Let our enemies know about the spirit proxy + for ( int i = 0; i < hhMonsterAI::allSimpleMonsters.Num(); i++ ) { + if ( hhMonsterAI::allSimpleMonsters[i]->GetEnemy() == this ) { + hhMonsterAI::allSimpleMonsters[i]->ProcessEvent( &MA_EnemyIsSpirit, this, spiritProxy.GetEntity() ); + } + } + + // If bound to another object (like on a rail ride), swap bindings with the proxy + if ( GetBindMaster() != NULL ) { + if (GetBindMaster()->IsType(hhBindController::Type)) { + hhBindController *binder = static_cast(GetBindMaster()); + bool loose = binder->IsLoose(); + binder->Detach(); + binder->Attach( spiritProxy.GetEntity(), loose ); + } else { + gameLocal.Warning("Handle this: spiritwalking while bound to non-rail"); + SwapBindInfo( spiritProxy.GetEntity() ); // untested + } + } + + SnapDownCurrentWeapon(); + + LighterOff(); + + // Set the player into spiritwalk mode + bSpiritWalk = true; +} + +//============================================================================= +// +// hhPlayer::DisableEthereal +// +// Returns the player from an ethereal state (used for both spiritwalk and deathwalk) +// This function does the following: +// - Clears bSpiritWalk (since this is true for both spirit and death walks) +// - Removes the proxy +// - Sets the new collision +// - Re-enables the weapons +// - Updates bindings +// - Fade volume back up on all class 0 sounds +//============================================================================= + +void hhPlayer::DisableEthereal( void ) { + hhFxInfo fxInfo; + idVec3 boneOffset; + idMat3 boneAxis; + + //rww - don't do anything on the client - DO NOT PUT ANYTHING ABOVE HERE + if (gameLocal.isClient) { + return; + } + + bSpiritWalk = false; + + // Spawn in an effect when the spirit is snapped back + GetJointWorldTransform( "waist", boneOffset, boneAxis ); + fxInfo.RemoveWhenDone( true ); + fxInfo.SetNormal( boneAxis[1] ); + BroadcastFxInfo( spawnArgs.GetString( "fx_spiritReturn" ), boneOffset, GetAxis(), &fxInfo ); + + if( spiritProxy.IsValid() ) { + // Handle case of returning to a body on a rail ride + idEntity *spiritMaster = spiritProxy->GetBindMaster(); + if (spiritMaster != NULL) { + if (spiritMaster->IsType(hhBindController::Type)) { + hhBindController *binder = static_cast(spiritMaster); + bool loose = binder->IsLoose(); + binder->Detach(); + spiritProxy->DeactivateProxy(); + binder->Attach(this, loose); + } else if (spiritMaster->IsType(hhMonsterAI::Type)) { + if (reinterpret_cast (spiritMaster)->GetEnemy() != static_cast (spiritProxy.GetEntity())) { + gameLocal.Warning("Monster has spirit proxy bound to it but spirit proxy is not it's enemy!\n"); + } + spiritProxy->DeactivateProxy(); + } else { + gameLocal.Warning("Handle this: spiritwalking while bound to non-rail"); + idEntity *temp = spiritProxy.GetEntity(); // this removed by DeactivateProxy + spiritProxy->DeactivateProxy(); + SwapBindInfo(temp); // untested + } + } + else { // Normal case + spiritProxy->DeactivateProxy(); + } + } + + PutawayEtherealWeapon(); + + GetPhysics()->SetClipMask( MASK_PLAYERSOLID ); + fl.acceptsWounds = true; + spawnArgs.Set("produces_splats", "1"); + + // Turn off low-pass effect + if (gameLocal.localClientNum == entityNumber) { //rww - don't do this for anyone but the local client on listen servers + gameLocal.SpiritWalkSoundMode( false ); + } + + // HUMANHEAD mdl: Let our enemies know the spirit proxy is going away + for ( int i = 0; i < hhMonsterAI::allSimpleMonsters.Num(); i++ ) { + if ( hhMonsterAI::allSimpleMonsters[i]->GetEnemy() == spiritProxy.GetEntity() ) { + hhMonsterAI::allSimpleMonsters[i]->ProcessEvent( &MA_EnemyIsPhysical, this, spiritProxy.GetEntity() ); + } else if ( hhMonsterAI::allSimpleMonsters[i]->GetEnemy() == this ) { // Targetting spirit that is going away + hhMonsterAI::allSimpleMonsters[i]->ProcessEvent( &MA_EnemyIsPhysical, this, NULL ); + } + } + + //HUMANHEAD rww - clear the focus now that we've changed positions by switching out of spirit mode + focusTime = 0; + UpdateFocus(); + //HUMANHEAD END + + spiritProxy = NULL; +} + +//============================================================================= +// +// hhPlayer::GetResurrectionPoint +// +// Get the position/orientation for resurrection after deathwalk +//============================================================================= +void hhPlayer::GetResurrectionPoint( idVec3& origin, idMat3& axis, idVec3& viewDir, idAngles& angles, idMat3& eyeAxis, const idBounds& absBounds, const idVec3& defaultOrigin, const idMat3& defaultAxis, const idVec3& defaultViewDir, const idAngles& defaultAngles ) { + idClipModel *clipModelList[ MAX_GENTITIES ]; + idClipModel* clipModel = NULL; + idEntity* entity = NULL; + hhSafeResurrectionVolume* volume = NULL; + + int num = gameLocal.clip.ClipModelsTouchingBounds( absBounds, CONTENTS_DEATHVOLUME, clipModelList, MAX_GENTITIES ); + for( int ix = 0; ix < num; ++ix ) { + clipModel = clipModelList[ix]; + + if( !clipModel ) { + continue; + } + + entity = clipModel->GetEntity(); + if( !entity || !entity->IsType(hhSafeResurrectionVolume::Type) ) { + continue; + } + + volume = static_cast( entity ); + volume->PickRandomPoint( origin, axis ); + viewDir = axis[0]; + angles = axis.ToAngles(); + eyeAxis = mat3_identity; + return; + } + + eyeAxis = GetEyeAxis(); + origin = defaultOrigin; + axis = defaultAxis; + viewDir = defaultViewDir; + angles = defaultAngles; + +// gameLocal.Printf("Deathwalk @:\n"); +// gameLocal.Printf(" origin: %s\n", origin.ToString(0)); +// gameLocal.Printf(" axis: %s\n", axis.ToString(2)); +// gameLocal.Printf(" viewDir: %s\n", viewDir.ToString(2)); +// gameLocal.Printf(" angles: %s\n", angles.ToString(0)); +} + + +//============================================================================= +// +// hhPlayer::SquishedByDoor +// +//============================================================================= +void hhPlayer::SquishedByDoor(idEntity *door) { + if ( door == this ) { // Don't allow the squished code to send a player back it the squisher is the player itself + return; + } + + // If there is a spirit player in the door, make him go back to physical so he doesn't become stuck + if (IsSpiritWalking()) { + StopSpiritWalk(); + } +} + +//============================================================================= +// +// hhPlayer::UpdatePossession +// +// Updates the possession effects (if the player is possessed) +// +// HUMANHEAD cjr +//============================================================================= + +void hhPlayer::UpdatePossession( void ) { + + if ( !IsPossessed() ) { // Player is not possessed + return; + } + +/* todo: + possessionTimer -= MS2SEC( USERCMD_MSEC ); + + // Update a shaderparm for the view screen to update based upon possessionTimer + possessionMax = spawnArgs.GetFloat( "possessionTime", "8" ); + possessionScale = possessionTimer / possessionMax; + playerView.SetViewOverlayColor( idVec4( 1.0f, 1.0f, 1.0f, possessionScale ) ); + + // Mess with the player's FOV while they are being possessed + possessionFOV = g_fov.GetFloat() + (1.0f - possessionScale) * 60 + 1.5f * sin( MS2SEC(gameLocal.time) * 3 ); + + if ( possessionTimer <= 0 ) { // The player is now fully possessed, kill them + // TODO: Extra code to show the possessed player running around and such? + Unpossess(); + Damage( NULL, NULL, vec3_origin, "damage_crush", 1.0f, INVALID_JOINT ); + } +*/ +} + +//============================================================================= +// +// hhPlayer::RechargeHealth +// +// Recharges the player's health to 25% +// +// This is done to avoid gameplay issues where the player has only a few points +// of health. The game will still be tense if the player is at 25%, but +// will play more fairly. +// +// HUMANHEAD cjr +//============================================================================= + +void hhPlayer::Event_RechargeHealth( void ) { + + if ( health > 0 && health < spawnArgs.GetInt( "healthRecharge", "25" ) && !IsSpiritOrDeathwalking() ) { // Only recharge if the player is alive and in the critical zone + if ( gameLocal.time - lastDamagedTime >= spawnArgs.GetInt( "healthRechargeDelay", "1500" ) ) { // Delay before recharging + health++; + } + } + + PostEventSec( &EV_RechargeHealth, spawnArgs.GetFloat( "healthRechargeRate", "0.5" ) ); +} + +//============================================================================= +// +// hhPlayer::RechargeRifleAmmo +// +// Recharges rifle ammo if the player is very low. This is done to guarantee +// that the player has at least some ammo to solve puzzles that require shooting +// something such as a gravity switch. +// +// HUMANHEAD cjr +//============================================================================= + +void hhPlayer::Event_RechargeRifleAmmo( void ) { + + int ammoIndex = inventory.AmmoIndexForAmmoClass( "ammo_rifle" ); + int ammoCount = inventory.ammo[ ammoIndex ]; + + int maxAmmo = spawnArgs.GetInt( "rifleAmmoRechargeMax", "20" ); + if ( ammoCount < maxAmmo && !AI_ATTACK_HELD ) { // CJR PCF 04/26/06 + inventory.ammo[ ammoIndex ] += 2; + if ( inventory.ammo[ ammoIndex ] > maxAmmo ) { + inventory.ammo[ ammoIndex ] = maxAmmo; + } + } + + PostEventSec( &EV_RechargeRifleAmmo, spawnArgs.GetFloat( "rifleAmmoRechargeRate", "2" ) ); +} + +//============================================================================= +// +// hhPlayer::Event_DrainSpiritPower +// +// HUMANHEAD pdm: 10 Hz timer used to drain spirit power at differing rates +//============================================================================= +void hhPlayer::Event_DrainSpiritPower() { + + // JRM: God mode, don't drain! + if(godmode) { + return; + } + + ammo_t ammo_spiritpower = idWeapon::GetAmmoNumForName( "ammo_spiritpower" ); + + if ( gameLocal.isMultiplayer ) { // CJR: Drain spirit power in MP + if ( !UseAmmo( ammo_spiritpower, 1 ) ) { + StopSpiritWalk(); + return; + } + } + + // spirit bow alt mode drain + if( weapon.IsValid() && weapon->IsType(hhWeaponSpiritBow::Type) ) { + hhWeaponSpiritBow *bow = static_cast( weapon.GetEntity() ); + if( bow->BowVisionIsEnabled() ) { + if( !UseAmmo(ammo_spiritpower, 1) ) { // Bow vision drains spirit power + return; + } + } + } + + PostEventMS( &EV_DrainSpiritPower, spiritDrainHeartbeatMS ); +} + +//============================================================================= +// +// hhPlayer::DeathWalk +// +//============================================================================= + +void hhPlayer::DeathWalk( const idVec3& resurrectOrigin, const idMat3& resurrectBBoxAxis, const idMat3& resurrectViewAxis, const idAngles& resurrectViewAngles, const idMat3& resurrectEyeAxis ) { + if ( IsSpiritWalking() ) { // Ensure that two proxies cannot be active at the same time + StopSpiritWalk(); + } + + bDeathWalk = true; + deathWalkTime = gameLocal.time; // Get the time of death + + health = 0; // Force health to zero + + deathWalkPower = spawnArgs.GetInt( "deathWalkPowerStart" ); // Power in deathwalk. When full, the player will return + fl.takedamage = false; // can no longer be damaged in deathwalk mode + + if (hud) { + hud->HandleNamedEvent("SwitchToEthereal"); + } + + // Disable clip until we get to deathwalk + bInDeathwalkTransition = true; + + //rww - for deathwalk, let's store these values on the player instead of relying on the death proxy + deathwalkLastOrigin = resurrectOrigin; + deathwalkLastBBoxAxis = resurrectBBoxAxis; + deathwalkLastViewAxis = resurrectViewAxis; + deathwalkLastViewAngles = resurrectViewAngles; + deathwalkLastEyeAxis = resurrectEyeAxis; + deathwalkLastCrouching = IsCrouching(); + + EnableEthereal( spawnArgs.GetString("def_deathProxy"), resurrectOrigin, resurrectBBoxAxis, resurrectViewAxis, resurrectViewAngles, resurrectEyeAxis ); + // Set the overlay material for DeathWalk + playerView.SetViewOverlayColor( colorWhite ); // Guarantee that the color is reset + PostEventMS( &EV_SetOverlayMaterial, 1, spawnArgs.GetString("mtr_deathWalk"), false ); + + //Don't allow weapon use for a bit + if ( weapon.GetEntity() ) { + weapon.GetEntity()->PutAway(); + } + + // Alert the scripts that death has occured so they can manage music + idEntity *dwJustDied = gameLocal.FindEntity("dw_justdied"); + if (dwJustDied) { + dwJustDied->PostEventMS(&EV_Activate, 0, this); + } + + deathwalkSoundController.StartSound( SND_CHANNEL_BODY, SND_CHANNEL_BODY2 ); + + // Flash the screen before entering death world + playerView.SetViewOverlayColor( idVec4( 1.0f, 1.0f, 1.0f, 0.0f ) ); + deathWalkFlash = 0.0f; + possessionFOV = 90.0f; + + CancelEvents( &EV_RechargeHealth ); + CancelEvents( &EV_RechargeRifleAmmo ); + + // If this is our first time, tell the script + if (!gameLocal.IsCompetitive() && !inventory.bHasDeathwalked) { + const function_t *firstFunc = gameLocal.program.FindFunction( "map_deathwalk::FirstDeathwalk" ); + if ( firstFunc ) { + idThread *thread = new idThread(); + thread->CallFunction( firstFunc, false ); + thread->DelayedStart( 0 ); + } + } + + //rww - if there is a deathwalk portal, let's create a target for it and activate it + if (!gameLocal.FindEntity("dw_deathLocation")) { //check to see if we've made ourselves a target yet + idEntity *dwPortal = gameLocal.FindEntity( "dw_deathPortal" ); + if (dwPortal) { + trace_t tr; + idVec3 camPos, camDir; + idAngles camAngles; + idDict args; + + //FIXME more complex logic that allows angles to change based on collision circumstances + //do wider trace that takes the actual view frustum of camera into account + //check for collisions with the actual view "plane" + camPos = resurrectOrigin; + camPos += resurrectBBoxAxis[2] * 64.0f; + gameLocal.clip.TracePoint(tr, resurrectOrigin, camPos, CONTENTS_SOLID, this); + camDir = resurrectOrigin-camPos; + camAngles = camDir.ToAngles(); + + args.Clear(); + args.SetVector("origin", tr.endpos); + args.SetAngles("angles", camAngles); + args.SetMatrix("rotation", camAngles.ToMat3()); + args.Set("name", "dw_deathLocation"); + gameLocal.SpawnObject("info_null", &args); + + dwPortal->ProcessEvent(&EV_UpdateCameraTarget); //update for the newly placed cameraTarget + dwPortal->PostEventMS(&EV_Activate, 0, this); + if (dwPortal->cameraTarget) { //if camera target was legitimate, force an update to the remoteRenderView now + dwPortal->GetRenderEntity()->remoteRenderView = dwPortal->cameraTarget->GetRenderView(); + } + } + } + + Hide(); // Don't show the player when they are in the limbo state between alive and dead + PostEventSec( &EV_PrepareForDeathWorld, spawnArgs.GetFloat( "prepareForDeathWorldDelay", "1.5" ) ); +} + +//============================================================================= +// +// hhPlayer::Event_PrepareForDeathWorld +// +// Flashes the screen / fades the FOV +//============================================================================= + +void hhPlayer::Event_PrepareForDeathWorld() { + deathWalkFlash += spawnArgs.GetFloat( "deathWalkFlashChange", "0.05" ); + if ( deathWalkFlash >= 1.0f ) { + deathWalkFlash = 1.0f; + PostEventSec( &EV_EnterDeathWorld, 0.0f ); // Actually send player into the deathworld + } else { + PostEventSec( &EV_PrepareForDeathWorld, 0.02f ); + } + + possessionFOV += spawnArgs.GetFloat( "deathWalkFOVChange", "3.0" ); + playerView.SetViewOverlayColor( idVec4( 1.0f, 1.0f, 1.0f, deathWalkFlash ) ); +} + +//============================================================================= +// +// hhPlayer::Event_EnterDeathWorld +// +// - Teleports the player to the death world +// - Spawns the death wraiths +// - Gives the player a weapon +//============================================================================= + +void hhPlayer::Event_EnterDeathWorld() { + // Reset player view information + playerView.SetViewOverlayColor( idVec4( 1.0f, 1.0f, 1.0f, 0.0f ) ); // Turn off the flash + possessionFOV = 0.0f; + bDeathWalkStage2 = false; + + // Spawn in DeathWraiths after a short period of time + if ( !inventory.bHasDeathwalked ) { // Delay the wraiths the first time the player deathwalks + int firstDelay = spawnArgs.GetInt( "deathWalk_firstTimeDelay", "15000" ); + PostEventMS( &EV_SpawnDeathWraith, firstDelay ); // CJR: delay the deathwraiths for several seconds while the Grandfather DW speech is going on + PostEventMS( &EV_AdjustSpiritPowerDeathWalk, firstDelay + 2000 ); + inventory.bHasDeathwalked = true; + } else { + PostEventMS( &EV_SpawnDeathWraith, 0 ); // CJR: Non-first time, spawn the wraiths instantly + PostEventMS( &EV_AdjustSpiritPowerDeathWalk, 2000 ); + } + + // Give the player the spirit bow + SelectEtherealWeapon(); + + // get the entity which tells us where we want to place our dw proxy + idEntity *dwProxyPosEnt = gameLocal.FindEntity( "dw_floatingBodyMarker" ); + if (dwProxyPosEnt) { + //if we found that, then this is the new deathwalk, so create the deathwalk proxy. + hhDeathWalkProxy *dwProxy = (hhDeathWalkProxy *)gameLocal.SpawnObject( spawnArgs.GetString("def_deathWalkProxy"), NULL ); + if (!dwProxy) { + gameLocal.Error("hhPlayer::Event_EnterDeathWorld: Could not create hhDeathWalkProxy"); + return; + } + idAngles dwProxyAngles; + dwProxyPosEnt->spawnArgs.GetAngles("angles", "0 0 0", dwProxyAngles); + dwProxy->ActivateProxy(this, dwProxyPosEnt->GetOrigin(), dwProxyAngles.ToMat3(), dwProxyAngles.ToMat3(), dwProxyAngles, GetEyeAxis()); + } + + idEntity* deathWalkPlayerStartManager = gameLocal.FindEntity( "dw_deathWalkPlayerStartManager" ); + if( !deathWalkPlayerStartManager ) { + return; + } + + idEntity* deathWalkStart = deathWalkPlayerStartManager->PickRandomTarget(); + if( deathWalkStart ) { + deathWalkStart->ProcessEvent( &EV_Activate, this ); + } + + // Trigger the 'DeathWalkEntered' entity + // This allows the scripters to do something upon entering deathwalk + idEntity *dwe = gameLocal.FindEntity("dw_DeathWalkEntered"); + if (dwe) { + dwe->PostEventMS(&EV_Activate, 0, this); + } + + // Allow collisions again now that we're in the death world + bInDeathwalkTransition = false; + Show(); // The player is visible in deathmode + + // Reset the player's health and spirit power + ammo_t ammo_spiritpower = idWeapon::GetAmmoNumForName( "ammo_spiritpower" ); + inventory.ammo[ ammo_spiritpower ] = 0; + SetHealth( hhMath::hhMax( health, spawnArgs.GetInt("minResurrectHealth", "50") ) ); +} + +//============================================================================= +// +// hhPlayer::Event_AdjustSpiritPowerDeathWalk +// +//============================================================================= +void hhPlayer::Event_AdjustSpiritPowerDeathWalk() { + if( !IsDeathWalking() ) { + return; + } + + // Increase deathwalk power + int power = GetDeathWalkPower(); + power += spawnArgs.GetInt( "deathWraithPowerAmount" ); + SetDeathWalkPower( power ); + + // Check if deathWalkPower has exceeded the maximum + if ( deathWalkPower >= spawnArgs.GetInt( "deathWalkPowerMax", "1000") ) { + DeathWalkSuccess(); + return; + } + + CancelEvents( &EV_AdjustSpiritPowerDeathWalk ); + PostEventSec( &EV_AdjustSpiritPowerDeathWalk, spawnArgs.GetFloat("deathWalkDeathPowerIncreaseRate") ); +} + +//============================================================================= +// +// hhPlayer::DeathWalkSuccess +// +// Called when deathwalk timer runs out or we run out of spirit power +//============================================================================= +void hhPlayer::DeathWalkSuccess() { + CancelEvents( &EV_AdjustSpiritPowerDeathWalk ); + bDeathWalkStage2 = true; + + idEntity *dw_deathWalkResult = gameLocal.FindEntity("dw_deathWalkSuccess"); + if (dw_deathWalkResult) { //if we have a result, activate it. + dw_deathWalkResult->ProcessEvent(&EV_Activate, this); + } +} + +//============================================================================= +// +// hhPlayer::ReallyKilled +// +// Called when the player should be truly killed +// Funnels through Killed(), but disabled the deathwalk functionality +//============================================================================= + +void hhPlayer::ReallyKilled( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if ( !IsSpiritOrDeathwalking() ) { + return; + } + + bDeathWalk = false; + bSpiritWalk = false; + + bReallyDead = true; + + // Turn off low-pass effect + if (!gameLocal.isClient) { //rww + if (gameLocal.localClientNum == entityNumber) { //rww - don't do this for anyone but the local client on listen servers + gameLocal.SpiritWalkSoundMode( false ); + gameLocal.DialogSoundMode( false ); + } + } + + if (hud) { + hud->HandleNamedEvent("SwitchFromEthereal"); + } + + Killed( inflictor, attacker, damage, dir, location ); +} + +//============================================================================= +// +// hhPlayer::Event_SpawnDeathWraith +// +// Spawns in a special type of wraith that is only visible in deathwalk +//============================================================================= + +void hhPlayer::Event_SpawnDeathWraith() { + idDict args; + idEntity *ent; + int maxDeathWalkWraiths = spawnArgs.GetInt( "deathWalkMaxWraiths" ); + + if ( !IsDeathWalking() ) { // Don't spawn in a wraith if the player has resurrected + return; + } + + float angle = hhMath::PI * gameLocal.random.RandomFloat(); + idVec3 startPoint = idVec3( hhMath::Sin(angle) * 600.0f, hhMath::Cos(angle) * 600.0f, 0.0f ) - GetAxis()[2] * 100.0f; + + args.Clear(); + args.SetVector( "origin", GetEyePosition() + startPoint ); + args.SetMatrix( "rotation", idAngles( 0.0f, (-viewAxis[0]).ToYaw(), 0.0f ).ToMat3() ); + ent = gameLocal.SpawnObject( spawnArgs.GetString("def_deathWraith"), &args ); + if ( ent ) { + hhDeathWraith *wraith = static_cast< hhDeathWraith * > ( ent ); + wraith->SetEnemy( this ); + } + + // Check the number of deathwraiths in the world + int count = 0; + for( ent = gameLocal.activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { + if ( ent && ent->IsType( hhDeathWraith::Type ) ) { + count++; + } + } + + if ( count < maxDeathWalkWraiths ) { + PostEventMS( &EV_SpawnDeathWraith, 0 ); + } +} + +//============================================================================= +// +// hhPlayer::GetDeathwalkEnergyDestination +// +//============================================================================= +idEntity *hhPlayer::GetDeathwalkEnergyDestination() { + return gameLocal.FindEntityOfType(hhDeathWalkProxy::Type, NULL); +} + +//============================================================================= +// +// hhPlayer::DeathWalkDamagedByWraith +// +//============================================================================= +void hhPlayer::DeathWalkDamagedByWraith(idEntity *attacker, const char *damageType) { + Damage( attacker, attacker, vec3_origin, damageType, spawnArgs.GetFloat( "deathWalkWraithDamage", "10" ), INVALID_JOINT ); + + CancelEvents( &EV_SpawnDeathWraith ); + PostEventMS( &EV_SpawnDeathWraith, 0 ); +} + +//============================================================================= +// +// hhPlayer::KilledDeathWraith +// +//============================================================================= + +void hhPlayer::KilledDeathWraith( void ) { + CancelEvents( &EV_SpawnDeathWraith ); + PostEventMS( &EV_SpawnDeathWraith, 0 ); +} + +//============================================================================= +// +// hhPlayer::DeathWraithEnergyArived +// +//============================================================================= + +void hhPlayer::DeathWraithEnergyArived(bool energyHealth) { + if (energyHealth) { + const char *healthAmount = spawnArgs.GetString("deathWraithHealthAmount"); + Give( "health", healthAmount ); + } + else { + Give( "ammo_spiritpower", spawnArgs.GetString( "deathWraithSpiritAmount" ) ); + } + + // Bump the deathwalk power up a bit so the player lowers faster + int power = GetDeathWalkPower(); + power += spawnArgs.GetInt( "deathWalkPowerEnergyBoost", "100" ); + SetDeathWalkPower( power ); +} + +//============================================================================= +// +// hhPlayer::Resurrect +// +//============================================================================= + +void hhPlayer::Resurrect( void ) { + + bDeathWalk = false; + + possessionFOV = 0.0f; + + PostEventSec( &EV_AllowDamage, 3 ); // Re-enable damage in 3 seconds + fl.noknockback = false; // Restore knockback ability + + // Cancel the impending translation to deathworld (only happens if the player resurrect cheats early) + CancelEvents( &EV_PrepareForDeathWorld ); + CancelEvents( &EV_EnterDeathWorld ); + CancelEvents( &EV_SpawnDeathWraith ); + + DisableEthereal(); + + // Reset the view overlay + playerView.SetViewOverlayTime( 0, true ); + playerView.SetViewOverlayColor( idVec4( 1.0f, 1.0f, 1.0f, 0.0f ) ); + + //rww - deathwalk position is restored from the player now. + RestorePlayerLocationFromDeathwalk(deathwalkLastOrigin, deathwalkLastBBoxAxis, deathwalkLastViewAxis[0], deathwalkLastViewAngles); + + deathwalkSoundController.StopSound( SND_CHANNEL_BODY, SND_CHANNEL_BODY2 ); + + ProcessEvent( &EV_RechargeHealth ); + ProcessEvent( &EV_RechargeRifleAmmo ); + + if (hud) { + hud->HandleNamedEvent("SwitchFromEthereal"); + } + + // TODO: Build in an autoimmune time? + lastResurrectTime = gameLocal.GetTime(); + + // Radius blast (doesn't damage enemies / objects, only shoves them back) + gameLocal.RadiusDamage( GetPhysics()->GetOrigin(), this, this, this, this, spawnArgs.GetString("def_resurrect_damage") ); + + Show(); // Ensure that the player is visible when they resurrect + + // trigger the 'DeathWalkExited' entity + idEntity *dwe = gameLocal.FindEntity("dw_DeathWalkExited"); + if (dwe) { + dwe->PostEventMS(&EV_Activate, 0, this); + } + + // bg: Visual effect for returning from DeathWalk uses landing camera movement. + landChange = -10; + landTime = gameLocal.time; + + //rww - deactivate dw portal and remove cam target + idEntity *dwPortal = gameLocal.FindEntity( "dw_deathPortal" ); + if (dwPortal && dwPortal->IsActive()) { + dwPortal->cameraTarget = NULL; //reset the camera target + dwPortal->PostEventMS(&EV_Activate, 0, dwPortal); + } + idEntity *dwCamTarget = gameLocal.FindEntity( "dw_deathLocation" ); + if (dwCamTarget) { + dwCamTarget->PostEventMS(&EV_Remove, 50); //make sure this is removed after the portal has stopped. + } +} + +/* +============ +hhPlayer::IsWallWalking +============ +*/ +bool hhPlayer::IsWallWalking( void ) const { + return( physicsObj.IsWallWalking() ); +} + +/* +============ +hhPlayer::WasWallWalking +============ +*/ +bool hhPlayer::WasWallWalking( void ) const { + return( physicsObj.WasWallWalking() ); +} + +/* +============ +hhPlayer::MangleControls +============ +*/ +void hhPlayer::MangleControls( usercmd_t *cmd ) { + // When frozen in a cinematic, we want to restrict any movement and disallow many features + if (guiWantsControls.IsValid()) { + if (cmd->buttons & BUTTON_ATTACK_ALT) { + guiWantsControls->LockedGuiReleased(this); + guiWantsControls = NULL; // release + } + else { + guiWantsControls->PlayerControls(cmd); + } + cmd->forwardmove = 0; + cmd->rightmove = 0; + cmd->upmove = 0; + cmd->buttons &= ~(BUTTON_ATTACK|BUTTON_ZOOM|BUTTON_ATTACK_ALT); + cmd->impulse = 0; + } + else if (InCinematic()) { + cmd->forwardmove = 0; + cmd->rightmove = 0; + cmd->upmove = 0; + cmd->buttons &= ~(BUTTON_ATTACK|BUTTON_ZOOM|BUTTON_ATTACK_ALT); + cmd->impulse = 0; + } +} + +/* +============ +hhPlayer::GetPilotInput + +Called from PilotVehicleInterface +============ +*/ +void hhPlayer::GetPilotInput( usercmd_t& pilotCmds, idAngles& pilotViewAngles ) { + pilotCmds = gameLocal.usercmds[ entityNumber ]; + pilotViewAngles = DetermineViewAngles( pilotCmds, cmdAngles ); +} + +/* +============== +hhPlayer::Think + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +============== +*/ +void hhPlayer::Think( void ) { + renderEntity_t *headRenderEnt; + + UpdatePossession(); + + UpdatePlayerIcons(); + + // latch button actions + oldButtons = usercmd.buttons; + + // grab out usercmd + usercmd_t oldCmd = usercmd; + usercmd = gameLocal.usercmds[ entityNumber ]; + buttonMask &= usercmd.buttons; + usercmd.buttons &= ~buttonMask; + + if ( gameLocal.inCinematic && gameLocal.skipCinematic ) { + return; + } + + //HUMANHEAD rww - keep air attacker timer refreshed + if (!AI_ONGROUND && airAttackerTime > gameLocal.time) { + airAttackerTime = gameLocal.time + 300; + } + + // HUMANHEAD pdm: Depending on our mode, might want to modify the input + MangleControls( &usercmd ); + // HUMANHEAD END + + // clear the ik before we do anything else so the skeleton doesn't get updated twice + walkIK.ClearJointMods(); + + // if this is the very first frame of the map, set the delta view angles + // based on the usercmd angles + if ( !spawnAnglesSet && ( gameLocal.GameState() != GAMESTATE_STARTUP ) ) { + spawnAnglesSet = true; + SetViewAngles( spawnAngles ); + oldFlags = usercmd.flags; + } + + if ( gameLocal.inCinematic || influenceActive ) { + usercmd.forwardmove = 0; + usercmd.rightmove = 0; + usercmd.upmove = 0; + } + + // log movement changes for weapon bobbing effects + if ( usercmd.forwardmove != oldCmd.forwardmove ) { + loggedAccel_t *acc = &loggedAccel[currentLoggedAccel&(NUM_LOGGED_ACCELS-1)]; + currentLoggedAccel++; + acc->time = gameLocal.time; + acc->dir[0] = usercmd.forwardmove - oldCmd.forwardmove; + acc->dir[1] = acc->dir[2] = 0; + } + + if ( usercmd.rightmove != oldCmd.rightmove ) { + loggedAccel_t *acc = &loggedAccel[currentLoggedAccel&(NUM_LOGGED_ACCELS-1)]; + currentLoggedAccel++; + acc->time = gameLocal.time; + acc->dir[1] = usercmd.rightmove - oldCmd.rightmove; + acc->dir[0] = acc->dir[2] = 0; + } + + // freelook centering + if ( ( usercmd.buttons ^ oldCmd.buttons ) & BUTTON_MLOOK ) { + centerView.Init( gameLocal.time, 200, viewAngles.pitch, 0 ); + } + + // zooming + if ( ( usercmd.buttons ^ oldCmd.buttons ) & BUTTON_ZOOM ) { + if ( ( usercmd.buttons & BUTTON_ZOOM ) && weapon.GetEntity() ) { + zoomFov.Init( gameLocal.time, 200.0f, CalcFov( false ), weapon.GetEntity()->GetZoomFov() ); + } else { + zoomFov.Init( gameLocal.time, 200.0f, zoomFov.GetCurrentValue( gameLocal.time ), DefaultFov() ); + } + } + + if ( g_fov.IsModified() ) { + idEntity *weaponEnt = weapon.GetEntity(); + if ( ! ( weaponEnt && + weaponEnt->IsType( hhWeaponZoomable::Type ) && + reinterpret_cast (weaponEnt)->IsZoomed() ) ) + { + GetZoomFov().Init( gameLocal.GetTime(), 0.0f, CalcFov(true), g_fov.GetInteger() ); + g_fov.ClearModified(); + } + } + + // if we have an active gui, we will unrotate the view angles as + // we turn the mouse movements into gui events + idUserInterface *gui = ActiveGui(); + if ( gui && gui != focusUI ) { + RouteGuiMouse( gui ); + } + + // set the push velocity on the weapon before running the physics + if ( weapon.GetEntity() ) { + weapon.GetEntity()->SetPushVelocity( physicsObj.GetPushedLinearVelocity() ); + } + + EvaluateControls(); + + if ( !af.IsActive() ) { + AdjustBodyAngles(); + CopyJointsFromBodyToHead(); + } + + //HUMANHEAD: aob - added vehicle check. Vehicle moves us + if( !InVehicle() ) { + Move(); + } + //HUMANHEAD END + + if ( !g_stopTime.GetBool() ) { + //HUMANHEAD: aob - changed heath check to IsDead check. Player needs to touch triggers when deathwalking. + if ( !noclip && !spectating && !IsDead() && !IsHidden() ) { + TouchTriggers(); + } + + UpdateLighter(); + + // update GUIs, Items, and character interactions + UpdateFocus(); + + UpdateLocation(); + + // update player script + UpdateScript(); + + // service animations + if ( !spectating && !af.IsActive() && !gameLocal.inCinematic ) { + UpdateConditions(); + UpdateAnimState(); + CheckBlink(); + } + + // clear out our pain flag so we can tell if we recieve any damage between now and the next time we think + AI_PAIN = false; + } + + // calculate the exact bobbed view position, which is used to + // position the view weapon, among other things + CalculateFirstPersonView(); + + // this may use firstPersonView, or a thirdPeroson / camera view + CalculateRenderView(); + + if ( spectating ) { + if (!gameLocal.isClient) { + UpdateSpectating(); + } + //HUMANHEAD: aob - changed heath check to IsDead check. Player needs to update weapon when deathwalking + } else if ( !IsDead() && !bFrozen ) { //HUMANHEAD bjk PCF (4-27-06) - no setting unnecessary weapon state + if ( !gameLocal.isClient || weapon.GetEntity()) { + UpdateWeapon(); + } + } + + if (InVehicle()) { + UpdateHud( GetVehicleInterfaceLocal()->GetHUD() ); + } + else { + UpdateHud( hud ); + } + + UpdateDeathSkin( false ); + + if ( gameLocal.isMultiplayer ) { + DrawPlayerIcons(); + } + + if ( head.GetEntity() ) { + headRenderEnt = head.GetEntity()->GetRenderEntity(); + } else { + headRenderEnt = NULL; + } + + if ( gameLocal.isMultiplayer || g_showPlayerShadow.GetBool() ) { + renderEntity.suppressShadowInViewID = 0; + if ( headRenderEnt ) { + headRenderEnt->suppressShadowInViewID = 0; + } + } else { + renderEntity.suppressShadowInViewID = entityNumber+1; + if ( headRenderEnt ) { + headRenderEnt->suppressShadowInViewID = entityNumber+1; + } + } + // never cast shadows from our first-person muzzle flashes + renderEntity.suppressShadowInLightID = LIGHTID_VIEW_MUZZLE_FLASH + entityNumber; + if ( headRenderEnt ) { + headRenderEnt->suppressShadowInLightID = LIGHTID_VIEW_MUZZLE_FLASH + entityNumber; + } + + if ( !g_stopTime.GetBool() ) { + UpdateAnimation(); + Present(); + UpdateDamageEffects(); + if (gameLocal.isMultiplayer) { //rww + UpdateWounds(); + } + LinkCombat(); + playerView.CalculateShake(); + } + + if ( g_showEnemies.GetBool() ) { + idActor *ent; + int num = 0; + for( ent = enemyList.Next(); ent != NULL; ent = ent->enemyNode.Next() ) { + gameLocal.Printf( "enemy (%d)'%s'\n", ent->entityNumber, ent->name.c_str() ); + gameRenderWorld->DebugBounds( colorRed, ent->GetPhysics()->GetBounds().Expand( 2 ), ent->GetPhysics()->GetOrigin() ); + num++; + } + gameLocal.Printf( "%d: enemies\n", num ); + } +} + +/* +============== +hhPlayer::AdjustBodyAngles +============== +*/ +void hhPlayer::AdjustBodyAngles( void ) { + if (InVehicle()) { + return; + } + + if (bClampYaw) { //rww - for slabs + //first, clamp the viewangles while bound to a slab + if (maxRelativePitch >= 0.0f) { + if (untransformedViewAngles.pitch > maxRelativePitch) { + untransformedViewAngles.pitch = maxRelativePitch; + SetViewAngles(untransformedViewAngles); + } + else if (untransformedViewAngles.pitch < -maxRelativePitch) { + untransformedViewAngles.pitch = -maxRelativePitch; + SetViewAngles(untransformedViewAngles); + } + } + + animator.SetJointAxis( hipJoint, JOINTMOD_WORLD, mat3_identity ); //no leg offset while slabbed + +#if 0 //old headlook code + if (head.IsValid() && head.GetEntity()) { //has a head + idVec3 origin; + idMat3 axis; + + head->GetPhysics()->Evaluate(gameLocal.time-gameLocal.previousTime, gameLocal.time); //make sure it is bolted up to date + + if (GetMasterPosition(origin, axis)) { + idAngles masterAngles = axis.ToAngles(); //bound angles + idAngles ang; + idMat3 rot; + + //determine our "look" angles + ang.yaw = viewAngles.yaw-masterAngles.yaw; + ang.pitch = viewAngles.pitch; + ang.roll = 0.0f; + + ang = ang.Normalize180(); + ang *= 0.5f; //scale the angles down so the head doesn't go completely sideways or anything + + //set the yaw only + animator.SetJointAxis(headJoint, JOINTMOD_WORLD, idAngles(0.0f, ang.yaw, 0.0f).ToMat3()); + ang.yaw = 0.0f; + + hhMath::BuildRotationMatrix(DEG2RAD(-ang.pitch), 0, rot); //create a rotation matrix for the pitch + + //get the local axis to multiply it by the pitch rotation matrix + animator.GetJointLocalTransform(headJoint, gameLocal.time, origin, axis); + + //use the multiplied axis + animator.SetJointAxis(headJoint, JOINTMOD_LOCAL, rot*axis); //point head in proper direction + } + } +#endif + } + else { +#if 0 //old headlook code + //rww - reset head joint override when not on a slab + animator.SetJointAxis( headJoint, JOINTMOD_LOCAL, idAngles( 0.0f, 0.0f, 0.0f ).ToMat3() ); +#endif + idPlayer::AdjustBodyAngles(); + } +} + +/* +============== +hhPlayer::Move + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +============== +*/ +void hhPlayer::Move( void ) { + float newEyeOffset; + idVec3 oldOrigin; + idVec3 oldVelocity; + idVec3 pushVelocity; + + // save old origin and velocity for crashlanding + oldOrigin = physicsObj.GetOrigin(); + oldVelocity = physicsObj.GetLinearVelocity(); + pushVelocity = physicsObj.GetPushedLinearVelocity(); + + // set physics variables + physicsObj.SetMaxStepHeight( GetStepHeight() );//HUMANHEAD: aob + physicsObj.SetMaxJumpHeight( pm_jumpheight.GetFloat() ); + + if ( noclip ) { + physicsObj.SetContents( 0 ); + physicsObj.SetMovementType( PM_NOCLIP ); + } else if ( spectating ) { + physicsObj.SetContents( 0 ); + physicsObj.SetMovementType( PM_SPECTATOR ); + } else if ( bInDeathwalkTransition ) { // In between life and death, don't allow ragdoll to collide with player + physicsObj.SetContents( 0 ); + physicsObj.SetMovementType( PM_NORMAL ); + } else if ( IsDead() ) { // HUMANHEAD cjr: Replaced health <= 0 with IsDead() call for deathwalk override + if (gameLocal.isMultiplayer) { // HUMANHEAD rww - contents 0 because we use a dead body proxy in mp + physicsObj.SetContents(0); + } + else { + physicsObj.SetContents( CONTENTS_CORPSE | CONTENTS_MONSTERCLIP ); + } //HUMANHEAD END + physicsObj.SetMovementType( PM_DEAD ); + } else if ( gameLocal.inCinematic || gameLocal.GetCamera() || privateCameraView ) { + physicsObj.SetContents( CONTENTS_BODY ); + physicsObj.SetMovementType( PM_FREEZE ); + } else if ( bFrozen && !IsSpiritOrDeathwalking() ) { // HUMANHEAD: freeze support + physicsObj.SetContents( CONTENTS_BODY ); + physicsObj.SetMovementType( PM_FREEZE ); + } else { + physicsObj.SetContents( CONTENTS_BODY ); + physicsObj.SetMovementType( PM_NORMAL ); + } + + if ( spectating ) { + physicsObj.SetClipMask( MASK_SPECTATOR ); + } else if ( IsDead() ) { // HUMANHEAD cjr: Replaced health <= 0 with IsDead() call for deathwalk override + physicsObj.SetClipMask( MASK_DEADSOLID ); + // HUMANHEAD cjr: allow spirit walk + } else if ( IsSpiritOrDeathwalking() ) { + physicsObj.SetClipMask( MASK_SPIRITPLAYER ); + // HUMANHEAD END + } else { + physicsObj.SetClipMask( MASK_PLAYERSOLID ); + } + + physicsObj.SetDebugLevel( g_debugMove.GetBool() ); + physicsObj.SetPlayerInput( usercmd, viewAngles ); + + //HUMANHEAD: aob - moved down a few lines + // FIXME: physics gets disabled somehow + //BecomeActive( TH_PHYSICS ); + //RunPhysics(); + //HUMANHEAD END + + // update our last valid AAS location for the AI + SetAASLocation(); + + if ( spectating ) { + newEyeOffset = 0.0f; + // HUMANHEAD cjr: Replaced health <= 0 with IsDead() call for deathwalk override + } else if ( IsDead() ) { + newEyeOffset = pm_deadviewheight.GetFloat(); + } else if ( physicsObj.IsCrouching() ) { + newEyeOffset = pm_crouchviewheight.GetFloat(); + } else if ( GetBindMaster() && GetBindMaster()->IsType( idAFEntity_Vehicle::Type ) ) { + newEyeOffset = 0.0f; + } else { + newEyeOffset = pm_normalviewheight.GetFloat(); + } + + if ( EyeHeight() != newEyeOffset ) { + //AOB: camera does smoothing + SetEyeHeight( newEyeOffset ); + } + + // HUMANHEAD aob + float camRotScale = p_camRotRateScale.GetFloat(); + //rww - if on a wallwalk mover, increase the scale + if (GetPhysics() == &physicsObj && physicsObj.GetGroundTrace().fraction < 1.0f) { + const trace_t &grTr = physicsObj.GetGroundTrace(); + if (grTr.c.entityNum >= 0 && grTr.c.entityNum < MAX_GENTITIES && gameLocal.entities[grTr.c.entityNum] && gameLocal.entities[grTr.c.entityNum]->IsType(hhMover::Type) && grTr.c.material && grTr.c.material->GetSurfaceType() == SURFTYPE_WALLWALK) { + camRotScale *= 5.0f; + } + } + cameraInterpolator.Setup( camRotScale, IT_VariableMidPointSinusoidal );//Also set in constructor + + // moved here to allow weapons physics get correct eyeOffset + BecomeActive( TH_PHYSICS ); //rww - prevent the bug that id so lovingly left for us + RunPhysics(); + // HUMANHEAD END + + // HUMANHEAD pdm: Experimental: allow player to jump up onto wallwalk +#define WALLWALK_EXPERIMENT 0 +#if WALLWALK_EXPERIMENT + if (!physicsObj.IsWallWalking() && physicsObj.HasJumped()) { + trace_t trace; + memset(&trace, 0, sizeof(trace)); + idVec3 start, end; + start = physicsObj.GetOrigin(); + end = start + idVec3(0,0,100); //fixme: jumpheight + if (gameLocal.clip.TracePoint( trace, start, end, MASK_SOLID, this )) { + if (gameLocal.GetMatterType(trace, NULL) == SURFTYPE_WALLWALK) { + gameLocal.Printf("Trying to flip over for wallwalk\n"); + + // Clear velocity, so won't miss wallwalk above + physicsObj.SetLinearVelocity(vec3_origin); + + SetGravity(-trace.c.normal * DEFAULT_GRAVITY); + ProcessEvent( &EV_ShouldRemainAlignedToAxial, (int)false ); + ProcessEvent( &EV_OrientToGravity, (int)true ); + + physicsObj.IsWallWalking( true ); + +/* idVec3 flippedOrigin = trace.endpos; + idMat3 flippedAxis; + flippedAxis[0] = GetAxis()[0]; + flippedAxis[2] = trace.c.normal; + flippedAxis[1] = flippedAxis[0].Cross(flippedAxis[2]); + +// cameraInterpolator.SetInterpolationType(flags); +// cameraInterpolator.SetTargetEyeOffset(idealEyeOffset, flags); +// cameraInterpolator.SetTargetPosition(idealPosition, flags); + + // Flip the physics object + physicsObj.SetOrigin(flippedOrigin); + physicsObj.SetAxis(flippedAxis); + physicsObj.CheckWallWalk(true);*/ + } + } + } +#endif + // HUMANHEAD END + + if ( noclip || gameLocal.inCinematic ) { + AI_CROUCH = false; + AI_ONGROUND = false; + AI_ONLADDER = false; + AI_JUMP = false; + AI_REALLYFALL = true; //HUMANHEAD rww - well, it fits with ONGROUND false + } else { + AI_CROUCH = physicsObj.IsCrouching(); + AI_ONGROUND = physicsObj.HasGroundContacts(); + AI_ONLADDER = physicsObj.OnLadder(); + AI_JUMP = physicsObj.HasJumped(); + //HUMANHEAD rww + if (!physicsObj.IsInwardGravity()) { + AI_REALLYFALL = !AI_ONGROUND; + } + else if (!AI_ONGROUND) { //for inward gravity zones, do an extra ground check when in-air + AI_REALLYFALL = !physicsObj.ExtraGroundCheck(); + } + //HUMANHEAD END + + // check if we're standing on top of a monster and give a push if we are + idEntity *groundEnt = physicsObj.GetGroundEntity(); + if ( groundEnt && groundEnt->IsType( idAI::Type ) ) { + idVec3 vel = physicsObj.GetLinearVelocity(); + if ( vel.ToVec2().LengthSqr() < 0.1f ) { + vel.ToVec2() = physicsObj.GetOrigin().ToVec2() - groundEnt->GetPhysics()->GetAbsBounds().GetCenter().ToVec2(); + vel.ToVec2().NormalizeFast(); + vel.ToVec2() *= pm_walkspeed.GetFloat(); + } else { + // give em a push in the direction they're going + vel *= 1.1f; + } + physicsObj.SetLinearVelocity( vel ); + } + } + + if ( AI_JUMP ) { + // bounce the view weapon + loggedAccel_t *acc = &loggedAccel[currentLoggedAccel&(NUM_LOGGED_ACCELS-1)]; + currentLoggedAccel++; + acc->time = gameLocal.time; + acc->dir[2] = 200; + acc->dir[0] = acc->dir[1] = 0; + } + + BobCycle( pushVelocity ); + if ( !noclip ) { // HUMANHEAD: Only crashland if not spiritwalking or not deathwalking + CrashLand( oldOrigin, oldVelocity ); + } + + //HUMANHEAD: aob - put this into helper func + if (!gameLocal.isClient) { + if( IsWallWalking() && !WasWallWalking() ) { + wallwalkSoundController.StartSound( SND_CHANNEL_WALLWALK, SND_CHANNEL_WALLWALK2, SSF_LOOPING, false ); + } else if( !IsWallWalking() && WasWallWalking() ) { + wallwalkSoundController.StopSound( SND_CHANNEL_WALLWALK, SND_CHANNEL_WALLWALK2, false ); + } + } + physicsObj.WasWallWalking( IsWallWalking() ); //rww - moved here + //HUMANHEAD END +} + +/* +=============== +hhPlayer::ShouldTouchTrigger +=============== +*/ +bool hhPlayer::ShouldTouchTrigger( idEntity* entity ) const { + if( !entity ) { + return false; + } + + if ( entity->fl.onlySpiritWalkTouch ) { // cjr - Trigger can only be touched by spirit + return IsSpiritWalking(); + } + + if( IsSpiritOrDeathwalking() && !entity->fl.allowSpiritWalkTouch ) { // Trigger can be touched by either physical or spirit + return false; // jrm - reversed logic + } + + return true; +} + +/* +=============== +hhPlayer::HandleSingleGuiCommand +=============== +*/ +bool hhPlayer::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + + idToken token; + + if (!src->ReadToken(&token)) { + return false; + } + + if (token == ";") { + return false; + } + + if (token.Icmp("guilockplayer") == 0) { + //Now locked in place: + // manglecontrols() will route controls to current gui focus + if (entityGui->IsType(hhConsole::Type)) { + guiWantsControls = static_cast(entityGui); + } + return true; + } + + src->UnreadToken(&token); + return idPlayer::HandleSingleGuiCommand(entityGui, src); +} + +void hhPlayer::SetOverlayGui(const char *guiName) { + if (guiName && *guiName && guiOverlay == NULL) { + guiOverlay = uiManager->FindGui(guiName, true); + if (guiOverlay) { + guiOverlay->Activate(true, gameLocal.time); + } + } + else { + guiOverlay = NULL; + } +} + +/* +============ +hhPlayer::ForceWeapon +nla: used to instantly force the spirit weapon, without lowering and raising +============ +*/ +void hhPlayer::ForceWeapon( int weaponNum ) { + const char * weaponDef; + hhWeapon * newWeapon; + + if (spectating) { //rww - if spectating this is bad. + gameLocal.Error("hhPlayer::ForceWeapon called on spectator. (client %i)", entityNumber); + } + + // HUMANHEAD mdl: Special case - coming out of spirit mode when no physical weapon is available + if ( weaponNum == -1 ) { + idealWeapon = 0; + currentWeapon = -1; + return; + } + + if ( weaponNum < 1 ) { + gameLocal.Warning( "Error: Illegal weapon num passed: %d\n", weaponNum ); + } + + weaponDef = GetWeaponName( weaponNum ); + + newWeapon = SpawnWeapon( weaponDef ); + + weapon = newWeapon; + + idealWeapon = currentWeapon = weaponNum; + + animPrefix = weaponDef; + animPrefix.Strip( "weaponobj_" ); +} + + +/* +============ +hhPlayer::ForceWeapon +============ +*/ +void hhPlayer::ForceWeapon( hhWeapon *newWeapon ) { + int weaponNum; + + + weapon = newWeapon; + + weaponNum = GetWeaponNum( weapon->GetDict()->GetString( "classname" ) ); + + idealWeapon = currentWeapon = weaponNum; + +} + +/* +=========== +hhPlayer::Teleport + +HUMANHEAD cjr: Now calls TeleportNoKillBox, then applies a kill box +============ +*/ +void hhPlayer::Teleport( const idVec3& origin, const idAngles& angles, idEntity *destination ) { + //pdm - converted this to call our other version so it doesn't unalign the clip model + TeleportNoKillBox( origin, mat3_identity, angles.ToForward(), angles ); + + // mdl: Moved here from TeleportNoBox, so player can't avoid falling damage by toggling spirit mode quickly + physicsObj.SetLinearVelocity( vec3_origin ); + physicsObj.SetKnockBack( 250 ); // Slow the player down after a teleport for a moment + + // kill anything at the new position + gameLocal.KillBox( this, destination != NULL ); + + teleportEntity = destination; +} + +/* +=========== +hhPlayer::Teleport + +HUMANHEAD aob: Needed for coming back from deathwalk +============ +*/ +void hhPlayer::Teleport( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& newUntransformedViewAngles, idEntity *destination ) { + TeleportNoKillBox( origin, bboxAxis, viewDir, newUntransformedViewAngles ); + + // kill anything at the new position + gameLocal.KillBox( this, destination != NULL ); + + teleportEntity = destination; +} + +/* +=========== +hhPlayer::TeleportNoKillBox + +HUMANHEAD cjr: Called from SpiritProxy +============ +*/ +void hhPlayer::TeleportNoKillBox( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& newUntransformedViewAngles ) { + DisableIK(); + + SetOrientation( origin, bboxAxis, viewDir, newUntransformedViewAngles ); + + CancelEvents( &EV_ResetGravity ); + ProcessEvent( &EV_ResetGravity ); // Guarantee that gravity is instantly reset after this teleport + + EnableIK(); +} + +/* +=========== +hhPlayer::TeleportNoKillBox + +HUMANHEAD cjr: Teleport, but don't telefrag anything +============ +*/ + +void hhPlayer::TeleportNoKillBox( const idVec3& origin, const idMat3& bboxAxis ) { + TeleportNoKillBox( origin, bboxAxis, GetAxis()[0], GetUntransformedViewAngles() ); +} + +/* +===================== +hhPlayer::Possess + +//HUMANHEAD: cjr - this is the entry point for possession. +===================== +*/ +void hhPlayer::Possess( idEntity* possessor ) { + bPossessed = true; + + /* TODO: + - needs a new spirit proxy (a little hidden object just as storage that is linked to the possessed human) + - disallow the player from returning until the possessed tommy has been "killed" + + + possessed tommy: + - attacks whatever + - health ticking down -- when it reaches zero, ragdoll and instantly kill the spirit player + - if killed by the spirit bow, then the wraith is removed (seperate health?) + */ + + // If we're spirit walking when we're possessed, snap back to our body for a moment, then get thrown back out. + //TODO this isn't ideal + if ( IsSpiritOrDeathwalking() ) { + DisableEthereal(); + } + + // Must allow spirit walking at this point + HH_ASSERT( bAllowSpirit ); + + // Force the player into spirit and thrust them backwards + StartSpiritWalk( true, true ); + + // test: spawn in a possessed version + // need: effects here...flash or whatever + possessedTommy = (hhPossessedTommy *)gameLocal.SpawnObject( "monster_possessed_tommy", NULL ); + + if ( possessedTommy.IsValid() ) { // Copy player stats to the possessed Tommy + /*if ( IsSpiritOrDeathwalking() ) { //TODO mdl: Remove this if we go with wraiths knocking players back to their body + // This should means we were called by + possessedTommy->SetOrigin( spiritProxy->GetOrigin() ); + possessedTommy->SetAxis( spiritProxy->GetAxis() ); + } else*/ { + possessedTommy->SetOrigin( GetOrigin() ); + possessedTommy->SetAxis( GetAxis() ); + } + possessedTommy->SetPossessedProxy( spiritProxy.GetEntity() ); + } +} + +/* +===================== +hhPlayer::Unpossess + +//HUMANHEAD: cjr +=====================*/ + +void hhPlayer::Unpossess() { + bPossessed = false; + + possessedTommy = NULL; +/* + idDict args; + idEntity *ent; + + playerView.SetViewOverlayMaterial( NULL ); + possessionFOV = 0; // Restore the original FOV + + // Spawn a wraith to fly out of the player's origin + args.Clear(); + args.SetVector( "origin", GetOrigin() + GetEyePosition() + GetAxis()[0] * 50 ); + args.SetMatrix( "rotation", GetAxis() ); +//TODO: Externalize this monster_wraith + args.Set( "classname", "monster_wraith" ); + + gameLocal.SpawnEntityDef( args, &ent ); + ((hhWraith *)ent)->SetEnemy( this ); + + // TODO: Unpossessed by portalling, so kill the wraith +*/ +} + +//============================================================================= +// +// hhPlayer::CanBePossessed +// +// Players can only be possessed if they are are not currently possessed +//============================================================================= + +bool hhPlayer::CanBePossessed( void ) { + if ( godmode || noclip || !bAllowSpirit ) { // Don't possess if in god mode or noclipping + return false; + } + + return !(IsPossessed() || InVehicle() || IsSpiritOrDeathwalking() ); +} + +//============================================================================= +// +// hhPlayer::PossessKilled +// +// Killed when possessed (spirit power ran out) +// +// - Ragdoll the possessed body +// - Return to the location of the possessed tommy and die +//============================================================================= + +void hhPlayer::PossessKilled( void ) { + if ( possessedTommy.IsValid() ) { + possessedTommy->PostEventMS( &EV_Remove, 0 ); + } + + Unpossess(); + StopSpiritWalk(); + Kill( 0, 0 ); // FIXME: Some other way to kill self (instead of suicide?) +} + +//============================================================================= +// +// hhPlayer::Portalled +// +// Player just portalled +//============================================================================= + +void hhPlayer::Portalled( idEntity *portal ) { + if ( talon.IsValid() && talon->GetBindMaster() != this ) { // If Talon isn't bound to Tommy, portal him with the player + talon->Portalled( portal ); + } + + if (gameLocal.isClient) { //HUMANHEAD rww - compensate for angle jump + bBufferNextSnapAngles = true; + smoothedAngles = viewAngles; + } + smoothedFrame = 0; //HUMANHEAD rww - skip smoothing on the portalled frame + walkIK.InvalidateHeights(); //HUMANHEAD rww - don't try to interpolate ik positions between two equal planes on either side of a portal + if (gameLocal.isServer) { //HUMANHEAD rww - send an unreliable event in the coming snapshot + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + msg.Init(msgBuf, sizeof(msgBuf)); + msg.WriteBits(gameLocal.GetSpawnId(portal), 32); + ServerSendEvent(EVENT_PORTALLED, &msg, false, -1, -1, true); //only send it to the client who portalled + } +} + +/* +================ +hhPlayer::UpdateModelTransform +================ +*/ +void hhPlayer::UpdateModelTransform( void ) { + idVec3 origin; + idMat3 axis; + + if( GetPhysicsToVisualTransform(origin, axis) ) { + //HUMANHEAD: aob + GetRenderEntity()->axis = axis; + idVec3 absOrigin = TransformToPlayerSpaceNotInterpolated( origin ); //rww - don't use interpolated origin + GetRenderEntity()->origin = absOrigin; + //HUMANHEAD END + } else { + //HUMANHEAD: aob + GetRenderEntity()->axis = GetAxis(); + GetRenderEntity()->origin = GetOrigin(); + //HUMANHEAD END + } +} + +/* +==================== +hhPlayer::CalcFov + +Fixed fov at intermissions, otherwise account for fov variable and zooms. +Takes possession FOV into account + +HUMANHEAD cjr +==================== +*/ +float hhPlayer::CalcFov( bool honorZoom ) { + float fov; + + // HUMANHEAD mdl: Refactored this to work properly with possessionFOV. Being zoomed in will ignore possessionFOV, however. + idEntity *weaponEnt = weapon.GetEntity(); + if ( possessionFOV > 0.0f && + ! ( weaponEnt && + weaponEnt->IsType( hhWeaponZoomable::Type ) && + reinterpret_cast (weaponEnt)->IsZoomed() ) ) + { + fov = possessionFOV; + } else if ( IsDeathWalking() ) { + fov = spawnArgs.GetFloat("deathwalkFOV", "90"); + } else if ( pm_thirdPerson.GetBool() ) { + fov = g_fov.GetFloat(); + } else if ( InCinematic() ) { + fov = cinematicFOV.GetCurrentValue(gameLocal.time); + } else { + fov = zoomFov.GetCurrentValue(gameLocal.time); + } + + //HUMANHEAD: aob + fov = hhMath::ClampFloat( 1.0f, 179.0f, fov ); + //HUMANHEAD END + + return fov; +} + +/* +=============== +hhPlayer::EnterVehicle +=============== +*/ +void hhPlayer::EnterVehicle( hhVehicle* vehicle ) { + if (!gameLocal.isClient) { //HUMANHEAD PCF rww 05/04/06 - do not do the check on the client, just listen to what the snapshot says. + if (!vehicle->WillAcceptPilot(this)) { + return; + } + } + + // Cancel any pending damage allowing events, since we'll be turning damage off + CancelEvents(&EV_AllowDamage); + + // Set shuttle view + if (entityNumber == gameLocal.localClientNum) { //rww + renderSystem->SetShuttleView( true ); + } + + // Move eye to proper height + SetEyeHeight( vehicle->spawnArgs.GetFloat("pilot_eyeHeight") ); + + // Turn off any illegal behaviors + if( IsSpiritWalking() ) { + StopSpiritWalk(); + } + + LighterOff(); + + // nla - Remove any offset cause by jumping. (Fixes the jumping and getting in the shuttle bug) + SetViewBob( vec3_origin ); + + // CJR: Inform Talon that the player has entered a vehicle + if( talon.IsValid() ) { + talon->OwnerEnteredVehicle(); + } + + ShouldRemainAlignedToAxial( false ); + + cameraInterpolator.SetInterpolationType( IT_None ); + SetOrientation( GetOrigin(), mat3_identity, vehicle->GetAxis()[0], vehicle->GetAxis()[0].ToAngles() ); + //Need to clear untransformedViewAxis so cameraInterpolator doesn't do any unnessacary transforms + SetUntransformedViewAxis( mat3_identity ); + + //Hack + cameraInterpolator.Reset( GetOrigin(), mat3_identity[2], EyeHeightIdeal() ); + + idPlayer::EnterVehicle( vehicle ); +} + +/* +=============== +hhPlayer::ExitVehicle + +This should only be called from vehicle +=============== +*/ +void hhPlayer::ExitVehicle( hhVehicle* vehicle ) { + + // Allow model in player's view again + if (vehicle) { + vehicle->GetRenderEntity()->suppressSurfaceInViewID = 0; + } + + // Set shuttle view off + if (entityNumber == gameLocal.localClientNum) { //rww + renderSystem->SetShuttleView( false ); + } + + ShouldRemainAlignedToAxial( true ); + + // CJR: Inform Talon that the player has left a vehicle + if( talon.IsValid() ) { + talon->OwnerExitedVehicle(); + } + + idPlayer::ExitVehicle( vehicle ); + + buttonMask |= BUTTON_ATTACK_ALT; //HUMANHEAD bjk + + // Reset the animPrefix + animPrefix = spawnArgs.GetString( va( "def_weapon%d", currentWeapon ) ); + animPrefix.Strip( "weaponobj_" ); + + if (vehicle) { + SetOrientation( GetOrigin(), mat3_identity, vehicle->GetAxis()[0], vehicle->GetAxis()[0].ToAngles().Normalize180() ); + } + cameraInterpolator.SetInterpolationType( IT_VariableMidPointSinusoidal ); +} + +// +// ResetClipModel() +// +// HUMANHEAD: aob +// +void hhPlayer::ResetClipModel() { + //Needed for touching triggers. Our bbox is its original size. + SetClipModel(); +} + +/* +=============== +hhPlayer::BecameBound +=============== +*/ +void hhPlayer::BecameBound(hhBindController *b) { + if( !gameLocal.isMultiplayer && weapon.IsValid() ) { //rww - can use weapon while bound in mp + weapon->Hide(); + } +} + +/* +=============== +hhPlayer::BecameUnbound +=============== +*/ +void hhPlayer::BecameUnbound(hhBindController *b) { + if( !gameLocal.isMultiplayer && weapon.IsValid() ) { //rww - can use weapon while bound in mp + weapon->Show(); + } +} + +void hhPlayer::GetLocationText( idStr &locationString ) { + idLocationEntity* locationEntity = gameLocal.LocationForPoint( GetEyePosition() ); + if( locationEntity ) { + locationString = locationEntity->GetLocation(); + } + else { + locationString = common->GetLanguageDict()->GetString( "#str_02911" ); + } +} + +void hhPlayer::UpdateLocation( void ) { + if( hud ) { + hud->SetStateBool("showlocations", developer.GetBool()); + if (developer.GetBool()) { + idStr locationString; + GetLocationText(locationString); + hud->SetStateString( "location", locationString.c_str() ); + hud->SetStateInt( "areanum", gameRenderWorld->PointInArea( GetEyePosition() )); + } + } +} + +/* +=============== +hhPlayer::FillDebugVars +=============== +*/ +void hhPlayer::FillDebugVars(idDict *args, int page) { + idStr text; + + switch(page) { + case 1: + args->SetInt("deathpower", GetDeathWalkPower()); + args->SetInt("spiritpower", GetSpiritPower()); + args->SetBool("AI_CROUCH", AI_CROUCH != 0); + args->SetBool("AI_ONGROUND", AI_ONGROUND != 0); + args->SetBool("AI_JUMP", AI_JUMP != 0); + args->SetBool("AI_SOFTLANDING", AI_SOFTLANDING != 0); + args->SetBool("AI_HARDLANDING", AI_HARDLANDING != 0); + args->SetBool("AI_FORWARD", AI_FORWARD != 0); + args->SetBool("AI_BACKWARD", AI_BACKWARD != 0); + args->SetBool("AI_STRAFE_LEFT", AI_STRAFE_LEFT != 0); + args->SetBool("AI_STRAFE_RIGHT", AI_STRAFE_RIGHT != 0); + args->SetBool("AI_ATTACK_HELD", AI_ATTACK_HELD != 0); + args->SetBool("AI_WEAPON_FIRED", AI_WEAPON_FIRED != 0); + args->SetBool("AI_JUMP", AI_JUMP != 0); + args->SetBool("AI_DEAD", AI_DEAD != 0); + args->SetBool("AI_PAIN", AI_PAIN != 0); + args->SetBool("AI_RELOAD", AI_RELOAD != 0); + args->SetBool("AI_TELEPORT", AI_TELEPORT != 0); + args->SetBool("AI_TURN_LEFT", AI_TURN_LEFT != 0); + args->SetBool("AI_TURN_RIGHT", AI_TURN_RIGHT != 0); + args->SetBool("AI_ASIDE", AI_ASIDE != 0); + args->SetBool("AI_REALLYFALL", AI_REALLYFALL != 0); //HUMANHEAD rww + args->SetBool("HasGroundContacts", physicsObj.HasGroundContacts()); + args->Set("animPrefix", animPrefix.c_str()); + break; + case 2: + args->Set("Physics Axis", GetPhysics()->GetAxis().ToAngles().ToString()); + args->Set("viewAxis", viewAxis.ToAngles().ToString()); + args->Set("cmdAngles", cmdAngles.ToString()); + args->Set("deltaViewAngles", GetDeltaViewAngles().ToString() ); + args->Set("viewAngles", viewAngles.ToString()); + args->Set("untransAngles", GetUntransformedViewAngles().ToString()); + args->Set("xPhysics Axis", GetPhysics()->GetAxis().ToString()); + args->Set("xviewAxis", viewAxis.ToString()); + args->Set("idealLegsYaw", va("%.1f", idealLegsYaw)); + args->SetInt("usercmd.forwardmove", usercmd.forwardmove); + args->SetInt("usercmd.rightmove", usercmd.rightmove); + args->SetInt("legsForward", legsForward); + + switch(physicsObj.GetWaterLevel()) { + case WATERLEVEL_NONE: text = "none"; break; + case WATERLEVEL_FEET: text = "feet"; break; + case WATERLEVEL_WAIST: text = "waist"; break; + case WATERLEVEL_HEAD: text = "head"; break; + } + args->Set("waterlevel", text); + text = collisionModelManager->ContentsName(physicsObj.GetWaterType()); + args->Set("watertype", text); + break; + case 3: + break; + } + idPlayer::FillDebugVars(args, page); +} + +// +// GetAimPosition() +// +idVec3 hhPlayer::GetAimPosition() const { + return GetEyePosition(); +} + +/* +=============== +hhPlayer::WriteToSnapshot +=============== +*/ +void hhPlayer::WriteToSnapshot( idBitMsgDelta &msg ) const { + bool vehControlling = vehicleInterfaceLocal.ControllingVehicle(); + + msg.WriteBits(vehControlling, 1); + + //rww - our stuff is now intermingled with id's for more ideal send ordering + physicsObj.WriteToSnapshot( msg, vehControlling ); + + if (!vehControlling) { + //not syncing these causes denormalization/nan issues, FIXME + msg.WriteFloat(untransformedViewAngles[0]); + msg.WriteFloat(untransformedViewAngles[1]); + msg.WriteFloat(untransformedViewAngles[2]); + idCQuat q = untransformedViewAxis.ToCQuat(); + msg.WriteFloat(q.x); + msg.WriteFloat(q.y); + msg.WriteFloat(q.z); + + //rww - write cameraInterpolator + cameraInterpolator.WriteToSnapshot(msg, this); + + //still need delta angles + //msg.WriteDeltaFloat( 0.0f, deltaViewAngles[0] ); + //msg.WriteDeltaFloat( 0.0f, deltaViewAngles[1] ); + //msg.WriteDeltaFloat( 0.0f, deltaViewAngles[2] ); + } +#if 0 //for debugging differences in snapshot + else { + msg.WriteFloat(0.0f); + msg.WriteFloat(0.0f); + msg.WriteFloat(0.0f); + msg.WriteFloat(0.0f); + msg.WriteFloat(0.0f); + msg.WriteFloat(0.0f); + + msg.WriteFloat(0.0f); + msg.WriteFloat(0.0f); + msg.WriteFloat(0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteFloat(0.0f, 4, 4); + + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteFloat(0.0f, 4, 4); + + msg.WriteFloat(0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteFloat(0.0f, 4, 4); + + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + msg.WriteDeltaFloat(0.0f, 0.0f); + } +#endif + + msg.WriteDeltaFloat( 0.0f, deltaViewAngles[0] ); + msg.WriteDeltaFloat( 0.0f, deltaViewAngles[1] ); + msg.WriteDeltaFloat( 0.0f, deltaViewAngles[2] ); + + msg.WriteShort( health ); + msg.WriteBits( gameLocal.ServerRemapDecl( -1, DECL_ENTITYDEF, lastDamageDef ), gameLocal.entityDefBits ); + msg.WriteDir( lastDamageDir, 9 ); + msg.WriteShort( lastDamageLocation ); + msg.WriteBits( idealWeapon, idMath::BitsForInteger( MAX_WEAPONS ) ); + msg.WriteBits( inventory.weapons, MAX_WEAPONS ); + msg.WriteBits( weapon.GetSpawnId(), 32 ); + msg.WriteBits( spectator, idMath::BitsForInteger( MAX_CLIENTS ) ); + msg.WriteBits( lastHitToggle, 1 ); + msg.WriteBits( weaponGone, 1 ); + WriteBindToSnapshot( msg ); +//===================END OF ID DATA + msg.WriteShort(inventory.maxHealth); //rww - since maxhealth can go above 100 in mp now + + msg.WriteBits( spiritProxy.GetSpawnId(), 32 ); + + //rww - more spiritwalk stuff + msg.WriteBits(lastWeaponSpirit, 32); + msg.WriteBits(bSpiritWalk, 1); + + //not needed anymore + /* + msg.WriteBits( bShowProgressBar, 1 ); + msg.WriteFloat( progressBarValue ); + msg.WriteBits( progressBarState, 2 ); // possible values [0..2] + */ + + //rww - send ammo for current weapon - this is because our weapon routines rely on if we have ammo + //in scripts and so on, and will be predicted wrong for other players (we get our own ammo in the playerstate) + //rwwFIXME: if we have proper weapon switching when you run out of ammo will this actually be necessary? + //i don't think it's all that costly, but still. + if (weapon.IsValid()) { + ammo_t ammoType = weapon->GetAmmoType(); + if (ammoType > 0) { + msg.WriteBits(inventory.ammo[ammoType], ASYNC_PLAYER_INV_AMMO_BITS); + } + else { + msg.WriteBits(0, ASYNC_PLAYER_INV_AMMO_BITS); + } + ammoType = weapon->GetAltAmmoType(); + if (ammoType > 0) { + msg.WriteBits(inventory.ammo[ammoType], ASYNC_PLAYER_INV_AMMO_BITS); + } + else { + msg.WriteBits(0, ASYNC_PLAYER_INV_AMMO_BITS); + } + } + else { + msg.WriteBits(0, ASYNC_PLAYER_INV_AMMO_BITS); + msg.WriteBits(0, ASYNC_PLAYER_INV_AMMO_BITS); + } + + //need to sync buttonMask since we're using it for some state-based things + msg.WriteBits(buttonMask, 8); + + msg.WriteFloat(EyeHeight()); + + //HUMANHEAD PCF rww 05/04/06 - do not sync AI_VEHICLE, it is now based purely on the clientside + //enter/exit of vehicles, with the vehControlling stack bool determining if we should be in a + //vehicle on the client or not. + //msg.WriteBits(AI_VEHICLE, 1); + + //rww - hand stuff + //weaponHandState.WriteToSnapshot(msg); + msg.WriteBits(hand.GetSpawnId(), 32); + msg.WriteBits(handNext.GetSpawnId(), 32); + + //jsh - vehicle stuff + msg.WriteBits( vehicleInterfaceLocal.GetVehicleSpawnId(), 32 ); + msg.WriteBits( vehicleInterfaceLocal.GetHandSpawnId(), 32 ); + //vehicleInterfaceLocal.GetWeaponHandState()->WriteToSnapshot(msg); + + //rww - lighter sync validation + msg.WriteBits((lighterHandle != -1), 1); + + //rww - tractor + msg.WriteBits(fl.isTractored, 1); + + spiritwalkSoundController.WriteToSnapshot(msg); + //deathwalkSoundController.WriteToSnapshot(msg); + wallwalkSoundController.WriteToSnapshot(msg); + + //HUMANHEAD rww - leechgun energy ammo + //note - current weapon's ammo always sent now + //ammo_t energyammo = hhWeaponFireController::GetAmmoType("ammo_energy"); + //msg.WriteBits(inventory.ammo[energyammo], ASYNC_PLAYER_INV_AMMO_BITS); +} + +/* +=============== +hhPlayer::ReadFromSnapshot +=============== +*/ +void hhPlayer::ReadFromSnapshot( const idBitMsgDelta &msg ) { + int i, oldHealth, newIdealWeapon, weaponSpawnId; + bool newHitToggle, stateHitch; + + bool vehControlling = !!msg.ReadBits(1); + + if ( snapshotSequence - lastSnapshotSequence > 1 ) { + stateHitch = true; + } else { + stateHitch = false; + } + lastSnapshotSequence = snapshotSequence; + + oldHealth = health; + + physicsObj.ReadFromSnapshot( msg, vehControlling ); + + if (!vehControlling) { + //not syncing these causes denormalization/nan issues, FIXME + if (bBufferNextSnapAngles) { + idAngles n; + n[0] = msg.ReadFloat(); + n[1] = msg.ReadFloat(); + n[2] = msg.ReadFloat(); + BufferLoggedViewAngles(n); + untransformedViewAngles = n; + bBufferNextSnapAngles = false; + } + else { + untransformedViewAngles[0] = msg.ReadFloat(); + untransformedViewAngles[1] = msg.ReadFloat(); + untransformedViewAngles[2] = msg.ReadFloat(); + } + idCQuat q; + q.x = msg.ReadFloat(); + q.y = msg.ReadFloat(); + q.z = msg.ReadFloat(); + untransformedViewAxis = q.ToMat3(); + + //rww - read cameraInterpolator + cameraInterpolator.ReadFromSnapshot(msg, this); + + //deltaViewAngles[0] = msg.ReadDeltaFloat( 0.0f ); + //deltaViewAngles[1] = msg.ReadDeltaFloat( 0.0f ); + //deltaViewAngles[2] = msg.ReadDeltaFloat( 0.0f ); + } +#if 0 //for debugging differences in snapshot + else { + msg.ReadFloat(); + msg.ReadFloat(); + msg.ReadFloat(); + msg.ReadFloat(); + msg.ReadFloat(); + msg.ReadFloat(); + + msg.ReadFloat(); + msg.ReadFloat(); + msg.ReadFloat(); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadFloat(4, 4); + + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadFloat(4, 4); + + msg.ReadFloat(); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadFloat(4, 4); + + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + msg.ReadDeltaFloat(0.0f); + } +#endif + + deltaViewAngles[0] = msg.ReadDeltaFloat( 0.0f ); + deltaViewAngles[1] = msg.ReadDeltaFloat( 0.0f ); + deltaViewAngles[2] = msg.ReadDeltaFloat( 0.0f ); + + health = msg.ReadShort(); + lastDamageDef = gameLocal.ClientRemapDecl( DECL_ENTITYDEF, msg.ReadBits( gameLocal.entityDefBits ) ); + lastDamageDir = msg.ReadDir( 9 ); + lastDamageLocation = msg.ReadShort(); + newIdealWeapon = msg.ReadBits( idMath::BitsForInteger( MAX_WEAPONS ) ); + inventory.weapons = msg.ReadBits( MAX_WEAPONS ); + weaponSpawnId = msg.ReadBits( 32 ); + spectator = msg.ReadBits( idMath::BitsForInteger( MAX_CLIENTS ) ); + newHitToggle = msg.ReadBits( 1 ) != 0; + weaponGone = msg.ReadBits( 1 ) != 0; + ReadBindFromSnapshot( msg ); + +//===================END OF ID DATA + + inventory.maxHealth = msg.ReadShort(); //rww - since maxhealth can go above 100 in mp now + + idQuat quat; + + /* + if( spiritProxy.SetSpawnId( msg.ReadBits( 32 ) ) ) { + if (spiritProxy.GetEntity() && spiritProxy.GetEntity()->IsType(hhSpiritProxy::Type)) + { + spiritProxy.GetEntity()->ActivateProxy( this, GetOrigin(), GetPhysics()->GetAxis(), viewAxis, viewAngles ); + } + }*/ + spiritProxy.SetSpawnId( msg.ReadBits( 32 ) ); + + //rww - more spiritwalk stuff + lastWeaponSpirit = msg.ReadBits(32); + + bool spiritWalking = !!msg.ReadBits(1); + if (spiritWalking != bSpiritWalk && (weapon.IsValid() || !spiritWalking)) { + bSpiritWalk = spiritWalking; + + LighterOff(); //make sure lighter is off when switching spiritwalk + + if (bSpiritWalk) { + if (gameLocal.localClientNum == entityNumber) { + if (hud) { + hud->HandleNamedEvent("SwitchToEthereal"); + } + gameLocal.SpiritWalkSoundMode( true ); + } + + // Set the player's skin to a glowy effect + SetSkinByName( spawnArgs.GetString("skin_Spiritwalk") ); + SetShaderParm( SHADERPARM_TIMEOFFSET, 1.0f ); // TEMP: cjr - Required by the forcefield material. Can remove when a proper spiritwalk texture is made + + //put bow in proper state + SelectEtherealWeapon(); + } + else { + if (gameLocal.localClientNum == entityNumber) { + if (hud) { + hud->HandleNamedEvent("SwitchFromEthereal"); + } + gameLocal.SpiritWalkSoundMode( false ); + } + + SetSkinByName( NULL ); + } + } + + //not needed anymore + /* + // Handle progress bar + bool bBar = msg.ReadBits(1) != 0; + float value = msg.ReadFloat(); + int state = msg.ReadBits(2); + CL_UpdateProgress(bBar, value, state); + */ + + // if not a local client assume the client has all ammo types + if ( entityNumber != gameLocal.localClientNum ) { + for( i = 0; i < AMMO_NUMTYPES; i++ ) { + inventory.ammo[ i ] = 999; + } + } + + //rww - send ammo for current weapon - this is because our weapon routines rely on if we have ammo + //in scripts and so on, and will be predicted wrong for other players (we get our own ammo in the playerstate) + //rwwFIXME: if we have proper weapon switching when you run out of ammo will this actually be necessary? + //i don't think it's all that costly, but still. + int primAmmo = msg.ReadBits(ASYNC_PLAYER_INV_AMMO_BITS); + int altAmmo = msg.ReadBits(ASYNC_PLAYER_INV_AMMO_BITS); + if (entityNumber != gameLocal.localClientNum) { + //since we have our own ammo in the playerstate, don't want to stomp it + if (weapon.IsValid()) { + ammo_t ammoType = weapon->GetAmmoType(); + if (ammoType > 0) { + inventory.ammo[ammoType] = primAmmo; + } + ammoType = weapon->GetAltAmmoType(); + if (ammoType > 0) { + inventory.ammo[ammoType] = altAmmo; + } + } + } + + //need to sync buttonMask since we're using it for some state-based things + buttonMask = msg.ReadBits(8); + + SetEyeHeight(msg.ReadFloat()); + + //HUMANHEAD PCF rww 05/04/06 - do not sync AI_VEHICLE, it is now based purely on the clientside + //enter/exit of vehicles, with the vehControlling stack bool determining if we should be in a + //vehicle on the client or not. + //AI_VEHICLE = !!msg.ReadBits(1); + + //rww - hand stuff + //weaponHandState.ReadFromSnapshot(msg); + hand.SetSpawnId(msg.ReadBits(32)); + handNext.SetSpawnId(msg.ReadBits(32)); + + //HUMANHEAD PCF rww 05/04/06 - base this check on AI_VEHICLE and make sure the player is exited, + //even if the vehicle is not in the snapshot at this point. + if (!vehControlling && AI_VEHICLE) { //then exit on the client + if (vehicleInterfaceLocal.ControllingVehicle()) { + hhVehicle *veh = GetVehicleInterface()->GetVehicle(); + if (veh && veh->GetPilot() == this) { + veh->EjectPilot(); + } + } + ExitVehicle(NULL); //be extra safe in case vehicle is no longer around + } + + //jsh - vehicle stuff + int vehSpawnId = msg.ReadBits( 32 ); + //HUMANHEAD PCF rww 05/04/06 - base this check ON AI_VEHICLE, in case snapshot where controlling and + //where the vehicle is set do not exactly coincide. + vehicleInterfaceLocal.SetVehicleSpawnId( vehSpawnId ); + if( !AI_VEHICLE ) { + if (vehControlling) { + hhVehicle *veh = vehicleInterfaceLocal.GetVehicle(); + if (veh && veh->IsType(hhVehicle::Type)) { + //GetVehicleInterface()->TakeControl( vehicleInterfaceLocal.GetVehicle(), this ); + EnterVehicle( vehicleInterfaceLocal.GetVehicle() ); + } + } + } + + /* + if( vehicleInterfaceLocal.SetHandSpawnId( msg.ReadBits( 32 ) ) ) { + vehicleInterfaceLocal.GetHandEntity()->AttachHand( this, true ); + } + */ + vehicleInterfaceLocal.SetHandSpawnId( msg.ReadBits( 32 ) ); + //vehicleInterfaceLocal.GetWeaponHandState()->ReadFromSnapshot(msg); + + //rww - lighter sync validation + bool newLighterOn = !!msg.ReadBits(1); + if (newLighterOn != IsLighterOn()) { + ToggleLighter(); + } + + //rww - tractor + fl.isTractored = !!msg.ReadBits(1); + + spiritwalkSoundController.ReadFromSnapshot(msg); + //deathwalkSoundController.ReadFromSnapshot(msg); + wallwalkSoundController.ReadFromSnapshot(msg); + + //=========================================== start id code + if ( weapon.SetSpawnId( weaponSpawnId ) ) { + //HUMANHEAD rww - i am getting crashes here and the stack is all messed up, + //claiming that SetOwner is off into null, when supposedly the entity is not. + //rearranging this code so it's easier to see what's going on in a crash. + hhWeapon *weapEnt = weapon.GetEntity(); + if ( weapEnt ) { + // maintain ownership locally + weapEnt->SetOwner( this ); + } + currentWeapon = -1; + } + + //HUMANHEAD rww - but we do want ammo count for the leechgun on other players for prediction + //note - current weapon's ammo always sent now + //ammo_t energyammo = hhWeaponFireController::GetAmmoType("ammo_energy"); + //inventory.ammo[energyammo] = msg.ReadBits(ASYNC_PLAYER_INV_AMMO_BITS); + + if ( oldHealth > 0 && health <= 0 ) { + if ( stateHitch ) { + // so we just hide and don't show a death skin + UpdateDeathSkin( true ); + } + // die + AI_DEAD = true; + SetAnimState( ANIMCHANNEL_LEGS, "Legs_Death", 4 ); + SetAnimState( ANIMCHANNEL_TORSO, "Torso_Death", 4 ); + SetWaitState( "" ); + animator.ClearAllJoints(); + + //HUMANHEAD rww - don't want this + /* + if ( entityNumber == gameLocal.localClientNum ) { + playerView.Fade( colorBlack, 12000 ); + } + */ + //HUMANHEAD END + + fl.clientEvents = true; //client event hackery for non-critical cosmetic cleanups + PostEventMS(&EV_RespawnCleanup, 32); + fl.clientEvents = false; + StartRagdoll(); + physicsObj.SetMovementType( PM_DEAD ); + if ( !stateHitch ) { + //HUMANHEAD PCF rww 09/15/06 - female mp sounds + if (IsFemale()) { + StartSound( "snd_death_female", SND_CHANNEL_VOICE, 0, false, NULL ); + } + else { + //HUMANHEAD END + StartSound( "snd_death", SND_CHANNEL_VOICE, 0, false, NULL ); + } + } + if ( weapon.GetEntity() ) { + weapon.GetEntity()->OwnerDied(); + } + + //HUMANHEAD rww + GetPhysics()->SetContents(0); + Hide(); + + //so camera can operate on client. + minRespawnTime = gameLocal.time + RAGDOLL_DEATH_TIME; + maxRespawnTime = minRespawnTime + 10000; + //HUMANHEAD END + } else if ( oldHealth <= 0 && health > 0 ) { + // respawn + Init(); + StopRagdoll(); + SetPhysics( &physicsObj ); + physicsObj.EnableClip(); + SetCombatContents( true ); + //HUMANHEAD rww + if (!spectating) { + Show(); + } + //HUMANHEAD END + } else if ( health < oldHealth && health > 0 ) { + if ( stateHitch ) { + lastDmgTime = gameLocal.time; + } else { + // damage feedback + const idDeclEntityDef *def = static_cast( declManager->DeclByIndex( DECL_ENTITYDEF, lastDamageDef, false ) ); + if ( def ) { + playerView.DamageImpulse( lastDamageDir * viewAxis.Transpose(), &def->dict ); + AI_PAIN = Pain( NULL, NULL, oldHealth - health, lastDamageDir, lastDamageLocation ); + lastDmgTime = gameLocal.time; + } else { + common->Warning( "NET: no damage def for damage feedback '%s'\n", lastDamageDef ); + } + } + } + + // If the player is alive, restore proper physics object + if ( health > 0 && IsActiveAF() ) { + StopRagdoll(); + SetPhysics( &physicsObj ); + physicsObj.EnableClip(); + SetCombatContents( true ); + } + + if ( idealWeapon != newIdealWeapon ) { + if ( stateHitch ) { + weaponCatchup = true; + } + idealWeapon = newIdealWeapon; + UpdateHudWeapon(); + } + + if ( lastHitToggle != newHitToggle ) { + SetLastHitTime( gameLocal.realClientTime ); + } + //=========================================== end id code + + if ( msg.HasChanged() ) { + UpdateVisuals(); + } +} + +/* +================ +hhPlayer::ServerReceiveEvent +================ +*/ +bool hhPlayer::ServerReceiveEvent( int event, int time, const idBitMsg &msg ) { + if ( idEntity::ServerReceiveEvent( event, time, msg ) ) { + return true; + } + + // client->server events + switch( event ) { + case EVENT_IMPULSE: { + int impulse = msg.ReadBits( 6 ); + PerformImpulse(impulse); + if (vehicleInterfaceLocal.ControllingVehicle()) { + hhVehicle *veh = vehicleInterfaceLocal.GetVehicle(); + if (veh) { + veh->DoPlayerImpulse(impulse); + } + } + return true; + } + default: { + return false; + } + } +} + +/* +================ +hhPlayer::WritePlayerStateToSnapshot +================ +*/ +void hhPlayer::WritePlayerStateToSnapshot( idBitMsgDelta &msg ) const { + idPlayer::WritePlayerStateToSnapshot(msg); + + //rww - extra playerView stuff + playerView.WriteToSnapshot(msg); + + //rww - scope view + msg.WriteBits(bScopeView, 1); + + //rww - zoom fov + msg.WriteFloat(zoomFov.GetDuration()); + msg.WriteFloat(zoomFov.GetEndValue()); + msg.WriteFloat(zoomFov.GetStartTime()); + msg.WriteFloat(zoomFov.GetStartValue()); + + //rww - view angle sensitivity + msg.WriteFloat(GetViewAnglesSensitivity()); +} + +/* +================ +hhPlayer::ReadPlayerStateFromSnapshot +================ +*/ +void hhPlayer::ReadPlayerStateFromSnapshot( const idBitMsgDelta &msg ) { + idPlayer::ReadPlayerStateFromSnapshot(msg); + + //rww - extra playerView stuff + playerView.ReadFromSnapshot(msg); + + //rww - scope and fov handling + bool canOverrideView = true; + if (weapon.IsValid() && weapon->IsType(hhWeaponZoomable::Type)) { //if we have a zoomed weapon, don't override with snapshot while the prediction fudge timer is on + hhWeaponZoomable *weap = static_cast(weapon.GetEntity()); + if (weap->clientZoomTime >= gameLocal.time) { + canOverrideView = false; + } + } + + bool scopeView = !!msg.ReadBits(1); + float zfDur = msg.ReadFloat(); + float zfEnd = msg.ReadFloat(); + float zfStT = msg.ReadFloat(); + float zfStV = msg.ReadFloat(); + + renderSystem->SetShuttleView( InVehicle() ); + + if (canOverrideView) { //if we are currently allowed to override with snapshot values, do so. + bScopeView = scopeView; + if (bScopeView != renderSystem->IsScopeView()) { + renderSystem->SetScopeView(bScopeView); + } + zoomFov.SetDuration(zfDur); + zoomFov.SetEndValue(zfEnd); + zoomFov.SetStartTime(zfStT); + zoomFov.SetStartValue(zfStV); + } + + //rww - view angle sensitivity + SetViewAnglesSensitivity(msg.ReadFloat()); +} + +/* +================ +hhPlayer::ClientPredictionThink +================ +*/ +void hhPlayer::ClientPredictionThink( void ) { + if (gameLocal.localClientNum != entityNumber && forcePredictionButtons) { + //used by some weapons which rely on prediction-based projectiles, to ensure we don't miss a launch + gameLocal.usercmds[ entityNumber ].buttons |= forcePredictionButtons; + if (gameLocal.isNewFrame) { + forcePredictionButtons = 0; + } + } + + idPlayer::ClientPredictionThink(); + + if (InVehicle()) { + UpdateHud( GetVehicleInterfaceLocal()->GetHUD() ); + } + else { + UpdateHud( hud ); + } +} + +/* +================ +hhPlayer::GetPhysicsToVisualTransform +================ +*/ +bool hhPlayer::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + if ( af.IsActive() ) { + af.GetPhysicsToVisualTransform( origin, axis ); + return true; + } + + //rww - adopted id's smoothing code + if ( gameLocal.isClient && !bindMaster && gameLocal.framenum >= smoothedFrame && ( entityNumber != gameLocal.localClientNum || selfSmooth ) ) { + if (!smoothedOriginUpdated) { + idVec3 renderOrigin = TransformToPlayerSpace(modelOffset); + idVec3 originalOrigin = renderOrigin; + + //id's cheesy smooth code (changed to vec3 because there is no "down" in prey) + idVec3 originDiff = renderOrigin - smoothedOrigin; + if (smoothedFrame == 0) { //for teleporting + originDiff = vec3_origin; + } + smoothedOrigin = renderOrigin; + if ( originDiff.LengthSqr() < Square( 100.0f ) ) { + // smoothen by pushing back to the previous position + if ( selfSmooth ) { + assert( entityNumber == gameLocal.localClientNum ); + renderOrigin -= net_clientSelfSmoothing.GetFloat() * originDiff; + } else { + renderOrigin -= gameLocal.clientSmoothing * originDiff; + } + + //get rid of the smoothing on the "vertical" axis + idVec3 a(renderOrigin.x*viewAxis[2].x, renderOrigin.y*viewAxis[2].y, renderOrigin.z*viewAxis[2].z); + idVec3 b(originalOrigin.x*viewAxis[2].x, originalOrigin.y*viewAxis[2].y, originalOrigin.z*viewAxis[2].z); + renderOrigin -= (a-b); + } + + smoothedFrame = gameLocal.framenum; + smoothedOriginUpdated = true; + } + + axis = viewAxis; + origin = ( smoothedOrigin - GetPhysics()->GetOrigin() ) * GetEyeAxis().Transpose(); + return true; + } + + if (bClampYaw) { //rww - lock model angles + idVec3 masterOrigin; + idMat3 masterAxis; + + if (GetMasterPosition(masterOrigin, masterAxis)) { + axis = masterAxis; + origin = modelOffset; + + return true; + } + } + + axis = viewAxis; + origin = modelOffset; + + return true; +} + +bool hhPlayer::UpdateAnimationControllers( void ) { + bool retValue = idPlayer::UpdateAnimationControllers(); + + hhAnimator *theAnimator; + if (head.IsValid()) { + theAnimator = head->GetAnimator(); + } + else { + theAnimator = GetAnimator(); + } + JawFlap(theAnimator); + + return retValue; +} + +/* +=============== +hhPlayer::Event_PlayWeaponAnim +=============== +*/ +void hhPlayer::Event_PlayWeaponAnim( const char* animName, int numTries ) { + //AOB: I would like a better solution then constantly banging until weapon is valid. + if( (!weapon.IsValid() || GetCurrentWeapon() != idealWeapon) && numTries > 0 ) { + CancelEvents( &EV_PlayWeaponAnim ); + PostEventMS( &EV_PlayWeaponAnim, 50, animName, numTries - 1 ); + return; + } + + CancelEvents( &EV_PlayWeaponAnim ); + if( weapon.IsValid() ) { + weapon->ProcessEvent( &EV_PlayAnimWhenReady, animName ); + } +} + +void hhPlayer::DialogStart(bool bDisallowPlayerDeath, bool bVoiceDucking, bool bLowerWeapon) { + bDialogDamageMode = bDisallowPlayerDeath; + bDialogWeaponMode = bLowerWeapon; + gameLocal.DialogSoundMode(bVoiceDucking); + if (bDialogWeaponMode) { // Lock weapon + preCinematicWeaponFlags = weaponFlags; + preCinematicWeapon = GetIdealWeapon(); + LockWeapon(-1); + } +} + +void hhPlayer::DialogStop() { + bDialogDamageMode = false; + gameLocal.DialogSoundMode(false); + if (bDialogWeaponMode) { + weaponFlags = preCinematicWeaponFlags; + SelectWeapon(preCinematicWeapon, true); + bDialogWeaponMode = false; + } +} + +/* +=============== +hhPlayer::Event_DialogStart + HUMANHEAD pdm +=============== +*/ +void hhPlayer::Event_DialogStart( int bDisallowPlayerDeath, int bVoiceDucking, int bLowerWeapon ) { + DialogStart(bDisallowPlayerDeath != 0, bVoiceDucking != 0, bLowerWeapon != 0); +} + +/* +=============== +hhPlayer::Event_DialogStop + HUMANHEAD pdm +=============== +*/ +void hhPlayer::Event_DialogStop() { + DialogStop(); +} + +/* +=============== +hhPlayer::Event_LotaTunnelMode + HUMANHEAD pdm +=============== +*/ +void hhPlayer::Event_LotaTunnelMode(bool on) { + bLotaTunnelMode = on; +} + +/* +=============== +hhPlayer::Event_Cinematic + HUMANHEAD pdm +=============== +*/ +void hhPlayer::Event_Cinematic( int on, int lockView ) { + bool cinematic = (on != 0); + InCinematic( cinematic ); + playerView.SetLetterBox(cinematic); + if (cinematic) { + if ( IsSpiritOrDeathwalking() ) { + StopSpiritWalk(); + } + if ( IsPossessed() ) { + Unpossess(); + } + + // Lock weapon + preCinematicWeaponFlags = weaponFlags; + preCinematicWeapon = GetCurrentWeapon(); + LockWeapon(-1); + + // disable damage + fl.takedamage = false; + this->lockView = (lockView != 0); + } + else { + fl.takedamage = true; + this->lockView = false; + + //UnlockWeapon(preCinematicWeapon); + weaponFlags = preCinematicWeaponFlags; + //SetCurrentWeapon(preCinematicWeapon); + SelectWeapon(preCinematicWeapon, true); + } +} + +//============================================================================= +// +// hhPlayer::Event_PrepareToResurrect +// +// Flashes the screen / fades the FOV +//============================================================================= + +void hhPlayer::Event_PrepareToResurrect() { + deathWalkFlash = 0; + possessionFOV = 90.0f; + PostEventSec( &EV_ResurrectScreenFade, 0 ); +} + +//============================================================================= +// +// hhPlayer::Event_ResurrectScreenFade +// +// The actual code to incrementally flash the screen +//============================================================================= + +void hhPlayer::Event_ResurrectScreenFade() { + deathWalkFlash += spawnArgs.GetFloat( "resurrectFlashChange", "0.04" ); + if ( deathWalkFlash >= 1.0f ) { + deathWalkFlash = 1.0f; + PostEventSec( &EV_Resurrect, 0.0f ); // Actually send player back to the physical realm + } else { + PostEventSec( &EV_ResurrectScreenFade, 0.02f ); + } + + possessionFOV += spawnArgs.GetFloat( "resurrectFOVChange", "1.0" ); + playerView.SetViewOverlayColor( idVec4( 1.0f, 1.0f, 1.0f, deathWalkFlash ) ); +} + +//============================================================================= +// +// hhPlayer::Event_Resurrect +// +//============================================================================= +void hhPlayer::Event_Resurrect() { + Resurrect(); +} + +//============================================================================= +// +// hhPlayer::Event_Event_ShouldRemainAlignedToAxial +// +//============================================================================= +void hhPlayer::Event_ShouldRemainAlignedToAxial( bool remainAligned ) { + ShouldRemainAlignedToAxial( remainAligned ); +} + +//============================================================================= +// +// hhPlayer::Event_OrientToGravity +// +//============================================================================= +void hhPlayer::Event_OrientToGravity( bool orient ) { + OrientToGravity( orient ); +} + +//============================================================================= +// +// hhPlayer::Event_ResetGravity +// HUMANHEAD: pdm: Posted when entity is leaving a gravity zone +//============================================================================= +void hhPlayer::Event_ResetGravity() { + if( IsWallWalking() ) { + return; // Don't reset if wallwalking + } + + if (spectating) { //HUMANHEAD rww - don't reset if spectating + return; + } + + idPlayer::Event_ResetGravity(); + + OrientToGravity( true ); // let it reset orientation +} + +//============================================================================= +// +// hhPlayer::Event_SetOverlayMaterial +// +//============================================================================= +void hhPlayer::Event_SetOverlayMaterial( const char *mtrName, const int requiresScratch ) { + if ( mtrName && mtrName[0] ) { + playerView.SetViewOverlayMaterial( declManager->FindMaterial(mtrName), requiresScratch ); + } else { + playerView.SetViewOverlayMaterial( NULL ); + } +} + +//============================================================================= +// +// hhPlayer::Event_SetOverlayTime +// +//============================================================================= +void hhPlayer::Event_SetOverlayTime( const float newTime, const int requiresScratch ) { + playerView.SetViewOverlayTime( newTime, requiresScratch ); +} + +//============================================================================= +// +// hhPlayer::Event_SetOverlayColor +// +//============================================================================= +void hhPlayer::Event_SetOverlayColor( const float r, const float g, const float b, const float a ) { + idVec4 color; + + color.x = r; + color.y = g; + color.z = b; + color.w = a; + + playerView.SetViewOverlayColor( color ); +} + +//============================================================================= +// +// hhPlayer::Event_DDAHeartBeat +// +//============================================================================= + +void hhPlayer::Event_DDAHeartBeat() { + gameLocal.GetDDA()->DDA_Heartbeat( this ); + PostEventMS( &EV_DDAHeartbeat, ddaHeartbeatMS ); +} + +//================ +//hhPlayer::Save +//================ +void hhPlayer::Save( idSaveGame *savefile ) const { + // Public vars + savefile->Write(weaponInfo, sizeof(weaponInfo_t)*15); + savefile->Write(altWeaponInfo, sizeof(weaponInfo_t)*15); + savefile->WriteFloat( lighterTemperature ); + savefile->WriteRenderLight( lighter ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->WriteInt( lighterHandle ); + spiritProxy.Save( savefile ); + savefile->WriteInt( lastWeaponSpirit ); + talon.Save( savefile ); + savefile->WriteInt( nextTalonAttackCommentTime ); + savefile->WriteBool( bTalonAttackComment ); + savefile->WriteBool( bSpiritWalk ); + savefile->WriteBool( bDeathWalk ); + savefile->WriteBool( bReallyDead ); + guiWantsControls.Save( savefile ); + savefile->WriteFloat( deathWalkFlash ); + savefile->WriteInt( deathWalkTime ); + savefile->WriteInt( deathWalkPower ); + savefile->WriteBool( bInDeathwalkTransition ); + savefile->WriteBool( bInCinematic ); + savefile->WriteBool( bPlayingLowHealthSound ); + savefile->WriteBool( bPossessed ); + savefile->WriteFloat( possessionTimer ); + savefile->WriteFloat( possessionFOV ); + savefile->WriteInt( preCinematicWeapon ); + savefile->WriteInt( preCinematicWeaponFlags ); + savefile->WriteInt( lastDamagedTime ); + savefile->WriteStaticObject( weaponHandState ); + hand.Save( savefile ); + handNext.Save( savefile ); + possessedTommy.Save( savefile ); + + savefile->WriteStaticObject( vehicleInterfaceLocal ); + savefile->WriteObject( vehicleInterfaceLocal.GetVehicle() ); + + deathLookAtEntity.Save( savefile ); + savefile->WriteString( deathLookAtBone ); + savefile->WriteString( deathCameraBone ); + savefile->WriteStaticObject( spiritwalkSoundController ); + savefile->WriteStaticObject( deathwalkSoundController ); + savefile->WriteStaticObject( wallwalkSoundController ); + savefile->WriteBool( bShowProgressBar ); + savefile->WriteFloat( progressBarValue ); + + savefile->WriteFloat( progressBarGuiValue.GetStartTime() ); // idInterpolate + savefile->WriteFloat( progressBarGuiValue.GetDuration() ); + savefile->WriteFloat( progressBarGuiValue.GetStartValue() ); + savefile->WriteFloat( progressBarGuiValue.GetEndValue() ); + + savefile->WriteInt( progressBarState ); + savefile->WriteBool( bClampYaw ); + savefile->WriteFloat( maxRelativeYaw ); + savefile->WriteFloat( maxRelativePitch ); + savefile->WriteFloat( bob ); + savefile->WriteInt( lastAppliedBobCycle ); + savefile->WriteInt( prevStepUpTime ); + savefile->WriteVec3( prevStepUpOrigin ); + savefile->WriteFloat( crashlandSpeed_fatal ); + savefile->WriteFloat( crashlandSpeed_soft ); + savefile->WriteFloat( crashlandSpeed_jump ); + // Protected vars + savefile->WriteUserInterface( guiOverlay, false ); + thirdPersonCameraClipBounds.Save( savefile ); + savefile->WriteFloat( viewAnglesSensitivity ); + savefile->WriteInt( lastResurrectTime ); + savefile->WriteInt( spiritDrainHeartbeatMS ); + savefile->WriteInt( ddaHeartbeatMS ); + + savefile->WriteInt( spiritWalkToggleTime ); + savefile->WriteBool( bDeathWalkStage2 ); + savefile->WriteBool( bFrozen ); + savefile->WriteAngles( untransformedViewAngles ); + savefile->WriteMat3( untransformedViewAxis ); + savefile->WriteInt( nextSpiritTime ); + + savefile->WriteFloat( cinematicFOV.GetStartTime() ); + savefile->WriteFloat( cinematicFOV.GetAcceleration() ); + savefile->WriteFloat( cinematicFOV.GetDeceleration() ); + savefile->WriteFloat( cinematicFOV.GetDuration() ); + savefile->WriteFloat( cinematicFOV.GetStartValue() ); + savefile->WriteFloat( cinematicFOV.GetEndValue() ); + savefile->WriteBool( bAllowSpirit ); + savefile->WriteInt( airAttackerTime ); + savefile->WriteBool( bScopeView ); + savefile->WriteInt( ddaNumEnemies ); + savefile->WriteFloat( ddaProbabilityAccum ); + savefile->WriteInt( weaponFlags ); + savefile->WriteBool( lockView ); + + savefile->WriteBool( bCollidingWithPortal ); + savefile->WriteBool( bLotaTunnelMode ); + savefile->WriteInt( forcePredictionButtons ); + + for (int ix=0; ixWriteInt( lastAttackers[ix].time ); + savefile->WriteBool( lastAttackers[ix].displayed ); + } + + if ( InVehicle() ) { + savefile->WriteMat3( vehicleInterfaceLocal.GetVehicle()->GetPhysics()->GetAxis() ); + savefile->WriteVec3( vehicleInterfaceLocal.GetVehicle()->GetAxis()[0] ); + } + + //HUMANHEAD PCF mdl 04/28/06 - Moved camera interpolater down here to fix jump off wallwalk view angle problem + cameraInterpolator.Save( savefile ); + //HUMANHEAD PCF mdl 05/04/06 - Save whether the light handle is active + savefile->WriteBool( IsLighterOn() ); +} + +//================ +//hhPlayer::Restore +//================ +void hhPlayer::Restore( idRestoreGame *savefile ) { + // Public vars + savefile->Read(weaponInfo, sizeof(weaponInfo_t)*15); + savefile->Read(altWeaponInfo, sizeof(weaponInfo_t)*15); + savefile->ReadFloat( lighterTemperature ); + savefile->ReadRenderLight( lighter ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->ReadInt( lighterHandle ); + spiritProxy.Restore( savefile ); + savefile->ReadInt( lastWeaponSpirit ); + talon.Restore( savefile ); + savefile->ReadInt( nextTalonAttackCommentTime ); + savefile->ReadBool( bTalonAttackComment ); + savefile->ReadBool( bSpiritWalk ); + savefile->ReadBool( bDeathWalk ); + savefile->ReadBool( bReallyDead ); + guiWantsControls.Restore( savefile ); + savefile->ReadFloat( deathWalkFlash ); + savefile->ReadInt( deathWalkTime ); + savefile->ReadInt( deathWalkPower ); + savefile->ReadBool( bInDeathwalkTransition ); + savefile->ReadBool( bInCinematic ); + savefile->ReadBool( bPlayingLowHealthSound ); + savefile->ReadBool( bPossessed ); + savefile->ReadFloat( possessionTimer ); + savefile->ReadFloat( possessionFOV ); + savefile->ReadInt( preCinematicWeapon ); + savefile->ReadInt( preCinematicWeaponFlags ); + savefile->ReadInt( lastDamagedTime ); + savefile->ReadStaticObject( weaponHandState ); + hand.Restore( savefile ); + handNext.Restore( savefile ); + possessedTommy.Restore( savefile ); + + savefile->ReadStaticObject( vehicleInterfaceLocal ); + SetVehicleInterface( &vehicleInterfaceLocal ); + + hhVehicle *vehicle; + savefile->ReadObject( reinterpret_cast ( vehicle ) ); + if( vehicle ) { + vehicle->RestorePilot( &vehicleInterfaceLocal ); + } + + deathLookAtEntity.Restore( savefile ); + savefile->ReadString( deathLookAtBone ); + savefile->ReadString( deathCameraBone ); + savefile->ReadStaticObject( spiritwalkSoundController ); + savefile->ReadStaticObject( deathwalkSoundController ); + savefile->ReadStaticObject( wallwalkSoundController ); + savefile->ReadBool( bShowProgressBar ); + savefile->ReadFloat( progressBarValue ); + + float set; + savefile->ReadFloat( set ); // idInterpolate + progressBarGuiValue.SetStartTime( set ); + savefile->ReadFloat( set ); + progressBarGuiValue.SetDuration( set ); + savefile->ReadFloat( set ); + progressBarGuiValue.SetStartValue(set); + savefile->ReadFloat( set ); + progressBarGuiValue.SetEndValue( set ); + + savefile->ReadInt( progressBarState ); + savefile->ReadBool( bClampYaw ); + savefile->ReadFloat( maxRelativeYaw ); + savefile->ReadFloat( maxRelativePitch ); + savefile->ReadFloat( bob ); + savefile->ReadInt( lastAppliedBobCycle ); + savefile->ReadInt( prevStepUpTime ); + savefile->ReadVec3( prevStepUpOrigin ); + savefile->ReadFloat( crashlandSpeed_fatal ); + savefile->ReadFloat( crashlandSpeed_soft ); + savefile->ReadFloat( crashlandSpeed_jump ); + // Protected vars + savefile->ReadUserInterface( guiOverlay ); + + thirdPersonCameraClipBounds.Restore( savefile ); + savefile->ReadFloat( viewAnglesSensitivity ); + savefile->ReadInt( lastResurrectTime ); + savefile->ReadInt( spiritDrainHeartbeatMS ); + savefile->ReadInt( ddaHeartbeatMS ); + + savefile->ReadInt( spiritWalkToggleTime ); + savefile->ReadBool( bDeathWalkStage2 ); + savefile->ReadBool( bFrozen ); + savefile->ReadAngles( untransformedViewAngles ); + savefile->ReadMat3( untransformedViewAxis ); + savefile->ReadInt( nextSpiritTime ); + + float startTime, accelTime, decelTime, duration, startPos, endPos; + + savefile->ReadFloat( startTime ); + savefile->ReadFloat( accelTime ); + savefile->ReadFloat( decelTime ); + savefile->ReadFloat( duration ); + savefile->ReadFloat( startPos ); + savefile->ReadFloat( endPos ); + cinematicFOV.Init( startTime, accelTime, decelTime, duration, startPos, endPos ); + savefile->ReadBool( bAllowSpirit ); + savefile->ReadInt( airAttackerTime ); + savefile->ReadBool( bScopeView ); + savefile->ReadInt( ddaNumEnemies ); + savefile->ReadFloat( ddaProbabilityAccum ); + savefile->ReadInt( weaponFlags ); + savefile->ReadBool( lockView ); + + savefile->ReadBool( bCollidingWithPortal ); + savefile->ReadBool( bLotaTunnelMode ); + savefile->ReadInt( forcePredictionButtons ); + + for (int ix=0; ixReadInt( lastAttackers[ix].time ); + savefile->ReadBool( lastAttackers[ix].displayed ); + } + + // We don't want to preserve these + memset( &oldCmdAngles, 0, sizeof( oldCmdAngles ) ); + + kickSpring = spawnArgs.GetFloat( "kickSpring" ); //HUMANHEAD bjk + kickDamping = spawnArgs.GetFloat( "kickDamping" ); //HUMANHEAD bjk + + if( InVehicle() ) { + hhVehicle *vehicle = vehicleInterfaceLocal.GetVehicle(); + HH_ASSERT( vehicle ); + + idMat3 axis; + idVec3 axis0; + + // This prevents a crashbug if the vehicle's physics doesn't exist yet. + savefile->ReadMat3( axis ); // vehicle->GetPhysics()->GetAxis() + savefile->ReadVec3( axis0 ); // vehicle->GetAxis()[0] + + // Keep orientation correct in vehicles + RestoreOrientation( GetOrigin(), axis, axis0, axis0.ToAngles() ); + + //HUMANHEAD PCF mdl 05/02/06 - Added this to re-enable shuttle view + if (renderSystem) { + // Set shuttle view + renderSystem->SetShuttleView( true ); + } + } else { + // Keep orientation correct for walkwalk and gravity rooms + RestoreOrientation( GetOrigin(), physicsObj.GetAxis(), viewAngles.ToMat3()[0], untransformedViewAngles ); + } + + //HUMANHEAD PCF mdl 04/28/06 - Moved camera interpolater down here to fix jump off wallwalk view angle problem + cameraInterpolator.Restore( savefile ); + + //HUMANHEAD PCF mdl 05/04/06 - Restore lighter + bool bLighter; + savefile->ReadBool( bLighter ); + if ( bLighter ) { + lighterHandle = gameRenderWorld->AddLightDef( &lighter ); + } +} + +int hhPlayer::GetSpiritPower() { + ammo_t ammo_spiritpower = idWeapon::GetAmmoNumForName("ammo_spiritpower"); + return inventory.ammo[ammo_spiritpower]; +} + +void hhPlayer::SetSpiritPower(int amount) { + ammo_t ammo_spiritpower = idWeapon::GetAmmoNumForName("ammo_spiritpower"); + inventory.ammo[ammo_spiritpower] = amount; + if (inventory.ammo[ammo_spiritpower] > inventory.maxSpirit) { + inventory.ammo[ammo_spiritpower] = inventory.maxSpirit; + } +} + +void hhPlayer::Event_StartHUDTranslation() { + vehicleInterfaceLocal.StartHUDTranslation(); +} + +void hhPlayer::Freeze(float unfreezeDelay) { + bFrozen = true; + StopFiring(); + if ( weapon.IsValid() ) { + weapon->PutAway(); + } + if ( unfreezeDelay > 0.0f ) { + PostEventSec( &EV_Unfreeze, unfreezeDelay ); + } +} + +void hhPlayer::Unfreeze(void) { + bFrozen = false; + if ( weapon.IsValid() ) { + weapon->PostEventMS( &EV_Show, 1000 ); + weapon->PostEventMS( &EV_Weapon_WeaponRising, 1000 ); + } +} + +void hhPlayer::Event_Unfreeze() { + Unfreeze(); + if ( bindMaster && bindMaster->RespondsTo( EV_BindUnfroze ) ) { + GetBindMaster()->PostEventMS( &EV_BindUnfroze, 0, this ); + } +} + +void hhPlayer::Event_GetSpiritPower() { //rww + idThread::ReturnFloat((float)GetSpiritPower()); +} + +void hhPlayer::Event_SetSpiritPower(const float s) { //rww + SetSpiritPower((int)s); +} + +void hhPlayer::Event_OnGround() { // bg + idThread::ReturnFloat( physicsObj.HasGroundContacts() ); +} + +void hhPlayer::SetupWeaponFlags( void ) { + const char *mapName = gameLocal.serverInfo.GetString( "si_map" ); + const idDecl *mapDecl = declManager->FindType(DECL_MAPDEF, mapName, false ); + if ( !mapDecl ) { + weaponFlags = -1; // No map decls? Allow all weapons + return; + } + const idDeclEntityDef *mapInfo = static_cast(mapDecl); + + const char *weaponList = mapInfo->dict.GetString( "disableWeapons" ); + weaponFlags = 0; + if ( weaponList && weaponList[0] ) { + if ( !idStr::Icmp( weaponList, "all" ) ) { + // No weapons are enabled, so just return + return; + } + + if ( idStr::FindText( weaponList, "wrench", false ) == -1 ) { + weaponFlags |= HH_WEAPON_WRENCH; + } + if ( idStr::FindText( weaponList, "rifle", false ) == -1 ) { + weaponFlags |= HH_WEAPON_RIFLE; + } + if ( idStr::FindText( weaponList, "crawler", false ) == -1 ) { + weaponFlags |= HH_WEAPON_CRAWLER; + } + if ( idStr::FindText( weaponList, "autocannon", false ) == -1 ) { + weaponFlags |= HH_WEAPON_AUTOCANNON; + } + if ( idStr::FindText( weaponList, "hiderweapon", false ) == -1 ) { + weaponFlags |= HH_WEAPON_HIDERWEAPON; + } + if ( idStr::FindText( weaponList, "rocketlauncher", false ) == -1 ) { + weaponFlags |= HH_WEAPON_ROCKETLAUNCHER; + } + if ( idStr::FindText( weaponList, "soulstripper", false ) == -1 ){ + weaponFlags |= HH_WEAPON_SOULSTRIPPER; + } + } + + if ( weaponFlags == 0 ) { + // Allow all weapons by default + weaponFlags = -1; + } +} + +void hhPlayer::LockWeapon( int weaponNum ) { + // Special case, -1 locks all weapons AND the lighter + if ( weaponNum == -1 ) { + if (IsLighterOn()) { + LighterOff(); + } + if (weapon.IsValid()) { + weapon->PutAway(); + } + weaponFlags = 0; + idealWeapon = 0; + currentWeapon = 0; + return; + } + + if ( weaponNum <= 0 || weaponNum >= MAX_WEAPONS ) { + gameLocal.Error( "Attempted to unlock unknown weapon '%d'", weaponNum ); + } + + int flag = ( 1 << ( weaponNum - 1 ) ); + if ( weaponFlags & flag ) { // Only lock if not already locked + weaponFlags &= ~flag; + if ( idealWeapon == weaponNum ) { + if (weapon.IsValid()) { + weapon->PutAway(); + } + NextWeapon(); + } + } +} + +void hhPlayer::UnlockWeapon( int weaponNum ) { + // Special case, -1 unlocks all weapons + if ( weaponNum == -1 ) { + weaponFlags = -1; + if ( idealWeapon == 0 ) { + idealWeapon = 1; // Default to the wrench + } + return; + } + + if ( weaponNum <= 0 || weaponNum >= MAX_WEAPONS ) { + gameLocal.Error( "Attempted to unlock unknown weapon '%d'", weaponNum ); + } + + int flag = ( 1 << ( weaponNum - 1 ) ); + if ( weaponFlags & flag ) { + // Already unlocked + return; + } + + weaponFlags |= flag; + idealWeapon = weaponNum; +} + +bool hhPlayer::IsLocked(int weaponNum) { + //HUMANHEAD PCF mdl 05/05/06 - Changed to < 1 to catch weaponNum = -1 + if (weaponNum < 1) { + return true; // CJR: if the player has no weapons, then consider all weapons locked + } + return !(weaponFlags & (1 << (weaponNum-1))); +} + +void hhPlayer::Event_LockWeapon( int weaponNum ) { + LockWeapon( weaponNum ); +} + +void hhPlayer::Event_UnlockWeapon( int weaponNum ) { + UnlockWeapon( weaponNum ); +} + +//HUMANHEAD rdr +void hhPlayer::Event_SetPrivateCameraView( idEntity *camView, int noHide ) { + if ( camView && camView->IsType( idCamera::Type ) ) { + idPlayer::SetPrivateCameraView( static_cast( camView ), noHide != 0 ); + } + else { + idPlayer::SetPrivateCameraView( NULL ); + } +} + +//HUMANHEAD rdr +void hhPlayer::Event_SetCinematicFOV( float fieldOfView, float accelTime, float decelTime, float duration ) { + if ( duration > 0.f ) { + cinematicFOV.Init( gameLocal.time, SEC2MS( accelTime ), SEC2MS( decelTime ), SEC2MS( duration ), cinematicFOV.GetCurrentValue( gameLocal.GetTime() ), fieldOfView ); + } else { + cinematicFOV.Init( gameLocal.time, 0.f, 0.f, 0.f, cinematicFOV.GetEndValue(), fieldOfView ); + } +} + +//HUMANHEAD rww +void hhPlayer::Event_StopSpiritWalk() { + StopSpiritWalk(); +} + +void hhPlayer::Event_DamagePlayer(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, char *damageDefName, float damageScale, int location) { + Damage(inflictor, attacker, dir, damageDefName, damageScale, location); +} +//HUMANHEAD END + +void hhPlayer::Event_GetSpiritProxy() { + if ( spiritProxy.IsValid() ) { + idThread::ReturnEntity( spiritProxy.GetEntity() ); + } else { + idThread::ReturnEntity( NULL ); + } +} + +void hhPlayer::Event_IsSpiritWalking() { + idThread::ReturnInt( IsSpiritWalking() ? 1 : 0 ); +} + +void hhPlayer::Event_IsDeathWalking() { + idThread::ReturnInt( IsDeathWalking() ? 1 : 0 ); +} + +void hhPlayer::Event_AllowLighter( bool allow ) { + inventory.requirements.bCanUseLighter = allow; + if ( !allow && IsLighterOn() ) { + LighterOff(); + } +} + +//============================================================================= +// +// hhPlayer::Event_GetDDAValue +// +// Returns the current DDA value from zero to one +//============================================================================= + +void hhPlayer::Event_GetDDAValue() { + idThread::ReturnFloat( gameLocal.GetDDAValue() ); +} + +//HUMANHEAD rww +hhArtificialPlayer::hhArtificialPlayer(void) { + memset(&lastAICmd, 0, sizeof(lastAICmd)); + testCrouchActive = false; + testCrouchTime = 0; +} + +void hhArtificialPlayer::Spawn( void ) { + idDict apUI; + gameLocal.GetAPUserInfo(apUI, entityNumber); + gameLocal.SetUserInfo(entityNumber, apUI, gameLocal.isClient, false); + + SetPlayerModel(false); //force an update on the player model +} + +void hhArtificialPlayer::Think( void ) { + PROFILE_START("hhArtificialPlayer::Think", PROFMASK_NORMAL); + bool iFeelHoppy = false; + usercmd_t *nextCmd = &gameLocal.usercmds[ entityNumber ]; + idAngles idealAngles = GetViewAngles(); + + nextCmd->gameFrame = gameLocal.framenum; + nextCmd->gameTime = gameLocal.time; + + nextCmd->forwardmove = idMath::ClampChar(127); + + //this is not meant to resemble proper ai logic, it's just for testing. + idEntity *closestEnt = NULL; + float closestDist = idMath::INFINITY; + + int numSourceAreas, sourceAreas[ idEntity::MAX_PVS_AREAS ]; + numSourceAreas = gameRenderWorld->BoundsInAreas( GetPlayerPhysics()->GetAbsBounds(), sourceAreas, idEntity::MAX_PVS_AREAS ); + pvsHandle_t pvsHandle = gameLocal.pvs.SetupCurrentPVS( sourceAreas, numSourceAreas, PVS_NORMAL ); + + for(int i=0;iIsType(hhPlayer::Type)); + hhPlayer *plEnt = static_cast(ent); + + float l = (ent->GetOrigin()-GetOrigin()).Length(); + if(l < 4096.0f && (l < closestDist || !closestEnt) && ent->PhysicsTeamInPVS(pvsHandle)) { + trace_t tr; + + gameLocal.clip.TracePoint(tr, GetOrigin(), ent->GetOrigin(), GetPhysics()->GetClipMask(), this); + if (tr.c.entityNum == ent->entityNumber) { //if we hit the thing then it's visible. + if (ent->health > 0) { + if (gameLocal.gameType != GAME_TDM || plEnt->team != team) { + closestDist = l; + closestEnt = ent; + } + } + } + } + } + } + + gameLocal.pvs.FreeCurrentPVS( pvsHandle ); + + if (closestEnt) { + idealAngles = (closestEnt->GetOrigin()-GetOrigin()).ToAngles(); + } + else { //if no one to run mindlessly at, do some stuff. + const float testDist = 64.0f; + idVec3 fwd = GetOrigin()+(idealAngles.ToForward()*testDist); + + trace_t tr; + gameLocal.clip.TraceBounds(tr, GetOrigin(), fwd, GetPhysics()->GetBounds(), GetPhysics()->GetClipMask(), this); + if (tr.fraction != 1.0f) { //if we hit something set the yaw to a new random angle. + iFeelHoppy = true; + + idealAngles.yaw = rand()%360; + } + } + + if ((closestEnt || health <= 0) && rand()%2 == 1) { + nextCmd->buttons |= BUTTON_ATTACK; + } + else { + nextCmd->buttons &= ~BUTTON_ATTACK; + } + + if (testCrouchTime < gameLocal.time) { + testCrouchTime = gameLocal.time + rand()%4000; + testCrouchActive = !testCrouchActive; + } + + if (iFeelHoppy) { + nextCmd->upmove = idMath::ClampChar(127); + } + else { + if (testCrouchActive) { + nextCmd->upmove = idMath::ClampChar(-127); + } + else { + nextCmd->upmove = 0; + } + } + + idealAngles.pitch = idMath::AngleNormalize180(idealAngles.pitch); + idealAngles.yaw = idMath::AngleNormalize180(idealAngles.yaw); + idealAngles.roll = idMath::AngleNormalize180(idealAngles.roll); + SetUntransformedViewAngles(idAngles(0.0f, 0.0f, 0.0f)); + UpdateDeltaViewAngles(idealAngles); + UpdateOrientation(idealAngles); + PROFILE_STOP("hhArtificialPlayer::Think", PROFMASK_NORMAL); + hhPlayer::Think(); +} + +void hhArtificialPlayer::ClientPredictionThink( void ) { + gameLocal.usercmds[entityNumber] = lastAICmd; //copy over the last manually snapshotted usercmd first + hhPlayer::ClientPredictionThink(); +} + +void hhArtificialPlayer::WriteToSnapshot( idBitMsgDelta &msg ) const { + //instead of sync'ing, just mirror what the server has in ::Spawn + /* + msg.WriteDict(*gameLocal.GetUserInfo(entityNumber)); //ap's don't broadcast their info to the server, so the server otherwise + //will never broadcast it out to other clients + */ + + hhPlayer::WriteToSnapshot(msg); + + usercmd_t &cmd = gameLocal.usercmds[entityNumber]; + msg.WriteLong( cmd.gameTime ); + msg.WriteByte( cmd.buttons ); + msg.WriteShort( cmd.mx ); + msg.WriteShort( cmd.my ); + msg.WriteChar( cmd.forwardmove ); + msg.WriteChar( cmd.rightmove ); + msg.WriteChar( cmd.upmove ); + msg.WriteShort( cmd.angles[0] ); + msg.WriteShort( cmd.angles[1] ); + msg.WriteShort( cmd.angles[2] ); + + msg.WriteBits(spectating, 1); +} + +void hhArtificialPlayer::ReadFromSnapshot( const idBitMsgDelta &msg ) { + /* + ((idBitMsgDelta)msg).ReadDict(*((idDict *)gameLocal.GetUserInfo(entityNumber))); + if (!clientReceivedUI) { + UserInfoChanged( false ); + clientReceivedUI = true; + } + */ + + hhPlayer::ReadFromSnapshot(msg); + + usercmd_t &cmd = lastAICmd; + cmd.gameTime = msg.ReadLong(); + cmd.buttons = msg.ReadByte(); + cmd.mx = msg.ReadShort(); + cmd.my = msg.ReadShort(); + cmd.forwardmove = msg.ReadChar(); + cmd.rightmove = msg.ReadChar(); + cmd.upmove = msg.ReadChar(); + cmd.angles[0] = msg.ReadShort(); + cmd.angles[1] = msg.ReadShort(); + cmd.angles[2] = msg.ReadShort(); + + bool spec = !!msg.ReadBits(1); //doesn't go through right for ap's or something based on events sometimes, hack fix + if (spec != spectating) { + Spectate(spec); + } +} +//HUMANHEAD END + +void hhPlayer::DisableSpiritWalk(int timeout) { + nextSpiritTime = gameLocal.time + SEC2MS(timeout); + StopSpiritWalk(); +} + +//============================================================================= +// +// hhPlayer::Event_UpdateDDA +// +// CJR: Update the probability the player will die this tick, and +// adjust the difficulty accordingly +//============================================================================= + +void hhPlayer::Event_UpdateDDA() { + idEntity *ent; + int updateFlags; + + // Find the number of alive, non-dormant creatures attacking the player + ddaNumEnemies = 0; + ddaProbabilityAccum = 1.0f; + updateFlags = 0; + + for( ent = gameLocal.activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { + if ( ent->fl.isDormant || !ent->IsType( hhMonsterAI::Type ) || ent->fl.hidden || ent->health <= 0 ) { + continue; + } + + hhMonsterAI *monster = static_cast(ent); + if ( monster->GetEnemy() != this ) { + continue; + } + + // PVS Check: only include creatures that the player can attack or be attacked by + if ( !gameLocal.InPlayerPVS( ent ) ) { + continue; + } + + // Accumulate the survival rate against each enemy + int index = monster->spawnArgs.GetInt( "ddaIndex", "0" ); + + if ( index == -1 ) { // Skip certain monsters, such as the vacuum or the crawlers + continue; + } + + float prob = gameLocal.GetDDA()->DDA_GetProbability( index, GetHealth() ); + + ddaProbabilityAccum *= prob; + + // Recompute the actual difficulty based upon the creatures in view + updateFlags |= ( 1 << index ); + + ddaNumEnemies++; + } + + gameLocal.GetDDA()->RecalculateDifficulty( updateFlags ); + + PostEventSec( &EV_UpdateDDA, spawnArgs.GetFloat( "updateDDARate", "0.25" ) ); + + if ( g_printDDA.GetBool() ) { + if ( ddaNumEnemies > 0 ){ + common->Printf("Probability [%.2f]\n", ddaProbabilityAccum ); + } + } +} + +void hhPlayer::Event_DisableSpirit(void) { + // HUMANHEAD PCF pdm 05-17-06: Removed assert, as scripters were potentially calling during deathwalk + // HUMANHEAD PCF pdm 05-17-06: Only exit spirit realm if spiritwalking, not deathwalking + + if ( IsSpiritWalking() ) { + StopSpiritWalk(); + } + + bAllowSpirit = false; +} + +void hhPlayer::Event_EnableSpirit(void) { + // HUMANHEAD PCF pdm 05-17-06: Removed assert + + bAllowSpirit = true; +} + +//HUMANHEAD bjk +void hhPlayer::ProjectOverlay( const idVec3 &origin, const idVec3 &dir, float size, const char *material ) { + // no need for these in sp + if( gameLocal.isMultiplayer ) { + idActor::ProjectOverlay( origin, dir, size, material ); + } +} + +// HUMANHEAD mdl: Ripped from idAI for short invulnerability after returning from deathwalk +void hhPlayer::Event_AllowDamage( void ) { + fl.takedamage = true; +} + +void hhPlayer::Event_IgnoreDamage( void ) { + fl.takedamage = false; +} + +// Precompute all the weapons' info for faster HUD updates +void hhPlayer::SetupWeaponInfo() { + const char *fireInfoName = NULL; + const idDeclEntityDef *decl = NULL; + const char *ammoName = NULL; + float maxAmmo; + + memset(weaponInfo, 0, sizeof(weaponInfo_t)*15); + memset(altWeaponInfo, 0, sizeof(weaponInfo_t)*15); + + for (int ix=0; ix<15; ix++) { + const char *weaponClassName = spawnArgs.GetString( va( "def_weapon%d", ix ), NULL ); + if ( weaponClassName && *weaponClassName ) { + + const idDeclEntityDef *weaponObjDecl = gameLocal.FindEntityDef( weaponClassName, false ); + if ( weaponObjDecl ) { + fireInfoName = weaponObjDecl->dict.GetString("def_fireInfo"); + decl = gameLocal.FindEntityDef( fireInfoName, false ); + if ( decl ) { + weaponInfo[ix].ammoType = inventory.AmmoIndexForAmmoClass( decl->dict.GetString( "ammoType" ) ); + weaponInfo[ix].ammoRequired = decl->dict.GetInt("ammoRequired"); + weaponInfo[ix].ammoLow = decl->dict.GetInt("lowAmmo"); + + ammoName = idWeapon::GetAmmoNameForNum( weaponInfo[ix].ammoType ); + maxAmmo = inventory.MaxAmmoForAmmoClass( this, ammoName ); + weaponInfo[ix].ammoMax = max(1, maxAmmo); + } + + fireInfoName = weaponObjDecl->dict.GetString("def_altFireInfo"); + decl = gameLocal.FindEntityDef( fireInfoName, false ); + if ( decl ) { + altWeaponInfo[ix].ammoType = inventory.AmmoIndexForAmmoClass( decl->dict.GetString( "ammoType" ) ); + altWeaponInfo[ix].ammoRequired = decl->dict.GetInt("ammoRequired"); + altWeaponInfo[ix].ammoLow = decl->dict.GetInt("lowAmmo"); + + ammoName = idWeapon::GetAmmoNameForNum( altWeaponInfo[ix].ammoType ); + maxAmmo = inventory.MaxAmmoForAmmoClass( this, ammoName ); + altWeaponInfo[ix].ammoMax = max(1, maxAmmo); + } + } + } + } +} + +/* +=============== +hhPlayer::ThrowGrenade +HUMANHEAD bjk +=============== +*/ +void hhPlayer::ThrowGrenade( void ) { + idMat3 axis; + idVec3 muzzle; + + if( inventory.HasAmmo(inventory.AmmoIndexForAmmoClass( "ammo_crawler" ), 1) == false ) + return; + + if (IsSpiritOrDeathwalking() || InVehicle() || bReallyDead) { + return; + } + + if ( privateCameraView || !weaponEnabled || spectating || gameLocal.inCinematic || health < 0 || gameLocal.isClient ) + return; + + if( weapon.IsValid() ) { + if ( inventory.weapons & ( 1 << 12 ) && currentWeapon != 12 && currentWeapon != 3 && idealWeapon == currentWeapon ) { + idealWeapon = 12; + previousWeapon = currentWeapon; + } + else if( currentWeapon == 3 ) { + FireWeapon(); + usercmd.buttons = usercmd.buttons | BUTTON_ATTACK; + } + } +} + +//HUMANHEAD bjk +void hhPlayer::Event_ReturnToWeapon() { + const char *weap; + + if ( !weaponEnabled || spectating || gameLocal.inCinematic || health < 0 ) { + idThread::ReturnInt(0); + return; + } + + if ( ( previousWeapon < 0 ) || ( previousWeapon >= MAX_WEAPONS ) ) { + idThread::ReturnInt(0); + return; + } + + if ( gameLocal.isClient ) { + idThread::ReturnInt(0); + return; + } + + weap = spawnArgs.GetString( va( "def_weapon%d", previousWeapon ) ); + if ( !weap[ 0 ] ) { + gameLocal.Printf( "Invalid weapon\n" ); + idThread::ReturnInt(0); + return; + } + + if ( inventory.weapons & ( 1 << previousWeapon ) ) { + idealWeapon = previousWeapon; + } + + idThread::ReturnInt(1); +} + +//HUMANHEAD rww +void hhPlayer::Event_CanAnimateTorso(void) { + if (!gameLocal.isMultiplayer) { //don't worry about it + idThread::ReturnInt(1); + return; + } + + if (!weapon.IsValid() || !weapon->IsType(hhWeapon::Type)) { //bad + idThread::ReturnInt(0); + return; + } + + idThread::ReturnInt(1); +} + +void hhPlayer::Show(void) { + idActor::Show(); + + hhWeapon *weap; + weap = weapon.GetEntity(); + if ( weap ) { + if (!IsLocked(currentWeapon)) { + weap->ShowWorldModel(); + } else { + weap->HideWorldModel(); + } + } +} diff --git a/src/Prey/game_player.h b/src/Prey/game_player.h new file mode 100644 index 0000000..740933f --- /dev/null +++ b/src/Prey/game_player.h @@ -0,0 +1,771 @@ + +#ifndef __PREY_GAME_PLAYER_H__ +#define __PREY_GAME_PLAYER_H__ + +extern const idEventDef EV_PlayWeaponAnim; +extern const idEventDef EV_Resurrect; +extern const idEventDef EV_PrepareToResurrect; +extern const idEventDef EV_SetOverlayMaterial; +extern const idEventDef EV_SetOverlayTime; +extern const idEventDef EV_SetOverlayColor; +extern const idEventDef EV_ShouldRemainAlignedToAxial; +extern const idEventDef EV_StartHudTranslation; +extern const idEventDef EV_StopSpiritWalk; //rww +extern const idEventDef EV_DamagePlayer; //rww + +// HUMANHEAD IMPULSES SHOULD START AT 50 +const int IMPULSE_50 = 50; // Unused +const int IMPULSE_51 = 51; // Unused +const int IMPULSE_52 = 52; // Unused +const int IMPULSE_53 = 53; // Unused +const int IMPULSE_54 = 54; // Spirit power toggle + +//Declared in idPlayer.cpp +extern const int RAGDOLL_DEATH_TIME; +extern const int LADDER_RUNG_DISTANCE; +extern const int HEALTH_PER_DOSE; +extern const int WEAPON_DROP_TIME; +extern const int WEAPON_SWITCH_DELAY; +extern const int SPECTATE_RAISE; +extern const int HEALTHPULSE_TIME; +extern const float MIN_BOB_SPEED; + +// Forward declaration +class hhSpiritProxy; +class hhConsole; +class hhTalon; +class hhPossessedTommy; + +#define MAX_HEALTH_NORMAL_MP 100 //rww - a probably temporary hack for trying out the pipe-as-armor concept + +#define HH_WEAPON_WRENCH 1 +#define HH_WEAPON_RIFLE 2 +#define HH_WEAPON_CRAWLER 4 +#define HH_WEAPON_AUTOCANNON 8 +#define HH_WEAPON_HIDERWEAPON 16 +#define HH_WEAPON_ROCKETLAUNCHER 32 +#define HH_WEAPON_SOULSTRIPPER 64 + +// Structure to hold all info about the weapons to remove the dictionary lookups every tick +typedef struct weaponInfo_s { + int ammoType; + int ammoLow; + int ammoMax; + int ammoRequired; +} weaponInfo_t; + +#define MAX_TRACKED_ATTACKERS 3 +typedef struct attackInfo_s { + idEntityPtr attacker; + int time; + bool displayed; +} attackInfo_t; + + +class hhPlayer : public idPlayer { + CLASS_PROTOTYPE(hhPlayer); + +public: + weaponInfo_t weaponInfo[15]; + weaponInfo_t altWeaponInfo[15]; + float lighterTemperature; // Temp of the lighter. 0 = cold, 1 = too hot to use + renderLight_t lighter; // lighter + int lighterHandle; + + idEntityPtr spiritProxy; // Proxy player when the player is spiritwalking + int lastWeaponSpirit; // Last weapon the player held before entering spirit mode + idEntityPtr talon; // Talon, the spirit hawk + int nextTalonAttackCommentTime; // Time between when Tommy can make talon comments + bool bTalonAttackComment; // If true, Tommy can randomly make comments when Talon is attacking + bool bSpiritWalk; // True if the player is spirit walking + bool bDeathWalk; // True if the player is dead and DeathWalking + bool bReallyDead; // True if the player truly died (only when in deathwalk mode) + idEntityPtr guiWantsControls; // Gui console that controls should be routed to + idEntityPtr possessedTommy; // ptr to the possessed version of the player + + //rww - keep track of who attacked us in air, so if we die from unnatural causes other than a player, + //the person who last attacked will be credited. + idEntityPtr airAttacker; + int airAttackerTime; + + float deathWalkFlash; + int deathWalkTime; // time the player entered deathwalk + int deathWalkPower; // Power in deathwalk - when full, the player wins deathwalk + bool bInDeathwalkTransition; + + + bool bInCinematic; + bool lockView; + + bool bPlayingLowHealthSound; + bool bPossessed; // True if the player can be possessed + float possessionTimer; // Countdown timer until the player is fully possessed + float possessionFOV; // FOV for possession + + int preCinematicWeapon; // selected weapon before cinematic started for restoration + int preCinematicWeaponFlags; // flags to restore after cinematic completes + + int lastDamagedTime; // Used to determine when to recharge health + + void ReportAttack(idEntity *attacker); + attackInfo_t lastAttackers[MAX_TRACKED_ATTACKERS]; // Used for tracking attackers for HUD + + //Make these idEntityPtr just to be safe + hhWeaponHandState weaponHandState; + idEntityPtr hand; + idEntityPtr handNext; // Next hand to pop up + hhPlayerVehicleInterface vehicleInterfaceLocal; + + // Death Variables + idEntityPtr deathLookAtEntity; // nla - Entity to look at when dead + idStr deathLookAtBone; // nla - Entity's bone to look at when dead + idStr deathCameraBone; + + hhCameraInterpolator cameraInterpolator; + idAngles oldCmdAngles; // HUMANHEAD: aob + bool bClampYaw; // HUMANHEAD pdm + float maxRelativeYaw; // HUMANHEAD pdm + float maxRelativePitch; // HUMANHEAD rww + + hhSoundLeadInController spiritwalkSoundController; + hhSoundLeadInController deathwalkSoundController; + hhSoundLeadInController wallwalkSoundController; + + bool bShowProgressBar; // Whether to display progress bar on HUDs + float progressBarValue; // Current progress (set by script) + idInterpolate progressBarGuiValue; // Interpolates towards progressBarValue + int progressBarState; // 0=none, 1=success, 2=failure + + bool bScopeView; // HUMANHEAD rww + + // HUMANHEAD bjk + float kickSpring; + float kickDamping; + // HUMANHEAD END + +#if GAMEPAD_SUPPORT // VENOM BEGIN +//SAVETODO: save these + int lastAutoLevelTime; + int lastAccelTime; + float lastAccelFactor; +#endif // VENOM END + + float bob; + int lastAppliedBobCycle; + int prevStepUpTime; + idVec3 prevStepUpOrigin; + + //Crashland + float crashlandSpeed_fatal; + float crashlandSpeed_soft; + float crashlandSpeed_jump; + + // CJR: DDA variables + int ddaNumEnemies; // CJR + float ddaProbabilityAccum; + + int forcePredictionButtons; //HUMANHEAD rww + + idScriptBool AI_ASIDE; + + // Methods + hhPlayer(); + void Spawn( void ); + virtual ~hhPlayer(); + + + // Overridden Methods + virtual void RestorePersistantInfo( void ); + virtual void SquishedByDoor(idEntity *door); + virtual void Init(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void SpawnToPoint( const idVec3 &spawn_origin, const idAngles &spawn_angles ); + virtual void Think( void ); + virtual void AdjustBodyAngles( void ); + virtual void Move( void ); + virtual void Teleport( const idVec3 &origin, const idAngles &angles, idEntity *destination ); + virtual void Teleport( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& newUntransformedViewAngles, idEntity *destination ); + virtual void TeleportNoKillBox( const idVec3& origin, const idMat3& bboxAxis ); // HUMANHEAD cjr: A teleport that doesn't telefrag + virtual void TeleportNoKillBox( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& newUntransformedViewAngles ); + virtual void LinkScriptVariables( void ); + virtual void Present(); + + virtual void UpdateFromPhysics( bool moveBack ); + virtual bool UpdateAnimationControllers( void ); + + virtual bool Give( const char *statname, const char *value ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual bool ServerReceiveEvent( int event, int time, const idBitMsg &msg ); //rww - override idPlayer + + virtual bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); //rww + + virtual void WritePlayerStateToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadPlayerStateFromSnapshot( const idBitMsgDelta &msg ); + //rww - our own prediction function + virtual void ClientPredictionThink( void ); + + bool SetWeaponSpawnId( int id ) { return weapon.SetSpawnId( id ); } + + virtual void NextBestWeapon( void ); + virtual void SelectWeapon( int num, bool force ); + virtual void NextWeapon( void ); + virtual void PrevWeapon( void ); + virtual void UpdateWeapon( void ); + virtual void UpdateFocus( void ); + virtual void UpdateViewAngles( void ); + virtual void UpdateDeltaViewAngles( const idAngles &angles ); + virtual idAngles DetermineViewAngles( const usercmd_t& cmd, idAngles& cmdAngles ); + virtual bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + virtual void GetViewPos( idVec3 &origin, idMat3 &axis ); //HUMANHEAD bjk + + virtual void UpdateHud( idUserInterface *_hud ); + virtual void UpdateHudWeapon(bool flashWeapon = true); + virtual void UpdateHudAmmo(idUserInterface *_hud); + virtual void UpdateHudStats( idUserInterface *_hud ); + virtual void DrawHUD( idUserInterface *_hud ); + virtual void DrawHUDVehicle( idUserInterface* _hud );//HUMANHEAD: aob + virtual void Weapon_Combat( void ); + virtual void Weapon_GUI( void ); + virtual void PerformImpulse(int impulse); + virtual void ApplyImpulse( idEntity *ent, int id, const idVec3 &point, const idVec3 &impulse ); + + virtual int HasAmmo( ammo_t type, int amount ); + virtual bool UseAmmo( ammo_t type, int amount ); + + virtual void CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ); + virtual void BobCycle( const idVec3 &pushVelocity ); + virtual void OffsetThirdPersonView( float angle, float range, float height, bool clip ); + virtual void CalculateRenderView( void ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual bool IsWallWalking( void ) const; + virtual bool WasWallWalking( void ) const; + virtual float GetStepHeight() const; + + virtual int GetWeaponNum( const char* weaponName ) const; + virtual const char* GetWeaponName( int num ) const; + + virtual idAngles GunTurningOffset( void ) { return idPlayer::GunTurningOffset(); } + virtual idVec3 GunAcceleratingOffset( void ) { return idPlayer::GunAcceleratingOffset(); } + + virtual void SetupWeaponEntity( void ); + + virtual void SetUntransformedViewAngles( const idAngles& newUntransformedViewAngles ) { untransformedViewAngles = newUntransformedViewAngles; } + virtual void SetUntransformedViewAxis( const idMat3& newUntransformedViewAxis ) { untransformedViewAxis = newUntransformedViewAxis; } + virtual const idAngles& GetUntransformedViewAngles() const; + virtual const idMat3& GetUntransformedViewAxis() const; + virtual idAngles GetViewAngles() const; + virtual void SetEyeAxis( const idMat3 &axis ); + virtual void SetEyeHeight( float height ); + virtual float EyeHeight( void ) const; + virtual idVec3 EyeOffset( void ) const; + virtual idMat3 GetEyeAxis() const; + virtual idAngles GetEyeAxisAsAngles() const; + virtual idQuat GetEyeAxisAsQuat() const; + void ShouldRemainAlignedToAxial( bool remainAligned ); + void OrientToGravity( bool bRotateToGravity ); + void SetOrientation( const idVec3& origin, const idMat3& bboxAxis, const idVec3& lookDir, const idAngles& newUntransformedViewAngles ); + void RestoreOrientation( const idVec3& origin, const idMat3& bboxAxis, const idVec3& lookDir, const idAngles& newUntransformedViewAngles ); + void UpdateOrientation( const idAngles& newUntransformedViewAngles ); + virtual bool CheckFOV( const idVec3 &pos ); + virtual bool CheckYawFOV( const idVec3 &pos ); + virtual idVec3 GetEyePosition( void ) const; + virtual idVec3 TransformToPlayerSpace( const idVec3& origin ) const; + virtual idVec3 TransformToPlayerSpaceNotInterpolated( const idVec3& origin ) const; //rww + virtual idMat3 TransformToPlayerSpace( const idMat3& axis ) const; + virtual idAngles TransformToPlayerSpace( const idAngles& angles ) const; + virtual bool ShouldTouchTrigger( idEntity* entity ) const; + + virtual void EnterVehicle( hhVehicle* vehicle ); + virtual void ExitVehicle( hhVehicle* vehicle ); + virtual void ResetClipModel(); + hhPlayerVehicleInterface* GetVehicleInterfaceLocal() { return &vehicleInterfaceLocal; } + virtual void BecameBound(hhBindController *b); // Just attached to a bindController + virtual void BecameUnbound(hhBindController *b); // Just detached from a bindController + + virtual void Possess( idEntity* possessor ); // cjr + virtual void Unpossess(); // cjr + virtual bool CanBePossessed( void ); // cjr + void PossessKilled( void ); // cjr + + virtual idVec3 GetPortalPoint( void ); // cjr: The entity will portal when this point crosses the portal plane. Origin for most, eye location for players + virtual void Portalled( idEntity *portal ); + void SetPortalColliding( bool b ) { bCollidingWithPortal = b; } + bool IsPortalColliding( void ) { return bCollidingWithPortal; } + + virtual void UpdateModelTransform( void );//aob + + virtual void PlayFootstepSound();//aob + virtual void PlayPainSound();//aob + + bool IsCrouching() const { return physicsObj.IsCrouching(); } + virtual void ForceCrouching() { physicsObj.ForceCrouching(); }//aob + virtual void FillDebugVars( idDict *args, int page ); + virtual bool ChangingWeapons() const { return idealWeapon != currentWeapon; } + void BufferLoggedViewAngles( const idAngles& newUntransformedViewAngles ); + virtual void GetPilotInput( usercmd_t& pilotCmds, idAngles& pilotViewAngles ); + + void UpdateDDA(); // CJR DDA + + virtual void UpdateLocation( void ); + + // Unique Methods + void SetupWeaponInfo(); + void DialogStart(bool bDisallowPlayerDeath, bool bVoiceDucking, bool bLowerWeapon); + void DialogStop(); + void DoDeathDrop(); + void GetLocationText( idStr &locationString ); + void TrySpawnTalon(); + void TalonAttackComment(); // cjr - comment on Talon attacking + void RemoveResources(); + int GetCurrentWeapon( void ) const; + void SetCurrentWeapon( const int _currentWeapon ); + int GetIdealWeapon( void ) const; + void InvalidateCurrentWeapon() { currentWeapon = -1; } + virtual bool ShouldRemainAlignedToAxial() const { return physicsObj.ShouldRemainAlignedToAxial(); } + virtual idVec3 ApplyLandDeflect( const idVec3& pos, float scale ); + virtual idVec3 ApplyBobCycle( const idVec3& pos, const idVec3& velocity ); + virtual void DetermineOwnerPosition( idVec3 &ownerOrigin, idMat3 &ownerAxis ); + void StartSpiritWalk( const bool bThrust, bool force = false ); // mdl: Added force for when the player is possessed + void StopSpiritWalk(bool forceAllowance = false); //rww - added forceAllowance + void ToggleSpiritWalk( void ); + + int GetDeathWalkPower( void ) { return deathWalkPower; } // cjr: used for determining winning deathwalk + void SetDeathWalkPower( int newPower ) { deathWalkPower = newPower; } // cjr: used for determining winning deathwalk + + void ToggleLighter( void ); + void LighterOn(); + void LighterOff(); + bool IsLighterOn() const; //HUMANHEAD PCF mdl 05/04/06 - Made const + void UpdateLighter(); + void ThrowGrenade(); + void Event_ReturnToWeapon(); + void EnableEthereal( const char *proxyName, const idVec3& origin, const idMat3& bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ); + void DisableEthereal( void ); + void DisableSpiritWalk( int timeout ); // Number of seconds to disable ethereal mode + void GetResurrectionPoint( idVec3& origin, idMat3& axis, idVec3& viewDir, idAngles& angles, idMat3& eyeAxis, const idBounds& absBounds, const idVec3& defaultOrigin, const idMat3& defaultAxis, const idVec3& defaultViewDir, const idAngles& defaultAngles ); + void UpdatePossession( void ); // cjr + virtual void Kill( bool delayRespawn, bool nodamage ); // aob + void DeathWalk( const idVec3& resurrectOrigin, const idMat3& resurrectAxis, const idMat3& resurrectViewAxis, const idAngles& resurrectAngles, const idMat3& resurrectEyeAxis ); // cjr: entering deathwalk mode + idEntity * GetDeathwalkEnergyDestination(); + void DeathWraithEnergyArived(bool energyHealth); + void DeathWalkDamagedByWraith(idEntity *attacker, const char *damageType); + void DeathWalkSuccess(); + bool DeathWalkStage2() { return bDeathWalkStage2; } + void ReallyKilled( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); // cjr: truly dead (killed while in deathwalk mode) + void Resurrect( void ); + void KilledDeathWraith( void ); // cjr: called when the player kills a deathwraith (determines if the player should resurrect) + virtual bool IsDead() const { return (!bDeathWalk && health <= 0); } // True if the player is really dead + virtual bool IsSpiritOrDeathwalking() const { return ( bSpiritWalk || bDeathWalk ); } // True if the player is spiritwalking or deathwalking + virtual bool IsSpiritWalking() const { return ( bSpiritWalk && !bDeathWalk ); } // True if the player is spiritwalking, but not deathwalking + virtual bool IsDeathWalking() const { return bDeathWalk; } // True if the player is deathwalking + virtual bool IsPossessed() const { return bPossessed; } + void SnapDownCurrentWeapon(); + void SnapUpCurrentWeapon(); + void SelectEtherealWeapon(); + void PutawayEtherealWeapon(); + bool SkipWeapon( int weaponNum ) const; + hhWeapon* SpawnWeapon( const char* name ); + virtual bool InGUIMode( ); // nla + void CL_UpdateProgress(bool bBar, float value, int state); + int GetSpiritPower(); + void SetSpiritPower(int amount); + + void ResetZoomFov() { zoomFov.Init( gameLocal.GetTime(), 0, g_fov.GetFloat(), g_fov.GetFloat() ); } + virtual float CalcFov( bool honorZoom ); + virtual void MangleControls(usercmd_t *cmd); + virtual void UpdateCrosshairs( void ); + + void SetOverlayGui(const char *guiName); + + void InCinematic( bool cinematic ) { bInCinematic = cinematic; } + bool InCinematic() const { return bInCinematic; } + + const char * GetGuiHandInfo(); // nla + + idInterpolate& GetZoomFov() { return zoomFov; } + + void SetViewBob(const idVec3 &v) {viewBob = v;} + int GetLastResurrectTime(void) const {return lastResurrectTime;} // Time stamp indicating when the player last was resurrected + virtual idVec3 GetAimPosition() const; // jrm + + void SetViewAnglesSensitivity( float factor ); + float GetViewAnglesSensitivity() const; + + //? Is there a better way of doing this? + // Force the weapon num... + virtual void ForceWeapon( int weaponNum ); + virtual void ForceWeapon( hhWeapon *newWeapon ); + void Freeze( float unfreezeDelay = 0.0f ); + void Unfreeze(void); + bool IsFrozen() { return bFrozen; }; + + void LockWeapon( int weaponNum ); + void UnlockWeapon( int weaponNum ); + bool IsLocked( int weaponNum ); + hhSpiritProxy* GetSpiritProxy() { return spiritProxy.IsValid() ? spiritProxy.GetEntity() : NULL; } + + //rww - instead of doing this through the death proxy, this is safer (being an af, the death proxy could be oriented strangely or removed in numerous ways) + virtual void RestorePlayerLocationFromDeathwalk( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& angles ); + + virtual void ProjectOverlay( const idVec3 &origin, const idVec3 &dir, float size, const char *material ); + + int mpHitFeedbackTime; //HUMANHEAD rww + + virtual void Show(); + +protected: + idUserInterface * guiOverlay; + idClipModel thirdPersonCameraClipBounds; + float viewAnglesSensitivity; + int lastResurrectTime; + int spiritDrainHeartbeatMS; + int ddaHeartbeatMS; + int spiritWalkToggleTime; + bool bDeathWalkStage2; + bool bFrozen; + int weaponFlags; // Flags to limit weapons on specific level -mdl + int nextSpiritTime; // Disable ethereal mode unless gameLocal.time is greater than this + idInterpolateAccelDecelSine cinematicFOV; // HUMANHEAD rdr: Interpolated FOV for scripter use + bool bAllowSpirit; + + bool bCollidingWithPortal; // CJR + bool bLotaTunnelMode; // Used to disable hud during LOTA tunnel transitions + + // HUMANHEAD: aob + idAngles untransformedViewAngles; + idMat3 untransformedViewAxis; + // HUMANHEAD END + + //HUMANHEAD rww + idVec3 deathwalkLastOrigin; + idMat3 deathwalkLastBBoxAxis; + idMat3 deathwalkLastViewAxis; + idAngles deathwalkLastViewAngles; + idMat3 deathwalkLastEyeAxis; + bool deathwalkLastCrouching; + //HUMANHEAD END + + // Overridden Methods + virtual int GetMaxHealth() { return inventory.maxHealth; } + + // Unique Methods + virtual void FireWeapon( void ); + virtual void FireWeaponAlt( void ); + + void StopFiring( void ); + + void SetupWeaponFlags( void ); + + void Event_LotaTunnelMode(bool on); + void Event_PlayWeaponAnim( const char* animName, int numTries = 0 ); + void Event_DrainSpiritPower(); // HUMANHEAD pdm + + void Event_RechargeHealth( void ); // cjr + void Event_RechargeRifleAmmo( void ); // cjr + + void Event_SpawnDeathWraith(); // cjr + void Event_AdjustSpiritPowerDeathWalk(); + void Event_PrepareForDeathWorld(); + void Event_EnterDeathWorld(); // cjr: set-up for teleporting the player to the deathworld + void Event_PrepareToResurrect(); + void Event_ResurrectScreenFade(); + void Event_Resurrect(); + virtual void Event_GetSpiritPower(); //rww + virtual void Event_SetSpiritPower(const float s); //rww + void Event_OnGround(); // bg + + void Event_Cinematic( int on, int lockView ); // pdm + void Event_DialogStart( int bDisallowPlayerDeath, int bVoiceDucking, int bLowerWeapon ); + void Event_DialogStop(); + void Event_ShouldRemainAlignedToAxial( bool remainAligned ); + void Event_OrientToGravity( bool orient ); + virtual void Event_ResetGravity(); + void Event_SetOverlayMaterial( const char *mtrName, const int requiresScratch ); + void Event_SetOverlayTime( const float newTime, const int requiresScratch ); + void Event_SetOverlayColor( const float r, const float g, const float b, const float a ); + + void Event_DDAHeartBeat(); + void Event_StartHUDTranslation(); + void Event_Unfreeze(); + void Event_LockWeapon( int weaponNum ); + void Event_UnlockWeapon( int weaponNum ); + + void Event_SetPrivateCameraView( idEntity *camView, int noHide ); //HUMANHEAD rdr + void Event_SetCinematicFOV( float fieldOfView, float accelTime, float decelTime, float duration ); //HUMANHEAD rdr + void Event_StopSpiritWalk(); //rww + void Event_DamagePlayer(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, char *damageDefName, float damageScale, int location); //rww + void Event_GetSpiritProxy(); + void Event_IsSpiritWalking(); + void Event_IsDeathWalking(); //bjk + void Event_GetDDAValue(); // cjr + void Event_AllowLighter( bool allow ); + void Event_DisableSpirit(); + void Event_EnableSpirit(); + void Event_CanAnimateTorso(void); //HUMANHEAD rww + + void Event_UpdateDDA(); // cjr + + void Event_AllowDamage(); // mdl + void Event_IgnoreDamage(); // mdl + + void Event_RespawnCleanup(void); //HUMANHEAD rww +}; + +/* +===================== +hhPlayer::SetEyeAxis +===================== +*/ +ID_INLINE void hhPlayer::SetEyeAxis( const idMat3 &axis ) { + if (!cameraInterpolator.GetIdealAxis().Compare(axis)) { + cameraInterpolator.SetTargetAxis( axis, INTERPOLATE_EYEOFFSET ); + } +} + +/* +===================== +hhPlayer::SetEyeHeight +===================== +*/ +ID_INLINE void hhPlayer::SetEyeHeight( float height ) { + idPlayer::SetEyeHeight( height ); + + cameraInterpolator.SetTargetEyeOffset( height, INTERPOLATE_EYEOFFSET ); +} + +/* +===================== +hhPlayer::EyeHeight +===================== +*/ +ID_INLINE float hhPlayer::EyeHeight( void ) const { + return cameraInterpolator.GetCurrentEyeHeight(); +} + +/* +===================== +hhPlayer::EyeOffset +===================== +*/ +ID_INLINE idVec3 hhPlayer::EyeOffset( void ) const {//HUMANHEAD + return cameraInterpolator.GetCurrentEyeOffset(); +} + +/* +===================== +hhPlayer::ShouldRemainAlignedToAxial + HUMANHEAD +===================== +*/ +ID_INLINE void hhPlayer::ShouldRemainAlignedToAxial( bool remainAligned ) {//HUMANHEAD + physicsObj.ShouldRemainAlignedToAxial( remainAligned ); +} + +/* +===================== +hhPlayer::OrientToGravity + HUMANHEAD +===================== +*/ +ID_INLINE void hhPlayer::OrientToGravity( bool orientToGravity ) {//HUMANHEAD + physicsObj.OrientToGravity( orientToGravity ); +} + +/* +===================== +hhPlayer::EyePosition + HUMANHEAD +===================== +*/ +ID_INLINE idVec3 hhPlayer::GetEyePosition( void ) const {//HUMANHEAD + return cameraInterpolator.GetEyePosition(); +} + +/* +===================== +hhPlayer::GetEyeAxis + HUMANHEAD +===================== +*/ +ID_INLINE idMat3 hhPlayer::GetEyeAxis() const { + return cameraInterpolator.GetCurrentAxis(); +} + +/* +===================== +hhPlayer::GetEyeAxisAsAngles + HUMANHEAD +===================== +*/ +ID_INLINE idAngles hhPlayer::GetEyeAxisAsAngles() const { + return cameraInterpolator.GetCurrentAngles(); +} + +/* +===================== +hhPlayer::GetEyeAxisAsQuat + HUMANHEAD +===================== +*/ +ID_INLINE idQuat hhPlayer::GetEyeAxisAsQuat() const { + return cameraInterpolator.GetCurrentRotation(); +} + +/* +===================== +hhPlayer::GetStepHeight + HUMANHEAD +===================== +*/ +ID_INLINE float hhPlayer::GetStepHeight() const { + if( IsWallWalking() ) { + return pm_wallwalkstepsize.GetFloat(); + } + + return pm_stepsize.GetFloat(); +} + +/* +===================== +hhPlayer::TransformToPlayerSpace + HUMANHEAD +===================== +*/ +ID_INLINE idVec3 hhPlayer::TransformToPlayerSpace( const idVec3& origin ) const { + return cameraInterpolator.GetCurrentPosition() + origin * GetEyeAxis(); +} + +/* +===================== +hhPlayer::TransformToPlayerSpaceNotInterpolated + HUMANHEAD rww +===================== +*/ +ID_INLINE idVec3 hhPlayer::TransformToPlayerSpaceNotInterpolated( const idVec3& origin ) const { + return cameraInterpolator.GetIdealPosition() + origin * cameraInterpolator.GetIdealAxis(); +} + +/* +===================== +hhPlayer::TransformToPlayerSpace + HUMANHEAD +===================== +*/ +ID_INLINE idMat3 hhPlayer::TransformToPlayerSpace( const idMat3& axis ) const { + return axis * GetEyeAxis(); +} + +/* +===================== +hhPlayer::TransformToPlayerSpace + HUMANHEAD +===================== +*/ +ID_INLINE idAngles hhPlayer::TransformToPlayerSpace( const idAngles& angles ) const { + return (angles.ToMat3() * GetEyeAxis()).ToAngles(); +} + +/* +===================== +hhPlayer::GetCurrentWeapon + HUMANHEAD +===================== +*/ +ID_INLINE int hhPlayer::GetCurrentWeapon( void ) const { + return currentWeapon; +} + +/* +===================== +hhPlayer::SetCurrentWeapon + HUMANHEAD +===================== +*/ +ID_INLINE void hhPlayer::SetCurrentWeapon( const int _currentWeapon ) { + if( _currentWeapon >= 0 && _currentWeapon < MAX_WEAPONS ) { + currentWeapon = _currentWeapon; + } +} + +/* +===================== +hhPlayer::GetIdealWeapon + HUMANHEAD +===================== +*/ +ID_INLINE int hhPlayer::GetIdealWeapon( void ) const { + return idealWeapon; +} + +/* +===================== +hhPlayer::GetUntransformedViewAngles + HUMANHEAD +===================== +*/ +ID_INLINE const idAngles& hhPlayer::GetUntransformedViewAngles() const { + return untransformedViewAngles; +} + +/* +===================== +hhPlayer::GetUntransformedViewAxis + HUMANHEAD +===================== +*/ +ID_INLINE const idMat3& hhPlayer::GetUntransformedViewAxis() const { + return untransformedViewAxis; +} + +/* +===================== +hhPlayer::GetViewAngles + HUMANHEAD +===================== +*/ +ID_INLINE idAngles hhPlayer::GetViewAngles() const { + return viewAngles; +} + +/* +===================== +hhPlayer::GetPortalPoint + HUMANHEAD +===================== +*/ +ID_INLINE idVec3 hhPlayer::GetPortalPoint( void ) { + return idEntity::GetPortalPoint(); +/* + idVec3 origin; + idMat3 axis; + + GetViewPos( origin, axis ); + return origin; // Compensate for the near-clip plane +*/ +} + +//HUMANHEAD rww +class hhArtificialPlayer : public hhPlayer { + CLASS_PROTOTYPE(hhArtificialPlayer); +public: + hhArtificialPlayer(void); + + void Spawn( void ); + virtual void Think( void ); + + virtual void ClientPredictionThink( void ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + +protected: + usercmd_t lastAICmd; + + bool testCrouchActive; + int testCrouchTime; +}; +//HUMANHEAD END +#endif /* __PREY_GAME_PLAYER_H__ */ diff --git a/src/Prey/game_playerview.cpp b/src/Prey/game_playerview.cpp new file mode 100644 index 0000000..43db729 --- /dev/null +++ b/src/Prey/game_playerview.cpp @@ -0,0 +1,748 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define LETTERBOX_HEIGHT_TOP 50 +#define LETTERBOX_HEIGHT_BOTTOM 50 + +const int IMPULSE_DELAY = 150; + +//HUMANHEAD rww - render demo madness +#if _HH_RENDERDEMO_HACKS + void RENDER_DEMO_VIEWRENDER(const renderView_t *view, const hhPlayerView *pView) { + if (!view) { + return; + } + + const renderView_t *v = view; + + if (pView) { + static renderView_t hackedView = *view; + hackedView.viewaxis = hackedView.viewaxis * pView->ShakeAxis(); + + v = &hackedView; + } + + renderSystem->LogViewRender(v); + } + + void RENDER_DEMO_VIEWRENDER_END(void) { + renderSystem->LogViewRender(NULL); + } +#endif +//HUMANHEAD END + +hhPlayerView::hhPlayerView() { + // HUMANHEAD pdm: we don't use the tunnel vision or armor + bLetterBox = false; + letterboxMaterial = declManager->FindMaterial( "_black" ); + dirDmgLeftMaterial = declManager->FindMaterial( "textures/interface/directionalDamageLeft" ); + dirDmgFrontMaterial = declManager->FindMaterial( "textures/interface/directionalDamageFront" ); + spiritMaterial = NULL; + viewOverlayMaterial = NULL; + viewOverlayColor = colorWhite; + voTotalTime = -1; + voRequiresScratchBuffer = false; + viewOffset.Zero(); + + kickSpeed.Zero(); + kickLastTime = 0; + hurtValue = 100.f; +} + +void hhPlayerView::Save(idSaveGame *savefile) const { + idPlayerView::Save( savefile ); + + savefile->WriteBool( bLetterBox ); + savefile->WriteFloat( mbAmplitude ); + savefile->WriteInt( mbFinishTime ); + savefile->WriteInt( mbTotalTime ); + savefile->WriteVec3( mbDirection ); + savefile->WriteVec3( viewOffset ); + savefile->WriteMaterial( viewOverlayMaterial ); + savefile->WriteVec4( viewOverlayColor ); + savefile->WriteInt( voFinishTime ); + savefile->WriteInt( voTotalTime ); + savefile->WriteInt( voRequiresScratchBuffer ); + savefile->WriteMaterial( letterboxMaterial ); + savefile->WriteMaterial( dirDmgLeftMaterial ); + savefile->WriteMaterial( dirDmgFrontMaterial ); + savefile->WriteMaterial( spiritMaterial ); + savefile->WriteVec3( lastDamageLocation ); + savefile->WriteInt( kickLastTime - gameLocal.time ); + savefile->WriteAngles( kickSpeed ); + savefile->WriteFloat( hurtValue ); +} + +void hhPlayerView::Restore( idRestoreGame *savefile ) { + idPlayerView::Restore( savefile ); + + savefile->ReadBool( bLetterBox ); + savefile->ReadFloat( mbAmplitude ); + savefile->ReadInt( mbFinishTime ); + savefile->ReadInt( mbTotalTime ); + savefile->ReadVec3( mbDirection ); + savefile->ReadVec3( viewOffset ); + savefile->ReadMaterial( viewOverlayMaterial ); + savefile->ReadVec4( viewOverlayColor ); + savefile->ReadInt( voFinishTime ); + savefile->ReadInt( voTotalTime ); + savefile->ReadInt( voRequiresScratchBuffer ); + savefile->ReadMaterial( letterboxMaterial ); + savefile->ReadMaterial( dirDmgLeftMaterial ); + savefile->ReadMaterial( dirDmgFrontMaterial ); + savefile->ReadMaterial( spiritMaterial ); + savefile->ReadVec3( lastDamageLocation ); + savefile->ReadInt( kickLastTime ); + kickLastTime += gameLocal.time; + savefile->ReadAngles( kickSpeed ); + savefile->ReadFloat( hurtValue ); +} + +#define MAX_SCREEN_BLOBS_SYNC 2//MAX_SCREEN_BLOBS + +//rwwFIXME it may be possible to remove a lot of this junk by assuring events happen on the client as they do on the server +//concerning damage etc. +void hhPlayerView::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteFloat(viewOffset.x); + msg.WriteFloat(viewOffset.y); + msg.WriteFloat(viewOffset.z); + + msg.WriteBits(voTotalTime, 32); + msg.WriteBits(voFinishTime, 32); + msg.WriteBits(voRequiresScratchBuffer, 1); + + if (viewOverlayMaterial) { + msg.WriteLong(gameLocal.ServerRemapDecl(-1, DECL_MATERIAL, viewOverlayMaterial->Index())); + } + else { + msg.WriteLong(-1); + } + + //write screen blobs to snapshot + for (int i = 0; i < MAX_SCREEN_BLOBS_SYNC; i++) { + const screenBlob_t *blob = &screenBlobs[i]; + + //i'm disabling this since it might work against delta compression being effective + /* + if ( blob->finishTime <= gameLocal.time ) { + msg.WriteBits(0, 1); //no need to do anymore, continue + continue; + } + + //otherwise continue writing + msg.WriteBits(1, 1); + */ + + if (blob->material) { + msg.WriteLong(gameLocal.ServerRemapDecl(-1, DECL_MATERIAL, blob->material->Index())); + } + else { + msg.WriteLong(-1); + } + + msg.WriteFloat(blob->x); + msg.WriteFloat(blob->y); + msg.WriteFloat(blob->w); + msg.WriteFloat(blob->h); + + msg.WriteFloat(blob->s1); + msg.WriteFloat(blob->t1); + msg.WriteFloat(blob->s2); + msg.WriteFloat(blob->t2); + + msg.WriteBits(blob->finishTime, 32); + msg.WriteBits(blob->startFadeTime, 32); + + msg.WriteFloat(blob->driftAmount); + } + + //motion blur + msg.WriteFloat(mbAmplitude); + msg.WriteBits(mbFinishTime, 32); + msg.WriteBits(mbTotalTime, 32); + msg.WriteFloat(mbDirection.x); + msg.WriteFloat(mbDirection.y); + msg.WriteFloat(mbDirection.z); + + msg.WriteFloat(viewOverlayColor.w); + msg.WriteFloat(viewOverlayColor.x); + msg.WriteFloat(viewOverlayColor.y); + msg.WriteFloat(viewOverlayColor.z); +} + +void hhPlayerView::ReadFromSnapshot( const idBitMsgDelta &msg ) { + viewOffset.x = msg.ReadFloat(); + viewOffset.y = msg.ReadFloat(); + viewOffset.z = msg.ReadFloat(); + + voTotalTime = msg.ReadBits(32); + voFinishTime = msg.ReadBits(32); + voRequiresScratchBuffer = !!msg.ReadBits(1); + + int matIndex = msg.ReadLong(); + if (matIndex == -1) { + viewOverlayMaterial = NULL; + } + else { + int mappedIndex; + + if (voTotalTime != -1 && gameLocal.time >= voFinishTime) { //it's timed out so force it off + voTotalTime = -1; + voRequiresScratchBuffer = false; + viewOverlayMaterial = NULL; + mappedIndex = -1; + } + else { + mappedIndex = gameLocal.ClientRemapDecl(DECL_MATERIAL, matIndex); + } + + if (mappedIndex != -1) { + viewOverlayMaterial = static_cast(declManager->DeclByIndex(DECL_MATERIAL, mappedIndex)); + } + else { + viewOverlayMaterial = NULL; + } + } + + //read screen blobs from snapshot + for (int i = 0; i < MAX_SCREEN_BLOBS_SYNC; i++) { + screenBlob_t *blob = &screenBlobs[i]; + + //i'm disabling this since it might work against delta compression being effective + /* + bool valid = !!msg.ReadBits(1); + if (!valid) { + continue; + } + */ + + int blobMat = msg.ReadLong(); + if (blobMat == -1) { + blob->material = NULL; + } + else { + int mappedIndex = gameLocal.ClientRemapDecl(DECL_MATERIAL, blobMat); + if (mappedIndex != -1) { + blob->material = static_cast(declManager->DeclByIndex(DECL_MATERIAL, mappedIndex)); + } + else { + blob->material = NULL; + } + } + + blob->x = msg.ReadFloat(); + blob->y = msg.ReadFloat(); + blob->w = msg.ReadFloat(); + blob->h = msg.ReadFloat(); + + blob->s1 = msg.ReadFloat(); + blob->t1 = msg.ReadFloat(); + blob->s2 = msg.ReadFloat(); + blob->t2 = msg.ReadFloat(); + + blob->finishTime = msg.ReadBits(32); + blob->startFadeTime = msg.ReadBits(32); + + blob->driftAmount = msg.ReadFloat(); + } + + //motion blur + mbAmplitude = msg.ReadFloat(); + mbFinishTime = msg.ReadBits(32); + mbTotalTime = msg.ReadBits(32); + mbDirection.x = msg.ReadFloat(); + mbDirection.y = msg.ReadFloat(); + mbDirection.z = msg.ReadFloat(); + + viewOverlayColor.w = msg.ReadFloat(); + viewOverlayColor.x = msg.ReadFloat(); + viewOverlayColor.y = msg.ReadFloat(); + viewOverlayColor.z = msg.ReadFloat(); +} + +void hhPlayerView::ClearEffects() { + idPlayerView::ClearEffects(); + + // HUMANHEAD pdm + mbFinishTime = gameLocal.time; +} + + +void hhPlayerView::SetDamageLoc(const idVec3 &damageLoc) { + // This called before damageImpulse() so store the location + lastDamageLocation = damageLoc; +} + + +//------------------------------------------------------ +// +// DamageImpulse +// +// LocalKickDir is the direction of force in the player's coordinate system, +// which will determine the head kick direction +//------------------------------------------------------ +void hhPlayerView::DamageImpulse( idVec3 localKickDir, const idDict *damageDef ) { + + // No screen damage effects when in third person + if (pm_thirdPerson.GetBool()) { + return; + } + + if ( lastDamageTime > 0.0f && SEC2MS( lastDamageTime ) + IMPULSE_DELAY > gameLocal.time ) { + // keep shotgun from obliterating the view + return; + } + + if (!player->InVehicle()) { + // + // Motion Blur effect + // HUMANHEAD pdm + float mbTime = damageDef->GetFloat( "mb_time" ); + float severity = damageDef->GetFloat( "mb_amplitude" ); + if ( mbTime ) { + idVec3 blurDirection; + blurDirection.y = localKickDir[0]; // forward/back kick will blur vertically + blurDirection.x = localKickDir[1]; // side kick will blur horizontally + blurDirection.y += localKickDir[2]; // up/down kick will add to vertical + MotionBlur(mbTime, severity, blurDirection); + } + + // + // head angle kick + // + float kickTime = damageDef->GetFloat( "kick_time" ); + if ( kickTime ) { + kickFinishTime = gameLocal.time + g_kickTime.GetFloat() * kickTime; + + // forward / back kick will pitch view + kickAngles[0] = localKickDir[0]; + + // side kick will yaw view + kickAngles[1] = localKickDir[1]*0.5f; + + // up / down kick will pitch view + kickAngles[0] += localKickDir[2]; + + // roll will come from side + kickAngles[2] = localKickDir[1]; + + float kickAmplitude = damageDef->GetFloat( "kick_amplitude" ); + if ( kickAmplitude ) { + kickAngles *= kickAmplitude; + } + } + + // + // HUMANHEAD: Screen blobs, changed functionality + // + float blobTime = damageDef->GetFloat( "blob_time" ); + if ( blobTime ) { + screenBlob_t *blob = GetScreenBlob(); + blob->startFadeTime = gameLocal.time; + blob->finishTime = gameLocal.time + blobTime * g_blobTime.GetFloat(); + + const char *materialName = damageDef->GetString( "mtr_blob" ); + blob->material = declManager->FindMaterial( materialName ); + + // Scale blob by 100% +/- blob_devscale% + float scale = ( 256.0f + ( 256.0f*damageDef->GetFloat("blob_devscale")*gameLocal.random.CRandomFloat() ) ) / 256.0f; + blob->w = damageDef->GetFloat( "blob_width" ) * g_blobSize.GetFloat() * scale; + blob->h = damageDef->GetFloat( "blob_height" ) * g_blobSize.GetFloat() * scale; + blob->s1 = 0; + blob->t1 = 0; + blob->s2 = 1; + blob->t2 = 1; + + if (damageDef->GetBool("blob_projected") && player->renderView) { + //rww - renderView null check added. I am not clear on when this may be null, but it apparently was. however, if a player were + //to take damage before thinking, it seems very possible this would occur. the renderView will not be initialized until after + //the first think. + // Project hit location onto screen + idVec3 localDamageLocation = hhUtils::ProjectOntoScreen(lastDamageLocation, *player->renderView); + blob->x = localDamageLocation.x - blob->w * 0.5f; + blob->y = localDamageLocation.y - blob->h * 0.5f; + } + else if (damageDef->GetBool("blob_directional")) { + // Directional blobs to show where damage is coming from (360 degree) + float dirDist = damageDef->GetFloat("blob_dirdistance"); + blob->x = 320.0f + (dirDist * 320.0f * localKickDir.y) - (blob->w * 0.5f); + blob->y = 240.0f + (dirDist * 240.0f * localKickDir.x) - (blob->h * 0.5f); + } + else { + // Place blob centered at (blob_x,blob_y) + blob->x = damageDef->GetFloat( "blob_x" ) - blob->w * 0.5f; + blob->y = damageDef->GetFloat( "blob_y" ) - blob->h * 0.5f; + } + + // Deviate blob +/- (blob_devx,blob_devy) + blob->x += damageDef->GetFloat( "blob_devx" ) * gameLocal.random.CRandomFloat(); + blob->y += damageDef->GetFloat( "blob_devy" ) * gameLocal.random.CRandomFloat(); + } + } + + + // + // Global directional damage system + // +/* + const int directionDamageTime = 1000; + screenBlob_t *blob = GetScreenBlob(); + blob->startFadeTime = gameLocal.time; + blob->finishTime = gameLocal.time + directionDamageTime; + + blob->s1 = 0; + blob->t1 = 0; + blob->s2 = 1; + blob->t2 = 1; + if (idMath::Fabs(localKickDir[0]) >= idMath::Fabs(localKickDir[1])) { + // More in X direction + blob->material = dirDmgFrontMaterial; + blob->w = 500.0f; + blob->h = 80.0f; + blob->x = 320.0f - blob->w * 0.5f; + if (localKickDir[0] >= 0.0f) { + // From Rear + blob->y = 480.0f - blob->h; + idSwap(blob->t1, blob->t2); + } + else { + // From Front + blob->y = 0.0f; + } + } + else { + // More in Y direction + blob->material = dirDmgLeftMaterial; + blob->w = 80.0f; + blob->h = 400.0f; + blob->y = 240.0f - blob->h * 0.5f; + if (localKickDir[1] >= 0.0f) { + // From Right + blob->x = 640.0f - blob->w; + idSwap(blob->s1, blob->s2); + } + else { + // From Left + blob->x = 0.0f; + } + } +*/ + + // + // save lastDamageTime for tunnel vision accentuation + // + lastDamageTime = MS2SEC( gameLocal.time ); + +} + +//------------------------------------------------------ +// WeaponFireFeedback +// HUMANHEAD bjk +//------------------------------------------------------ +void hhPlayerView::WeaponFireFeedback( const idDict *weaponDef ) { + idAngles angles; + idVec2 pitch, yaw, viewSpring; + weaponDef->GetVec2( "recoilPitch", "0 0", pitch ); + weaponDef->GetVec2( "recoilYaw", "0 0", yaw ); + weaponDef->GetVec2( "viewSpring", "150 11", viewSpring ); + + player->kickSpring = viewSpring.x; + player->kickDamping = viewSpring.y; + + pitch.x = pitch.x + gameLocal.random.RandomFloat()*(pitch.y - pitch.x); + yaw.x = yaw.x + gameLocal.random.RandomFloat()*(yaw.y - yaw.x); + angles=idAngles(-pitch.x,-yaw.x,0); + kickSpeed+=angles; + assert(!FLOAT_IS_NAN(kickSpeed.pitch) && !FLOAT_IS_NAN(kickSpeed.yaw) && !FLOAT_IS_NAN(kickSpeed.roll)); +} + +//------------------------------------------------------ +// AngleOffset +// HUMANHEAD bjk +//------------------------------------------------------ +idAngles hhPlayerView::AngleOffset(float kickSpring, float kickDamping) { + //HUMANHEAD rww - for other clients, do not add angle offset, it isn't predicted well + if (gameLocal.isMultiplayer && !gameLocal.isServer && gameLocal.localClientNum != -1 && player && gameLocal.localClientNum != player->entityNumber) { + kickSpeed.Zero(); + kickAngles.Zero(); + return kickAngles; + } + //HUMANHEAD END + + float frametime = (gameLocal.time - kickLastTime); + kickLastTime = gameLocal.time; + //kickAngles = kickAngles*(1-offset*kickSpeed)+kickBlendTo*offset*kickSpeed; + //kickBlendTo = kickBlendTo*(1-offset*kickReturnSpeed)+ang*offset*kickReturnSpeed; + + //HUMANHEAD PCF rww 04/27/06 - these values can get unreasonable at times +#if 0 + for (int i = 0; i < 3; i++) { + if (fabsf(kickSpeed[i]) > 9999.0f) { + kickSpeed[i] = 0.0f; + } + if (fabsf(kickAngles[i]) > 9999.0f) { + kickAngles[i] = 0.0f; + } + } +#endif + if (frametime > 48.0f) { + frametime = 48.0f; + } + //HUMANHEAD END + + assert(!FLOAT_IS_NAN(kickSpeed.pitch) && !FLOAT_IS_NAN(kickSpeed.yaw) && !FLOAT_IS_NAN(kickSpeed.roll)); + assert(!FLOAT_IS_NAN(kickAngles.pitch) && !FLOAT_IS_NAN(kickAngles.yaw) && !FLOAT_IS_NAN(kickAngles.roll)); + + kickSpeed-=kickDamping*kickSpeed*frametime/1000; + kickSpeed-=kickSpring*kickAngles*frametime/1000; + + if (gameLocal.isNewFrame) { //HUMANHEAD rww + kickAngles+=kickSpeed*frametime/1000; + } + + return kickAngles; +} + +//------------------------------------------------------ +// SingleView +//------------------------------------------------------ +void hhPlayerView::SingleView( idUserInterface *hud, const renderView_t *view ) { + // normal rendering + + if ( !view ) { + return; + } + + // place the sound origin for the player + gameSoundWorld->PlaceListener( view->vieworg, view->viewaxis, player->entityNumber + 1, gameLocal.time, hud ? hud->State().GetString( "location" ) : "Undefined" ); + + // hack the shake in at the very last moment, so it can't cause any consistency problems + renderView_t hackedView = *view; + hackedView.viewaxis = hackedView.viewaxis * ShakeAxis(); + + gameRenderWorld->RenderScene( &hackedView ); + + if ( player->spectating ) { + return; + } + + // draw screen blobs + if ( !pm_thirdPerson.GetBool() && !g_skipViewEffects.GetBool() ) { + for ( int i = 0 ; i < MAX_SCREEN_BLOBS ; i++ ) { + screenBlob_t *blob = &screenBlobs[i]; + if ( blob->finishTime <= gameLocal.time ) { + continue; + } + + blob->y += blob->driftAmount; + + float fade = (float)( blob->finishTime - gameLocal.time ) / ( blob->finishTime - blob->startFadeTime ); + if ( fade > 1.0f ) { + fade = 1.0f; + } + if ( fade ) { + renderSystem->SetColor4( 1,1,1,fade ); + renderSystem->DrawStretchPic( blob->x, blob->y, blob->w, blob->h, blob->s1, blob->t1, blob->s2, blob->t2, blob->material ); + } + } + + // HUMANHEAD: CJR + if ( voTotalTime != -1 ) { // Check if the viewOverlay should time out + if ( gameLocal.time >= voFinishTime ) { + voTotalTime = -1; + voRequiresScratchBuffer = false; + viewOverlayMaterial = NULL; // Remove the overlay + } + } + + //HUMANHEAD: aob + if( viewOverlayMaterial ) { + renderSystem->SetColor4( viewOverlayColor[0], viewOverlayColor[1], viewOverlayColor[2], viewOverlayColor[3] ); + renderSystem->DrawStretchPic(0, 0, 640, 480, 0, 1, 1, 0, viewOverlayMaterial); + } + //HUMANHEAD END + + if ( player->health > 0 && player->health < 25 && static_cast(player)->IsSpiritOrDeathwalking()==false ) { + hurtValue += 0.05f*(player->health - hurtValue); + if( player->health > 30 ) + hurtValue = player->health; + + renderSystem->SetColor4( hurtValue/25.0f, 1, 1, 1 ); + renderSystem->DrawStretchPic(0, 0, 640, 480, 0, 1, 1, 0, hurtMaterial); + } + } + + // If this level has a sun corona, then attempt to draw it - CJR + if ( !g_skipViewEffects.GetBool() && gameLocal.GetSunCorona() ) { + gameLocal.GetSunCorona()->Draw( (hhPlayer *)player ); + } + + // test a single material drawn over everything + if ( g_testPostProcess.GetString()[0] ) { + const idMaterial *mtr = declManager->FindMaterial( g_testPostProcess.GetString(), false ); + if ( !mtr ) { + common->Printf( "Material not found.\n" ); + g_testPostProcess.SetString( "" ); + } else { + renderSystem->SetColor4( 1.0f, 1.0f, 1.0f, 1.0f ); + renderSystem->DrawStretchPic( 0.0f, 0.0f, 640.0f, 480.0f, 0.0f, 0.0f, 1.0f, 1.0f, mtr ); + } + } +} + +// Hermite() +// Hermite Interpolator +// Returns an alpha value [0..1] based on Hermite Parameters N1, N2, S1, S2 and an input alpha 't' +float Hermite(float t, float N1, float N2, float S1, float S2) { + float tSquared = t*t; + float tCubed = tSquared*t; + return (2*tCubed - 3*tSquared + 1)*N1 + + (-2*tCubed + 3*tSquared)*N2 + + (tCubed - 2*tSquared + t)*S1 + + (tCubed - tSquared)*S2; +} + + +//------------------------------------------------------ +// MotionBlurVision +// HUMANHEAD pdm +//------------------------------------------------------ + +void hhPlayerView::MotionBlurVision(idUserInterface *hud, const renderView_t *view) { + if ( !g_doubleVision.GetBool() ) { + SingleView( hud, view ); + return; + } + + float alpha; + const float N1 = 0.2f; + const float N2 = 0.0f; + const float S1 = 1.0f; + const float S2 = 1.0f; + + float remainingTime = mbFinishTime - gameLocal.time; + float elapsedTime = mbTotalTime - remainingTime; + float scale = remainingTime / mbTotalTime; + + // Render to a texture + RENDER_DEMO_VIEWRENDER(view, this); //HUMANHEAD rww + renderSystem->CropRenderSize( 512, 256, true ); + SingleView( hud, view ); + renderSystem->CaptureRenderToImage( "_scratch" ); + renderSystem->UnCrop(); + RENDER_DEMO_VIEWRENDER_END(); //HUMANHEAD rww + + // Motion blur + float xshift, yshift; + for (int index=0; index1?1:(a)) + renderSystem->SetColor4( 1,1,1, index==0 ? 1.0f : 0.2f ); + renderSystem->DrawStretchPic( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, + CLIP(xshift), CLIP(1+yshift), CLIP(1+xshift), CLIP(yshift), scratchMaterial ); // clipped + } +} + +//------------------------------------------------------ +// SpiritVision +// HUMANHEAD cjr +//------------------------------------------------------ + +void hhPlayerView::SpiritVision( idUserInterface *hud, const renderView_t *view ) { + int oldTime = voTotalTime; + const idMaterial *oldMaterial = viewOverlayMaterial; + + voTotalTime = -1; + viewOverlayMaterial = NULL; + + SingleView( hud, view ); + + // Draw the spiritwalk image over the top + if ( player && !spiritMaterial ) { + spiritMaterial = declManager->FindMaterial( player->spawnArgs.GetString( "mtr_spiritwalk" ) ); + } + + renderSystem->SetColor4( 1, 1, 1, 1 ); + renderSystem->DrawStretchPic(0, 0, 640, 480, 0, 1, 1, 0, spiritMaterial ); + + voTotalTime = oldTime; + viewOverlayMaterial = oldMaterial; +} + +//------------------------------------------------------ +// ApplyLetterbox +// HUMANHEAD pdm +//------------------------------------------------------ +void hhPlayerView::ApplyLetterBox(const renderView_t *view) { + if (bLetterBox) { + renderSystem->SetColor4( 1.0f, 1.0f, 1.0f, 1.0f ); + renderSystem->DrawStretchPic(0, 0, 640, LETTERBOX_HEIGHT_TOP, 0, 0, 1, 1, letterboxMaterial); + renderSystem->DrawStretchPic(0, 480-LETTERBOX_HEIGHT_BOTTOM, 640, LETTERBOX_HEIGHT_BOTTOM, 0, 0, 1, 1, letterboxMaterial); + } +} + +//------------------------------------------------------ +// MotionBlur +// HUMANHEAD pdm +//------------------------------------------------------ +void hhPlayerView::MotionBlur(int mbTime, float severity, idVec3 &direction) { + mbTotalTime = mbTime; + mbFinishTime = gameLocal.time + mbTotalTime; + mbAmplitude = severity; + mbDirection = direction; +} + +//------------------------------------------------------ +// SetLetterBox +// HUMANHEAD pdm +//------------------------------------------------------ +void hhPlayerView::SetLetterBox(bool on) { + bLetterBox = on; +} + +//------------------------------------------------------ +// RenderPlayerView +//------------------------------------------------------ +void hhPlayerView::RenderPlayerView( idUserInterface *hud ) { + const renderView_t *view = player->GetRenderView(); + + if ( g_skipViewEffects.GetBool() ) { + SingleView( hud, view ); + } else { + if (gameLocal.time < mbFinishTime) { + MotionBlurVision(hud, view); + } else if ( ((hhPlayer *)player)->IsSpiritWalking() ) { + SpiritVision( hud, view ); + } else { + SingleView(hud, view); + } + ScreenFade(); + + // HUMANHEAD pdm: letterbox + ApplyLetterBox(view); + } + + // HUMANHEAD: Draw the HUD after all over overlay effects. + if (hud) { + hud->SetStateBool("letterbox", bLetterBox); + player->DrawHUD( hud ); + } + + if ( net_clientLagOMeter.GetBool() && lagoMaterial && gameLocal.isClient ) { + renderSystem->SetColor4( 1.0f, 1.0f, 1.0f, 1.0f ); + renderSystem->DrawStretchPic( 10.0f, 380.0f, 64.0f, 64.0f, 0.0f, 0.0f, 1.0f, 1.0f, lagoMaterial ); + } +} diff --git a/src/Prey/game_playerview.h b/src/Prey/game_playerview.h new file mode 100644 index 0000000..8067193 --- /dev/null +++ b/src/Prey/game_playerview.h @@ -0,0 +1,108 @@ +#ifndef __PREY_GAME_PLAYERVIEW_H__ +#define __PREY_GAME_PLAYERVIEW_H__ + +class hhPlayerView : public idPlayerView { +public: + hhPlayerView(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void ClearEffects( void ); + virtual void DamageImpulse( idVec3 localKickDir, const idDict *damageDef ); + virtual void RenderPlayerView( idUserInterface *hud ); + + void WeaponFireFeedback( const idDict *weaponDef ); // HUMANHEAD bjk + idAngles AngleOffset( float kickSpeed, float kickReturnSpeed ); // HUMANHEAD bjk + + // HUMANHEAD + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + void MotionBlur(int mbTime, float severity, idVec3 &direction); + idVec3 ViewOffset() { return viewOffset; }//HUMANHEAD: aob + void SetViewOverlayMaterial( const idMaterial* material, int scratchBuffer = false ); + void SetViewOverlayColor( idVec4 color ); // HUMANHEAD cjr + void SetViewOverlayTime( int time, int scratchBuffer = false ); + void SetLetterBox(bool on); + void SetDamageLoc(const idVec3 &damageLoc); + // HUMANHEAD END + +protected: + virtual void SingleView(idUserInterface *hud, const renderView_t *view); + + // HUMANHEAD pdm + void MotionBlurVision(idUserInterface *hud, const renderView_t *view); + void ApplyLetterBox(const renderView_t *view); + void SpiritVision( idUserInterface *hud, const renderView_t *view ); + +protected: + bool bLetterBox; // HUMANHEAD pdm: whether we are in letterbox mode + float mbAmplitude; + int mbFinishTime; + int mbTotalTime; + idVec3 mbDirection; + int kickLastTime; // HUMANHEAD bjk + idAngles kickSpeed; // HUMANHEAD bjk + idVec3 viewOffset; // HUMANHEAD: aob + const idMaterial * viewOverlayMaterial; // HUMANHEAD: aob + idVec4 viewOverlayColor; // HUMANHEAD cjr: Used to pass colors/parms to the view overlay + int voFinishTime; // HUMANHEAD cjr: Used to set the time that the view overlay stays on + int voTotalTime; // HUMANHEAD cjr: Used to set the time that the view overlay stays on + int voRequiresScratchBuffer;// HUMANHEAD cjr: requires the screen rendered to the scratch buffer + const idMaterial *letterboxMaterial; // HUMANHEAD pdm + const idMaterial *dirDmgLeftMaterial; // HUMANHEAD pdm + const idMaterial *dirDmgFrontMaterial; // HUMANHEAD pdm + const idMaterial *spiritMaterial; // HUMANHEAD cjr + idVec3 lastDamageLocation; // HUMANHEAD PDM: saved damage location + float hurtValue; // HUMANHEAD bjk: to smooth the health +}; + +//HUMANHEAD rww - render demo madness +#if _HH_RENDERDEMO_HACKS + void RENDER_DEMO_VIEWRENDER(const renderView_t *view, const hhPlayerView *pView = NULL); + void RENDER_DEMO_VIEWRENDER_END(void); +#else + #define RENDER_DEMO_VIEWRENDER(a, b)//ignore + #define RENDER_DEMO_VIEWRENDER_END() //ignore +#endif +//HUMANHEAD END + + +//------------------------------------------------------ +// hhPlayerView::SetViewOverlayMaterial +// +// HUMANHEAD: aob +//------------------------------------------------------ +ID_INLINE void hhPlayerView::SetViewOverlayMaterial( const idMaterial* material, int scratchBuffer ) { + viewOverlayMaterial = material; + + voTotalTime = -1; + voFinishTime = gameLocal.time + voTotalTime; + voRequiresScratchBuffer = scratchBuffer; +} + +//------------------------------------------------------ +// hhPlayerView::SetViewOverlayColor +// +// HUMANHEAD: cjr +//------------------------------------------------------ +ID_INLINE void hhPlayerView::SetViewOverlayColor( idVec4 color ) { + viewOverlayColor = color; +} + +//------------------------------------------------------ +// hhPlayerView::SetViewOverlayTime +// +// HUMANHEAD: cjr +// +// Time can be set in SetViewOverlayMaterial +// or set/updated with this function call +//------------------------------------------------------ +ID_INLINE void hhPlayerView::SetViewOverlayTime( int time, int scratchBuffer ) { + voTotalTime = time; + voFinishTime = gameLocal.time + voTotalTime; + voRequiresScratchBuffer = scratchBuffer; +} + +#endif /* __PREY_GAME_PLAYERVIEW_H__ */ + diff --git a/src/Prey/game_pod.cpp b/src/Prey/game_pod.cpp new file mode 100644 index 0000000..94904f1 --- /dev/null +++ b/src/Prey/game_pod.cpp @@ -0,0 +1,157 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// +// hhPod +// +// type of mine that can be spawned from a pod spawner +//========================================================================== + +CLASS_DECLARATION(hhMine, hhPod) +END_CLASS + +hhPod::hhPod() { + bMoverThink = false; + additionalAxis.Identity(); +} + +void hhPod::Spawn(void) { + // Uses clipModel because it is a moveable, which requires it + + // Turn off collision with corpses + int oldMask = GetPhysics()->GetClipMask(); + GetPhysics()->SetClipMask( oldMask & (~CONTENTS_CORPSE) ); + + // Rolling support + radius = (GetPhysics()->GetBounds()[1][0] - GetPhysics()->GetBounds()[0][0]) * 0.5f; + lastOrigin = GetPhysics()->GetOrigin(); + additionalAxis.Identity(); + BecomeActive(TH_THINK); +} + +void hhPod::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( radius ); + savefile->WriteVec3( lastOrigin ); + savefile->WriteMat3( additionalAxis ); + savefile->WriteBool( bMoverThink ); +} + +void hhPod::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( radius ); + savefile->ReadVec3( lastOrigin ); + savefile->ReadMat3( additionalAxis ); + savefile->ReadBool( bMoverThink ); +} + +bool hhPod::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + StartSound( "snd_pain", SND_CHANNEL_ANY ); + + float scale = 1.5f - (0.5f * health / spawnArgs.GetFloat("health")); + scale = idMath::ClampFloat(1.0f, 1.5f, scale); + SetShaderParm(SHADERPARM_ANY_DEFORM_PARM2, scale); + + return( idEntity::Pain(inflictor, attacker, damage, dir, location) ); +} + +void hhPod::Release() { + idVec3 initLinearVelocity, initAngularVelocity; + spawnArgs.GetVector( "init_velocity", "0 0 0", initLinearVelocity ); + spawnArgs.GetVector( "init_avelocity", "0 0 0", initAngularVelocity ); + + // Make the pod physics kick in + physicsObj.EnableImpact(); + physicsObj.Activate(); + physicsObj.SetLinearVelocity( initLinearVelocity ); + physicsObj.SetAngularVelocity( initAngularVelocity ); + + // Start deforming again (disabled on pod spawners) + float parm1 = spawnArgs.GetFloat("deformParm1"); + float parm2 = spawnArgs.GetFloat("deformParm2"); + SetDeformation(DEFORMTYPE_POD, parm1, parm2); +} + + +bool hhPod::Collide( const trace_t &collision, const idVec3 &velocity ) { + AttemptToPlayBounceSound( collision, velocity ); + + return hhMine::Collide( collision, velocity ); +} + + +void hhPod::RollThink( void ) { + float movedDistance, angle; + idVec3 curOrigin, gravityNormal, dir; + + bool wasAtRest = IsAtRest(); + + RunPhysics(); + + // only need to give the visual model an additional rotation if the physics were run + if ( !wasAtRest ) { + + // current physics state + curOrigin = GetPhysics()->GetOrigin(); + + dir = curOrigin - lastOrigin; + float movedDistanceSquared = dir.LengthSqr(); + + // if the pod moved + if ( movedDistanceSquared > 0.0f && movedDistanceSquared < 100.0f) { + + gravityNormal = GetPhysics()->GetGravityNormal(); + + // movement since last frame + movedDistance = idMath::Sqrt( movedDistanceSquared ); + dir *= 1.0f / movedDistance; + + // Get local coordinate axes + idVec3 right = -dir.Cross(gravityNormal); + + // Rotate about it proportional to the distance moved using axis/angle + angle = 180.0f * movedDistance / (radius*idMath::PI); + additionalAxis *= (idRotation( vec3_origin, right, angle).ToMat3()); + } + + // save state for next think + lastOrigin = curOrigin; + } + + Present(); +} + +void hhPod::Event_HoverTo( const idVec3 &position ) { + bMoverThink = true; + hhMine::Event_HoverTo( position ); +} + +void hhPod::Event_Unhover() { + bDetonateOnCollision = true; + bMoverThink = false; + hhMine::Event_Unhover(); +} + +void hhPod::Think() { + if ( bMoverThink ) { + hhMoveable::Think(); + } else { + RollThink(); + } +} + +void hhPod::ClientPredictionThink() { + if ( bMoverThink ) { + hhMoveable::Think(); + } else { + RollThink(); + } +} + +bool hhPod::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + origin = vec3_origin; + axis = additionalAxis * GetPhysics()->GetAxis().Inverse(); + return true; +} + diff --git a/src/Prey/game_pod.h b/src/Prey/game_pod.h new file mode 100644 index 0000000..e8813e2 --- /dev/null +++ b/src/Prey/game_pod.h @@ -0,0 +1,34 @@ + +#ifndef __GAME_POD_H__ +#define __GAME_POD_H__ + +class hhPod : public hhMine { +public: + CLASS_PROTOTYPE( hhPod ); + + hhPod(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual bool Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + void Release(); + virtual bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); + virtual void Think( void ); + virtual void ClientPredictionThink( void ); + + virtual bool Collide( const trace_t &collision, const idVec3 &velocity ); + virtual void Event_HoverTo( const idVec3 &position ); + virtual void Event_Unhover(); +protected: + void RollThink( void ); + +private: + float radius; // radius of barrel + idVec3 lastOrigin; // origin of the barrel the last frame + idMat3 additionalAxis; // transformation for visual model + bool bMoverThink; +}; + + +#endif /* __GAME_POD_H__ */ + diff --git a/src/Prey/game_podspawner.cpp b/src/Prey/game_podspawner.cpp new file mode 100644 index 0000000..8038c40 --- /dev/null +++ b/src/Prey/game_podspawner.cpp @@ -0,0 +1,141 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// +// hhPodSpawner +// +// type of mine spawner that animates and spawns pods +//========================================================================== + +const idEventDef EV_DropPod("", NULL); +const idEventDef EV_DoneSpawning("", NULL); + +CLASS_DECLARATION(hhMineSpawner, hhPodSpawner) + EVENT( EV_DropPod, hhPodSpawner::Event_DropPod) + EVENT( EV_PlayIdle, hhPodSpawner::Event_PlayIdle) + EVENT( EV_DoneSpawning, hhPodSpawner::Event_DoneSpawning) +END_CLASS + + +void hhPodSpawner::Spawn(void) { + fl.takedamage = false; + GetPhysics()->SetContents( CONTENTS_BODY ); + + pod = NULL; + spawning = false; + idleAnim = GetAnimator()->GetAnim("idle"); + painAnim = GetAnimator()->GetAnim("pain"); + spawnAnim = GetAnimator()->GetAnim("spawn"); + + PostEventMS(&EV_PlayIdle, 0); +} + +void hhPodSpawner::Save(idSaveGame *savefile) const { + savefile->WriteInt( idleAnim ); + savefile->WriteInt( painAnim ); + savefile->WriteInt( spawnAnim ); + savefile->WriteBool( spawning ); + savefile->WriteObject(pod); +} + +void hhPodSpawner::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( idleAnim ); + savefile->ReadInt( painAnim ); + savefile->ReadInt( spawnAnim ); + savefile->ReadBool( spawning ); + savefile->ReadObject( reinterpret_cast(pod) ); +} + +void hhPodSpawner::SpawnMine() { + idVec3 offset; + + if (spawning || health < 0 || fl.isDormant) { + return; + } + + population++; + spawning = true; + + offset = idVec3(0, 0, -64); // Move up to top + + idDict args; + args.Clear(); + args.Set( "origin", (GetPhysics()->GetOrigin() + offset).ToString() ); + args.Set( "nodrop", "1" ); // Don't put on the floor, wait for release + args.Set( "deformType", "0" ); // Don't start deforming until released + + // pass along any spawn keys + const idKeyValue *arg = spawnArgs.MatchPrefix("spawn_"); + while( arg ) { + args.Set( arg->GetKey().Right( arg->GetKey().Length() - 6 ), arg->GetValue() ); + arg = spawnArgs.MatchPrefix( "spawn_", arg ); + } + + // spawn a pod + pod = static_cast(gameLocal.SpawnObject(spawnArgs.GetString("def_pod"), &args)); + + // attach to bone + const char *podBone = "PodPosition"; + pod->MoveToJoint(this, podBone); + pod->AlignToJoint(this, podBone); + pod->BindToJoint(this, podBone, true); + pod->fl.takedamage = false; // No damage while attached + pod->SetSpawner(this); + + // Due to the animation, bone isn't in position yet, so wait a little + // before making it visible + pod->Hide(); + pod->PostEventMS(&EV_Show, 500); + + // start animation + if (spawnAnim) { + GetAnimator()->ClearAllAnims(gameLocal.time, 0); + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, spawnAnim, gameLocal.time, 500); + int opentime = GetAnimator()->GetAnim( spawnAnim )->Length(); + PostEventMS( &EV_PlayIdle, opentime ); + + //TODO: Move this to a frame command (event) + PostEventMS( &EV_DropPod, 3700 ); + + PostEventMS( &EV_DoneSpawning, opentime ); + + StartSound( "snd_spawn", SND_CHANNEL_ANY ); + } +} + +void hhPodSpawner::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + // Don't actually take damage, but give feedback + + // Play pain + if (painAnim) { + GetAnimator()->ClearAllAnims(gameLocal.time, 0); + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, painAnim, gameLocal.time, 500); + int opentime = GetAnimator()->GetAnim( painAnim )->Length(); + PostEventMS( &EV_PlayIdle, opentime ); + StartSound( "snd_pain", SND_CHANNEL_ANY ); + } +} + +void hhPodSpawner::Event_PlayIdle() { + if (idleAnim) { + GetAnimator()->ClearAllAnims(gameLocal.time, 0); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleAnim, gameLocal.time, 0); + } +} + +void hhPodSpawner::Event_DropPod() { + if (pod) { + pod->Unbind(); + pod->fl.takedamage = true; + pod->Release(); + } +} + +void hhPodSpawner::Event_DoneSpawning() { + spawning = false; + CheckPopulation(); +} + diff --git a/src/Prey/game_podspawner.h b/src/Prey/game_podspawner.h new file mode 100644 index 0000000..c182585 --- /dev/null +++ b/src/Prey/game_podspawner.h @@ -0,0 +1,31 @@ + +#ifndef __GAME_PODSPAWNER_H__ +#define __GAME_PODSPAWNER_H__ + +class hhPodSpawner : public hhMineSpawner { +public: + CLASS_PROTOTYPE( hhPodSpawner ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + void SpawnMine(); + +protected: + void Event_PlayIdle(); + void Event_DropPod(); + void Event_DoneSpawning(); + +private: + int idleAnim; + int painAnim; + int spawnAnim; + + bool spawning; + hhPod *pod; +}; + + +#endif /* __GAME_PODSPAWNER_H__ */ diff --git a/src/Prey/game_poker.cpp b/src/Prey/game_poker.cpp new file mode 100644 index 0000000..f4d8c67 --- /dev/null +++ b/src/Prey/game_poker.cpp @@ -0,0 +1,807 @@ +// game_poker.cpp +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// Static declaration of our hands array +const hand_t localhands[NUM_POKER_HANDS] = { + { HAND_ROYALFLUSH, 5000, PREQ_ROYALS|PREQ_FLUSH|PREQ_STRAIGHT }, + { HAND_STRAIGHTFLUSH, 1000, PREQ_FLUSH|PREQ_STRAIGHT }, + { HAND_FOUROFKIND, 100, PREQ_4MATCH }, + { HAND_FULLHOUSE, 50, PREQ_FULLHOUSE }, + { HAND_FLUSH, 20, PREQ_FLUSH }, + { HAND_STRAIGHT, 10, PREQ_STRAIGHT }, + { HAND_THREEOFKIND, 5, PREQ_3MATCH }, + { HAND_TWOPAIR, 3, PREQ_2PAIR }, + // Jacks or better is hardcoded at 2 + { HAND_PAIR, 1, PREQ_2MATCH }, + { HAND_NOTHING, 0, 0 } +}; + +// Assign reference to our local array to our static class +const hand_t * hhPoker::hands = localhands; + + +//============================================================== +// hhPokerHand utility class +//============================================================== + +CLASS_DECLARATION(idClass, hhPokerHand) +END_CLASS + +hhPokerHand::hhPokerHand() { + Clear(); +} + +void hhPokerHand::Save(idSaveGame *savefile) const { + savefile->Write(values, sizeof(suitHist_t)*NUM_SUITS); + savefile->Write(suits, sizeof(valueHist_t)*NUM_CARD_VALUES); + + savefile->WriteInt(cards.Num()); // idList + for (int i=0; iWrite(&cards[i], sizeof(hhCard)); + } +} + +void hhPokerHand::Restore( idRestoreGame *savefile ) { + int i, num; + hhCard card; + + savefile->Read(values, sizeof(suitHist_t)*NUM_SUITS); + savefile->Read(suits, sizeof(valueHist_t)*NUM_CARD_VALUES); + + cards.Clear(); + savefile->ReadInt( num ); + cards.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->Read(&card, sizeof(hhCard)); + cards[i] = card; + } +} + +void hhPokerHand::operator=(hhPokerHand &other) { + Clear(); + for (int ix=0; ix maxcount) { + maxcount = values[ix].count; + } + } + return maxcount; +} + +int hhPokerHand::ValueOfMaxCount() { + int maxcount = 0; + int maxcountvalue = -1; + for (int ix=0; ix maxcount) { + maxcount = values[ix].count; + maxcountvalue = ix; + } + } + return maxcountvalue; +} + +int hhPokerHand::GetLowValue() { + int first = -1; + for (int ix=0; ix 0) { + first = ix; + break; + } + } + return first; +} + +int hhPokerHand::GetHighValue() { + int last = -1; + for (int ix=0; ix 0) { + last = ix; + } + } + return last; +} + +bool hhPokerHand::HasValue(int value) { + return values[value].count > 0; +} + +int hhPokerHand::NumUniqueValues() { + int numvalues = 0; + for (int ix=0; ix 0) { + numvalues++; + } + } + return numvalues; +} + +int hhPokerHand::NumOfValue(int value) { + assert(value>=0 && value 0) { + count++; + } + } + return count; +} + +int hhPokerHand::SuitOfMaxCount() { + int maxcount = 0; + int maxsuit = -1; + for (int ix=0; ix maxcount) { + maxcount = suits[ix].count; + maxsuit = ix; + } + } + return maxsuit; +} + +bool hhPokerHand::Search(hhCard &card) { + for (int ix=0; ix", NULL); + +CLASS_DECLARATION(hhConsole, hhPoker) + EVENT( EV_Deal, hhPoker::Event_Deal) + EVENT( EV_Draw, hhPoker::Event_Draw) + EVENT( EV_UpdateView, hhPoker::Event_UpdateView) +END_CLASS + +void hhPoker::Spawn() { + + Reset(); +} + +void hhPoker::Reset() { + bCanDeal = true; + bCanIncBet = true; + bCanDecBet = true; + bCanDraw = false; + bGameOver = false; + Bet = PlayerBet = 1; + memset(markedCards, 0, sizeof(markedCards)); + victoryAmount = spawnArgs.GetInt("victory"); + PlayerCredits = spawnArgs.GetInt("credits"); + currentHandIndex = -1; + creditsWon = 0; + + bCanMark1 = bCanMark2 = bCanMark3 = bCanMark4 = bCanMark5 = false; + PlayerHand.Clear(); + + UpdateView(); +} + +void hhPoker::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject(deck); + savefile->WriteStaticObject(PlayerHand); + savefile->Write(markedCards, sizeof(bool)*5); + savefile->WriteInt(Bet); + savefile->WriteInt(PlayerBet); + savefile->WriteInt(PlayerCredits); + savefile->WriteInt(victoryAmount); + savefile->WriteInt(currentHandIndex); + + savefile->WriteBool( bGameOver ); + savefile->WriteBool( bCanDeal ); + savefile->WriteBool( bCanIncBet ); + savefile->WriteBool( bCanDecBet ); + savefile->WriteBool( bCanDraw ); + savefile->WriteBool( bCanMark1 ); + savefile->WriteBool( bCanMark2 ); + savefile->WriteBool( bCanMark3 ); + savefile->WriteBool( bCanMark4 ); + savefile->WriteBool( bCanMark5 ); + savefile->WriteInt( creditsWon ); +} + +void hhPoker::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject(deck); + savefile->ReadStaticObject(PlayerHand); + savefile->Read(markedCards, sizeof(bool)*5); + savefile->ReadInt(Bet); + savefile->ReadInt(PlayerBet); + savefile->ReadInt(PlayerCredits); + savefile->ReadInt(victoryAmount); + savefile->ReadInt(currentHandIndex); + + savefile->ReadBool( bGameOver ); + savefile->ReadBool( bCanDeal ); + savefile->ReadBool( bCanIncBet ); + savefile->ReadBool( bCanDecBet ); + savefile->ReadBool( bCanDraw ); + savefile->ReadBool( bCanMark1 ); + savefile->ReadBool( bCanMark2 ); + savefile->ReadBool( bCanMark3 ); + savefile->ReadBool( bCanMark4 ); + savefile->ReadBool( bCanMark5 ); + savefile->ReadInt( creditsWon ); +} + +void hhPoker::Deal() { + + // Shuffle + deck.Generate(); + deck.Shuffle(); + + Bet = PlayerBet; + PlayerCredits -= Bet; + creditsWon = 0; + + // Deal initial hand + PlayerHand.Clear(); + PlayerHand.AddCard(deck.GetCard()); + PlayerHand.AddCard(deck.GetCard()); + PlayerHand.AddCard(deck.GetCard()); + PlayerHand.AddCard(deck.GetCard()); + PlayerHand.AddCard(deck.GetCard()); + + EvaluateHand(false); + + bCanDeal = bCanIncBet = bCanDecBet = false; + bCanDraw = bCanMark1 = bCanMark2 = bCanMark3 = bCanMark4 = bCanMark5 = true; + for (int ix=0; ix<5; ix++) { + markedCards[ix] = true; + } + + UpdateView(); +} + +void hhPoker::Draw() { + bCanDeal = bCanIncBet = bCanDecBet = true; + bCanDraw = bCanMark1 = bCanMark2 = bCanMark3 = bCanMark4 = bCanMark5 = false; + + float luck = idMath::ClampFloat(0.0f, 1.0f, spawnArgs.GetFloat("luck")); + if (luck) { + BestHand(luck); + } + else { + // Replace marked cards + for (int ix=0; ix<5; ix++) { + if (markedCards[ix]) { + PlayerHand.cards[ix] = deck.GetCard(); + markedCards[ix] = false; + } + } + hhPokerHand temp; + temp = PlayerHand; + PlayerHand = temp; // Rebuild player hand for correct stats + } + + EvaluateHand(true); + UpdateView(); +} + +void hhPoker::Mark(int card) { + markedCards[card] ^= 1; + UpdateView(); +} + +void hhPoker::IncBet() { + + int amount = 1; + idUserInterface *gui = renderEntity.gui[0]; + if (gui) { + amount = gui->GetStateInt("increment"); + } + + if (bCanIncBet) { + int oldBet = PlayerBet; + PlayerBet = idMath::ClampInt(PlayerBet, PlayerCredits, PlayerBet+amount); + PlayerBet = idMath::ClampInt(0, 999999, PlayerBet); + if (PlayerBet != oldBet) { + StartSound( "snd_betchange", SND_CHANNEL_ANY ); + } + } + UpdateView(); +} + +void hhPoker::DecBet() { + int amount = 1; + idUserInterface *gui = renderEntity.gui[0]; + if (gui) { + amount = gui->GetStateInt("increment"); + } + + if (bCanDecBet) { + int oldBet = PlayerBet; + if (PlayerBet > amount) { + PlayerBet -= amount; + } + else if (PlayerBet > 1) { + PlayerBet = 1; + } + if (PlayerBet != oldBet) { + StartSound( "snd_betchange", SND_CHANNEL_ANY ); + } + } + UpdateView(); +} + + +/* + hhPoker utility functions +*/ + +void hhPoker::EvaluateHand(bool score) { + int ix; + currentHandIndex = -1; + + int requirements = 0; + int low = PlayerHand.GetLowValue(); + int high = PlayerHand.GetHighValue(); + int maxcount = PlayerHand.MaxCountOfValues(); + + if (low >= CARD_TEN) { + requirements |= PREQ_ROYALS; + } + + if (PlayerHand.NumUniqueSuits() == 1) { + requirements |= PREQ_FLUSH; + } + + // Check for straight + if (maxcount==1) { + requirements |= PREQ_STRAIGHT; + int cur = low; + for (ix=1; ix<5; ix++) { + int next = cur+1; + if (low == CARD_DEUCE && next == CARD_SIX && PlayerHand.HasValue(CARD_ACE)) { + // If looking for a '6', accept an 'Ace' also for Ace low straights + } + else if (!PlayerHand.HasValue(next)) { + requirements &= ~PREQ_STRAIGHT; + break; + } + cur = next; + } + } + + // Check for Pairs, etc. + for (ix=0; ix= CARD_JACK) { + payoff = 2; + } + + creditsWon = Bet * payoff; + PlayerCredits += creditsWon; + PlayerCredits = idMath::ClampInt(0, 999999999, PlayerCredits); + if (PlayerCredits < PlayerBet) { + PlayerBet = PlayerCredits; + } + + if (PlayerCredits <= 0) { + bCanIncBet = bCanDecBet = bCanDraw = bCanDeal = bCanMark1 = bCanMark2 = bCanMark3 = bCanMark4 = bCanMark5 = false; + bGameOver = true; + } + + if (victoryAmount && PlayerCredits >= victoryAmount) { + StartSound( "snd_victory", SND_CHANNEL_ANY ); + ActivateTargets( gameLocal.GetLocalPlayer() ); + victoryAmount = 0; + } + else if (payoff > 10) { + StartSound( "snd_winbig", SND_CHANNEL_ANY ); + } + else if (payoff > 0) { + StartSound( "snd_win", SND_CHANNEL_ANY ); + } + else { + StartSound( "snd_lose", SND_CHANNEL_ANY ); + } + } +} + +void hhPoker::UpdateView() { + // UpdateView() is posted as an event because sometimes we're already nested down in the gui handling code when it is called + // and it in turn re-enters the gui handling code with HandleNamedEvent() + CancelEvents(&EV_UpdateView); + PostEventMS(&EV_UpdateView, 0); +} + +void hhPoker::Event_UpdateView() { + int ix; + idUserInterface *gui = renderEntity.gui[0]; + + if (gui) { + + bool atLeastOneCardKept = !(markedCards[0] && markedCards[1] && markedCards[2] && markedCards[3] && markedCards[4]); + + gui->SetStateBool("bgameover", bGameOver); + gui->SetStateBool("bcanincbet", bCanIncBet); + gui->SetStateBool("bcandecbet", bCanDecBet); + gui->SetStateBool("bcandraw", bCanDraw);// && atLeastOneCardKept); + gui->SetStateBool("bcandeal", bCanDeal); + gui->SetStateBool("bcanmark1", bCanMark1); + gui->SetStateBool("bcanmark2", bCanMark2); + gui->SetStateBool("bcanmark3", bCanMark3); + gui->SetStateBool("bcanmark4", bCanMark4); + gui->SetStateBool("bcanmark5", bCanMark5); + gui->SetStateInt("currentbet", PlayerBet); + gui->SetStateInt("credits", PlayerCredits); + gui->SetStateInt("hand", currentHandIndex); + gui->SetStateInt("creditswon", creditsWon); + + for (ix=0; ix<5; ix++) { + gui->SetStateInt(va("Player%d_Visible", ix+1), 0); + } + + // Show player hand + for (ix=0; ixSetStateInt(va("Player%d_Visible", ix+1), 1); + gui->SetStateInt(va("Player%d_Suit", ix+1), PlayerHand.cards[ix].Suit()); + char cardChar = PlayerHand.cards[ix].ValueName(); + if (cardChar == 'T') { + gui->SetStateString(va("Player%d_Card", ix+1), "10"); + } + else { + gui->SetStateString(va("Player%d_Card", ix+1), va("%c", cardChar)); + } + gui->SetStateInt(va("Player%d_Red", ix+1), PlayerHand.cards[ix].Suit()==SUIT_HEARTS || PlayerHand.cards[ix].Suit()==SUIT_DIAMONDS ? 1 : 0); + gui->SetStateString(va("Player%d_Mark", ix+1), markedCards[ix] ? "X" : ""); + gui->SetStateBool(va("Player%d_Marked", ix+1), markedCards[ix] ); + } + + gui->StateChanged(gameLocal.time, true); + CallNamedEvent("Update"); + } +} + +bool hhPoker::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + + idToken token; + + if (!src->ReadToken(&token)) { + return false; + } + + if (token == ";") { + return false; + } + + if (token.Icmp("deal") == 0) { + Deal(); + } + else if (token.Icmp("draw") == 0) { + Draw(); + } + else if (token.Icmp("mark1") == 0) { + Mark(0); + } + else if (token.Icmp("mark2") == 0) { + Mark(1); + } + else if (token.Icmp("mark3") == 0) { + Mark(2); + } + else if (token.Icmp("mark4") == 0) { + Mark(3); + } + else if (token.Icmp("mark5") == 0) { + Mark(4); + } + else if (token.Icmp("incbet") == 0) { + IncBet(); + } + else if (token.Icmp("decbet") == 0) { + DecBet(); + } + else if (token.Icmp("reset") == 0) { + Reset(); + } + else if (token.Icmp("restart") == 0) { + bCanDeal = bCanIncBet = bCanDecBet = true; + bCanDraw = bCanMark1 = bCanMark2 = bCanMark3 = bCanMark4 = bCanMark5 = false; + PlayerCredits = spawnArgs.GetInt("credits"); + bGameOver = false; + Bet = PlayerBet = 1; + UpdateView(); + } + else { + src->UnreadToken(&token); + return false; + } + + return true; +} + +void hhPoker::Event_Deal() { + Deal(); +} + +void hhPoker::Event_Draw() { + Draw(); +} + +// Experiment: get best hand given discards +void hhPoker::BestHand(float probability) { + int ix; + + hhPokerHand hand; + hhPokerHand discards; + hhPokerHand originalHand; + + hand.Clear(); + originalHand.Clear(); + discards.Clear(); + + for (ix=0; ix<5; ix++) { + if (markedCards[ix]) { + discards.AddCard(PlayerHand.cards[ix]); + } + else { + hand.AddCard(PlayerHand.cards[ix]); + originalHand.AddCard(PlayerHand.cards[ix]); + } + } + + int possibilities = 0xFF; + + // Check for possibilities of requirements using hands + for (ix=0; ix 1) { + possibilities &= ~PREQ_STRAIGHT; + } + if (first != -1 && last != -1 && last-first>=5) { + if (hand.HasValue(CARD_ACE)) { // Extra check to allow for ace low straight + last = first; + first = -1; // put ace at begining, find new last and try again + for (ix=0; ix 0) { + last = ix; + } + } + if (last-first>=5) { + possibilities &= ~PREQ_STRAIGHT; + } + } + else { + possibilities &= ~PREQ_STRAIGHT; + } + } + + // Check for full house possibility + if (hand.NumUniqueValues() > 2) { + possibilities &= ~PREQ_FULLHOUSE; + possibilities &= ~PREQ_2PAIR; + } + + // check for 'x of a kind' possibility + if (maxcount + discards.cards.Num() < 4) { + possibilities &= ~PREQ_4MATCH; + } + if (maxcount + discards.cards.Num() < 3) { + possibilities &= ~PREQ_3MATCH; + } + if (maxcount + discards.cards.Num() < 2) { + possibilities &= ~PREQ_2MATCH; + } + + // Go through hands in decending order of greatness looking for possibilities + for (ix=0; ixCARD_TEN ? CARD_TEN : first; // Start at ten or lower so it will fit + } + while (hand.HasValue(value)) { + value++; + } + } + + int hotvalue; + if (requirements&PREQ_FULLHOUSE) { + hotvalue = hand.ValueOfMaxCount(); + if (hotvalue != -1) { + if (hand.NumOfValue(hotvalue) < 3) { + value = hotvalue; + } + else { + value = (hotvalue==first) ? last : first; + } + } + } + + if (requirements&PREQ_2PAIR) { + if (hand.NumOfValue(first) < 2) { + value = first; + } + else if (hand.NumOfValue(last) < 2) { + value = last; + } + else { + value = CARD_ACE; + while (hand.HasValue(value)) { + value--; + } + } + } + + if ((hand.MaxCountOfValues()<4) && (requirements&PREQ_4MATCH)) { + value = hand.ValueOfMaxCount(); + } + if ((hand.MaxCountOfValues()<3) && (requirements&PREQ_3MATCH)) { + value = hand.ValueOfMaxCount(); + } + if ((hand.MaxCountOfValues()<2) && (requirements&PREQ_2MATCH)) { + value = hand.ValueOfMaxCount(); + } + + // Determine suit properties + if (requirements&PREQ_FLUSH) { + suit = hand.SuitOfMaxCount(); + if (suit == -1) { + suit = gameLocal.random.RandomInt() % NUM_SUITS; // random fixed suit + } + if (value == -1) { // Need a value + value = CARD_ACE; + while (!deck.HasCard(hhCard(value, suit))) { + value--; + } + } + } + else if (value != -1) { + // Choose a suit different from held values + suit = 0; + while ( !deck.HasCard(hhCard(value, suit)) ) { + if (++suit >= NUM_SUITS) { + break; + } + } + if (suit >= NUM_SUITS) { // Can't make our hand - set back to a used card and it will break out to next possibility below + suit = SUIT_DIAMONDS; + } + } + + // Add card to hand + if (value == -1) { // This means any card is okay, give one from deck + hand.AddCard(deck.GetCard()); + } + else if (deck.HasCard(hhCard(value,suit))) { + hand.AddCard(deck.GetCard(value, suit)); + } + else { + for (int jx=originalHand.cards.Num(); jx probability) { + hand.cards[ix] = deck.GetCard(); + bHandCompromised = true; // only compromise hand with one card + } + } + + // Copy into playerhand + hhPokerHand temp; + int drawcard = originalHand.cards.Num(); + for (ix=0; ix<5; ix++) { + if (markedCards[ix]) { + PlayerHand.cards[ix] = hand.cards[drawcard++]; + markedCards[ix] = false; + } + temp.AddCard(PlayerHand.cards[ix]); + } + PlayerHand.Clear(); // Rebuild player hand for correct stats + for (ix=0; ix<5; ix++) { + PlayerHand.AddCard(temp.cards[ix]); + } +} + diff --git a/src/Prey/game_poker.h b/src/Prey/game_poker.h new file mode 100644 index 0000000..4ba7d98 --- /dev/null +++ b/src/Prey/game_poker.h @@ -0,0 +1,123 @@ + +#ifndef __GAME_POKER_H__ +#define __GAME_POKER_H__ + +extern const idEventDef EV_UpdateView; + +#define PREQ_FLUSH 0x00000001 +#define PREQ_STRAIGHT 0x00000002 +#define PREQ_ROYALS 0x00000004 +#define PREQ_4MATCH 0x00000008 +#define PREQ_3MATCH 0x00000010 +#define PREQ_2MATCH 0x00000020 +#define PREQ_FULLHOUSE 0x00000040 +#define PREQ_2PAIR 0x00000080 + +typedef enum pokerhand_e { + HAND_ROYALFLUSH=0, + HAND_STRAIGHTFLUSH, + HAND_FOUROFKIND, + HAND_FULLHOUSE, + HAND_FLUSH, + HAND_STRAIGHT, + HAND_THREEOFKIND, + HAND_TWOPAIR, + HAND_PAIR, + HAND_NOTHING, + NUM_POKER_HANDS +}pokerhand_t; + +typedef struct hand_s { + pokerhand_t hand; + int payoff; + int requirement; +} hand_t; + + +class hhPokerHand : public idClass { + CLASS_PROTOTYPE( hhPokerHand ); + +public: + typedef struct valueHist_s { + int count; + } valueHist_t; + + typedef struct suitHist_s { + int count; + } suitHist_t; + + hhPokerHand(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Clear(); + int MaxCountOfValues(); + int ValueOfMaxCount(); + int GetLowValue(); + int GetHighValue(); + bool HasValue(int value); + int NumOfValue(int value); + int SuitOfMaxCount(); + int NumUniqueValues(); + int NumUniqueSuits(); + void AddCard(hhCard card); + bool Search(hhCard &card); + void operator=(hhPokerHand &other); + +public: + suitHist_t suits[NUM_SUITS]; + valueHist_t values[NUM_CARD_VALUES]; + idList cards; + +}; + +class hhPoker : public hhConsole { + CLASS_PROTOTYPE( hhPoker ); +public: + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void Deal(); + void Draw(); + void Mark(int card); + void IncBet(); + void DecBet(); + void BestHand(float probability); + void Reset(); + + void EvaluateHand(bool score); + void UpdateView(); + bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + +protected: + void Event_Deal(); + void Event_Draw(); + void Event_UpdateView(); + +private: + static const hand_t *hands; + hhDeck deck; + hhPokerHand PlayerHand; + + bool markedCards[5]; + int Bet; + int PlayerBet; + int PlayerCredits; + int victoryAmount; + int currentHandIndex; + int creditsWon; + + bool bGameOver; + bool bCanDeal; + bool bCanIncBet; + bool bCanDecBet; + bool bCanDraw; + bool bCanMark1; + bool bCanMark2; + bool bCanMark3; + bool bCanMark4; + bool bCanMark5; +}; + +#endif /* __GAME_POKER_H__ */ diff --git a/src/Prey/game_portal.cpp b/src/Prey/game_portal.cpp new file mode 100644 index 0000000..d66a30e --- /dev/null +++ b/src/Prey/game_portal.cpp @@ -0,0 +1,1164 @@ +//************************************************************************** +//** +//** GAME_PORTAL.CPP +//** +//** Game code for Prey-specific portals +//** +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define MP_PORTAL_RANGE_DEFAULT 356.0f //rww - was 512, and then it was 256, and now it is 356 +#define MAX_PORTAL_BOUNDS 256.0f //used for unchanging bounds in mp + +//========================================================================== +// +// hhArtificialPortal +// +//========================================================================== + +const idEventDef EV_SetGamePortalState("setPortalState", "dd"); + +#if GAMEPORTAL_PVS + +CLASS_DECLARATION(idEntity, hhArtificialPortal) + EVENT( EV_SetGamePortalState, hhArtificialPortal::Event_SetPortalState ) +END_CLASS + +void hhArtificialPortal::Spawn() { + areaPortal = gameRenderWorld->FindGamePortal( GetName() ); + if (areaPortal && !gameLocal.isClient) { + SetPortalState(spawnArgs.GetBool("startOpenPVS"), spawnArgs.GetBool("startOpenSound")); + } + fl.networkSync = true; +} + +void hhArtificialPortal::SetPortalState(bool openPVS, bool openSound) { + if (areaPortal && !gameLocal.isClient) { + int blockMask = (openPVS ? PS_BLOCK_NONE : PS_BLOCK_VIEW) | (openSound ? PS_BLOCK_NONE : PS_BLOCK_SOUND); + gameLocal.SetPortalState( areaPortal, blockMask ); + } +} + +void hhArtificialPortal::Event_SetPortalState(bool openPVS, bool openSound) { + SetPortalState(openPVS, openSound); +} + +void hhArtificialPortal::Save(idSaveGame *savefile) const { + savefile->WriteInt(areaPortal); + if ( areaPortal ) { + savefile->WriteInt( gameRenderWorld->GetPortalState( areaPortal ) ); + } +} + +void hhArtificialPortal::Restore(idRestoreGame *savefile) { + savefile->ReadInt(areaPortal); + if ( areaPortal ) { + int portalState; + savefile->ReadInt( portalState ); + gameLocal.SetPortalState( areaPortal, portalState ); + } +} + + +#endif + + +//========================================================================== +// +// hhPortal +// +//========================================================================== + +const idEventDef EV_Opened("", NULL); +const idEventDef EV_Closed("", NULL); +const idEventDef EV_PortalSpark("", NULL ); +const idEventDef EV_PortalSparkEnd("", NULL ); +const idEventDef EV_ShowGlowPortal( "showGlowPortal", NULL ); +const idEventDef EV_HideGlowPortal( "hideGlowPortal", NULL ); + +CLASS_DECLARATION(hhAnimatedEntity, hhPortal) + EVENT( EV_PostSpawn, hhPortal::PostSpawn ) + EVENT( EV_Activate, hhPortal::Event_Trigger) + EVENT( EV_Opened, hhPortal::Event_Opened ) + EVENT( EV_Closed, hhPortal::Event_Closed ) + EVENT( EV_ResetGravity, hhPortal::Event_ResetGravity ) + EVENT( EV_PortalSpark, hhPortal::Event_PortalSpark ) + EVENT( EV_PortalSparkEnd, hhPortal::Event_PortalSparkEnd ) + EVENT( EV_ShowGlowPortal, hhPortal::Event_ShowGlowPortal ) + EVENT( EV_HideGlowPortal, hhPortal::Event_HideGlowPortal ) +END_CLASS + +hhPortal::hhPortal(void) { + areaPortal = 0; +} + +hhPortal::~hhPortal() { + proximityEntities.Clear(); // Clear the list of potential entities to be portalled + SAFE_REMOVE(m_portalIdleFx); + +#if GAMEPORTAL_PVS + if (areaPortal && !gameLocal.isClient) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_ALL ); + } +#endif +} + +void hhPortal::Spawn(void) { + idBounds bounds; + + fl.clientEvents = true; + + bNoTeleport = spawnArgs.GetBool( "noTeleport"); + bGlowPortal = spawnArgs.GetBool( "glowPortal"); // This is a glow portal, and so will have fx, sound, particles, etc + closeDelay = spawnArgs.GetFloat( "closeDelay"); + monsterportal = spawnArgs.GetBool( "monsterportal" ); + distanceToggle = spawnArgs.GetFloat( "distanceToggle" ); + if (gameLocal.isMultiplayer && bGlowPortal/* && distanceToggle == 0.0f*/) { //mp has a default shutoff distance for glow portals. + //now ignoring distance toggle keys in mp, and forcing it. + distanceToggle = MP_PORTAL_RANGE_DEFAULT; + } + if (!spawnArgs.GetFloat( "distanceCull", "0.0", distanceCull )) { //rww - distance to shut off the vis gameportal but not the render portal + //if not value specified, default to shaderParm5+4 + if (!renderEntity.shaderParms[5]) { + distanceCull = 0.0f; + } + else { + distanceCull = renderEntity.shaderParms[5]+4.0f; + } + } + areaPortalCulling = false; + if ( !gameLocal.isMultiplayer && bGlowPortal ) { + alertMonsters = spawnArgs.GetBool( "alertMonsters", "0" ); + } else { + alertMonsters = false; + } + + if ( bNoTeleport ) { + GetPhysics()->SetContents( 0 ); + } else { + GetPhysics()->SetContents( CONTENTS_SOLID ); + } + + // Setup the initial state for the portal + if(spawnArgs.GetBool("startActive")) { // Start Active + portalState = PORTAL_OPENED; + + if ( bGlowPortal ) { + int anim = GetAnimator()->GetAnim("opened"); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, 0 ); + StartSound( "snd_loop", SND_CHANNEL_ANY ); + PostEventSec( &EV_PortalSpark, gameLocal.random.RandomFloat() ); + SetSkinByName( spawnArgs.GetString( "skin" ) ); + } + } else { // Start closed + Hide(); + portalState = PORTAL_CLOSED; + GetPhysics()->SetContents( 0 ); //rww - if closed, ensure things will not hit and go through + } + +#if GAMEPORTAL_PVS + const char *gamePortalName = spawnArgs.GetString("gamePortalName", GetName()); + areaPortal = gameRenderWorld->FindGamePortal( gamePortalName ); + if (areaPortal && !gameLocal.isClient) { + gameLocal.SetPortalState( areaPortal, portalState == PORTAL_CLOSED ? PS_BLOCK_ALL : PS_BLOCK_NONE ); + } +#endif + + // Default to normal gravity. This could be changed by any zones the portal is within + SetGravity( gameLocal.GetGravity() ); + + BecomeActive( TH_THINK | TH_UPDATEVISUALS | TH_ANIMATE ); + + UpdateVisuals(); + + PostEventMS( &EV_PostSpawn, 0 ); + + fl.networkSync = true; //rww + + if (gameLocal.isMultiplayer) { //rww - if portals grab their renderEntity bounds from the animation, they can cause client-server pvs discrepancies + renderEntity.bounds[0] = idVec3(-MAX_PORTAL_BOUNDS, -MAX_PORTAL_BOUNDS, -MAX_PORTAL_BOUNDS); + renderEntity.bounds[1] = idVec3(MAX_PORTAL_BOUNDS, MAX_PORTAL_BOUNDS, MAX_PORTAL_BOUNDS); + } + + proximityEntities.Clear(); // Clear the list of potential entities to be portalled +} + + +void hhPortal::PostSpawn( void ) { + CheckForBuddy(); +} + +void hhPortal::Save(idSaveGame *savefile) const { +#if GAMEPORTAL_PVS + savefile->WriteInt( areaPortal ); + if ( areaPortal ) { + savefile->WriteInt( gameRenderWorld->GetPortalState( areaPortal ) ); + } +#endif + savefile->WriteInt( portalState ); + savefile->WriteBool( bNoTeleport ); + savefile->WriteBool( bGlowPortal ); + savefile->WriteVec3( portalGravity ); + savefile->WriteFloat( closeDelay ); + savefile->WriteBool( monsterportal ); + savefile->WriteBool( alertMonsters ); + + savefile->WriteInt( slavePortals.Num() ); // idList > + for (int i=0; iWriteFloat(distanceToggle); + savefile->WriteFloat(distanceCull); + savefile->WriteBool(areaPortalCulling); + + savefile->WriteInt( proximityEntities.Num() ); + for( int i = 0; i < proximityEntities.Num(); i++ ) { + proximityEntities[i].entity.Save( savefile ); + savefile->WriteVec3( proximityEntities[i].lastPortalPoint ); + } + + m_portalIdleFx.Save(savefile); +} + +void hhPortal::Restore( idRestoreGame *savefile ) { + int i, num; + +#if GAMEPORTAL_PVS + savefile->ReadInt( areaPortal ); + if ( areaPortal ) { + int portalState; + savefile->ReadInt( portalState ); + gameLocal.SetPortalState( areaPortal, portalState ); + } +#endif + savefile->ReadInt( (int &)portalState ); + savefile->ReadBool( bNoTeleport ); + savefile->ReadBool( bGlowPortal ); + savefile->ReadVec3( portalGravity ); + savefile->ReadFloat( closeDelay ); + savefile->ReadBool( monsterportal ); + savefile->ReadBool( alertMonsters ); + + slavePortals.Clear(); + savefile->ReadInt( num ); // idList > + slavePortals.SetNum( num ); + for (i=0; iReadFloat(distanceToggle); + savefile->ReadFloat(distanceCull); + savefile->ReadBool(areaPortalCulling); + + savefile->ReadInt( num ); + proximityEntities.SetNum( num ); + for( i = 0; i < num; i++ ) { + proximityEntities[i].entity.Restore( savefile ); + savefile->ReadVec3( proximityEntities[i].lastPortalPoint ); + } + + m_portalIdleFx.Restore(savefile); +} + +void hhPortal::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteFloat(portalGravity.x); + msg.WriteFloat(portalGravity.y); + msg.WriteFloat(portalGravity.z); + msg.WriteBits(masterPortal.GetSpawnId(), 32); + + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_MODE]); + msg.WriteFloat(renderEntity.shaderParms[SHADERPARM_TIMEOFFSET]); + + msg.WriteBits(portalState, 4); +} + +void hhPortal::ReadFromSnapshot( const idBitMsgDelta &msg ) { + portalGravity.x = msg.ReadFloat(); + portalGravity.y = msg.ReadFloat(); + portalGravity.z = msg.ReadFloat(); + masterPortal.SetSpawnId(msg.ReadBits(32)); + + renderEntity.shaderParms[SHADERPARM_MODE] = msg.ReadFloat(); + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] = msg.ReadFloat(); + + portalStates_t newPortalState = (portalStates_t)msg.ReadBits(4); + if ((newPortalState == PORTAL_CLOSED || newPortalState == PORTAL_OPENED) && + newPortalState != portalState && portalState != PORTAL_CLOSING && portalState != PORTAL_OPENING) { + Trigger(this); + } +} + +void hhPortal::ClientPredictionThink( void ) { + Think(); +} + +void hhPortal::CheckPlayerDistances(void) { + float closest = distanceToggle+distanceCull; + hhPortal *targetPortal = NULL; + + if (cameraTarget && cameraTarget->IsType(hhPortal::Type)) { //we want to measure distance from the target portal too if we have one + targetPortal = static_cast(cameraTarget); + } + + for (int i = 0; i < gameLocal.numClients; i++) { //loop through any active clients and get the closest distance to one. + idEntity *ent = gameLocal.entities[i]; + if (ent && ent->IsType(hhPlayer::Type)) { + hhPlayer *pl = static_cast(ent); + + if (pl->health > 0 && !pl->spectating && !pl->InVehicle()) { //don't open for spectators or dead players or players in vehicles + float d = (pl->GetOrigin()-GetOrigin()).Length(); + if (d < closest) { + closest = d; + } + if (targetPortal) { //check distance from the target portal + d = (pl->GetOrigin()-targetPortal->GetOrigin()).Length(); + if (d < closest) { + closest = d; + } + } + } + } + } + + if (distanceToggle != 0.0f) { //toggling portal based on distance of player + if (closest < distanceToggle) { //should be open + if (portalState == PORTAL_CLOSED) { + Trigger(this); + } + } + else { //should be closed + if (portalState == PORTAL_OPENED) { + Trigger(this); + } + } + } + + if (distanceCull != 0.0f) { //toggling only area portal based on distance of player + if (closest >= distanceCull) { //should be closed + if (!areaPortalCulling) { + areaPortalCulling = true; + if (!gameLocal.isClient) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_ALL ); + } + } + } + else if (areaPortalCulling && (portalState == PORTAL_OPENING || portalState == PORTAL_OPENED)) { //otherwise make sure it's on (if the portal is open) + if (!gameLocal.isClient) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_NONE ); + } + areaPortalCulling = false; + } + } +} + + +//========================================================================== +// +// hhPortal::Think +// +//========================================================================== + +#define NEAR_CLIP 0 //6.5 + +void hhPortal::Think( void ) { + int i; + idEntity *hit; + idPlane plane; + + if ((distanceCull != 0.0f || distanceToggle != 0.0f) && areaPortal) { //check to turn the areaportal on and off based on distance. + if (!gameLocal.isClient) { + CheckPlayerDistances(); + } + else { //since this is not sync'd, and we don't need to sync it, do an extra check here (for mp) + if (portalState == PORTAL_OPENED && renderEntity.customSkin && renderEntity.customSkin == declManager->FindSkin(spawnArgs.GetString( "skin_onlyWarp" ))) { + SetSkinByName( spawnArgs.GetString( "skin" ) ); + } + } + + if (distanceToggle != 0.0f) { //for pop-open portals, check fx state + if ((portalState == PORTAL_CLOSED || portalState == PORTAL_CLOSING) && bGlowPortal) { + //rww - broadcast fx while closed + if (!m_portalIdleFx.IsValid()) { + const char *portalIdleFx = spawnArgs.GetString("fx_idleclosed", "fx/portal_closed_idle"); + if (portalIdleFx[0]) { + hhFxInfo fxInfo; + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone(false); + + m_portalIdleFx = SpawnFxLocal(portalIdleFx, GetOrigin(), GetAxis(), &fxInfo, true); + if (!m_portalIdleFx.IsValid()) { //spawn failure? + gameLocal.Warning("hhPortal::Think: portal could not spawn fx for fx_idleclosed (%s).", portalIdleFx); + } + } + } + + if (m_portalIdleFx.IsValid() && !m_portalIdleFx->IsActive(TH_THINK)) { + m_portalIdleFx->Nozzle(true); + } + } + else if (m_portalIdleFx.IsValid() && m_portalIdleFx->IsActive(TH_THINK)) { //if it's not closed/closing and has fx, then stop them + m_portalIdleFx->Nozzle(false); + } + } + } + + hhAnimatedEntity::Think(); + + if (gameLocal.isMultiplayer) { //rww - if portals grab their renderEntity bounds from the animation, they can cause client-server pvs discrepancies + renderEntity.bounds[0] = idVec3(-MAX_PORTAL_BOUNDS, -MAX_PORTAL_BOUNDS, -MAX_PORTAL_BOUNDS); + renderEntity.bounds[1] = idVec3(MAX_PORTAL_BOUNDS, MAX_PORTAL_BOUNDS, MAX_PORTAL_BOUNDS); + } + + if( portalState == PORTAL_CLOSED || bNoTeleport ) { + return; + } + + // Force visuals to update until a remote renderview has been created for this portal + if ( !renderEntity.remoteRenderView ) { + BecomeActive( TH_UPDATEVISUALS ); + } + + // Bit of a hack for noclipping players: If they are close to the portal, then add them to the proximity list automatically + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( player && player->noclip ) { //rww - note that the local player is NULL for dedicated servers. + if ( (player->GetOrigin() - GetOrigin()).LengthFast() < 256.0f ) { + AddProximityEntity( player ); + } + } + + // Build a plane for the portal surface + plane.SetNormal(GetPhysics()->GetAxis()[0]); + plane.FitThroughPoint( GetPhysics()->GetOrigin() + plane.Normal() * NEAR_CLIP ); + + idVec3 origin = GetOrigin(); + idMat3 axis = GetAxis(); + + for ( i = 0; i < proximityEntities.Num(); i++ ) { + if ( !proximityEntities[i].entity.IsValid() ) { + // Remove this entity from the list + proximityEntities.RemoveIndex( i ); + continue; + } + + hit = proximityEntities[i].entity.GetEntity(); + idVec3 location = proximityEntities[i].lastPortalPoint; + idVec3 nextLocation = hit->GetPortalPoint(); + proximityEntities[i].lastPortalPoint = nextLocation; + if ( !AttemptPortal( plane, hit, location, nextLocation ) ) { + proximityEntities.RemoveIndex( i ); + + // If the entity is a player, then inform the player that they are no longer close to a portal + if ( hit->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast(hit); + player->SetPortalColliding( false ); + } + } + } +} + +bool hhPortal::AttemptPortal( idPlane &plane, idEntity *hit, idVec3 location, idVec3 nextLocation ) { + + // Don't try to portal self + if( hit == this ) { + return false; + } + + if ( hit->IsBound() ) { // Do not portal bound objects -- let the master portalling handle bound entities + return false; + } + + // Don't allow idMovers to portal. TODO: Restrict other entities? + if ( hit->IsType( idMover::Type ) || hit->IsType(hhVehicle::Type) ) { //rww - do not allow shuttles either + return false; + } + + if (hit->IsType(hhPlayer::Type)) { //rww - don't portal dead players + hhPlayer *pl = static_cast(hit); + if (pl->health <= 0) { + return false; + } + + // Check if the player is intersecting this portal. If not, then don't try to portal it + if ( !GetPhysics()->GetAbsBounds().IntersectsBounds( pl->GetPhysics()->GetAbsBounds() ) ) { + return false; + } + } + + int side = plane.Side( location ); + if ( side == PLANESIDE_ON || side == PLANESIDE_CROSS ) { + side = PLANESIDE_BACK; + } + + int nextSide = plane.Side( nextLocation ); + if ( nextSide == PLANESIDE_ON || nextSide == PLANESIDE_CROSS ) { + nextSide = PLANESIDE_BACK; + } + + if( side == PLANESIDE_BACK ) { // On the backside, remove this entity from the list + return false; + } else if ( side == nextSide ) { // Entirely on one side + // Check if the entity is too far from the plane and remove it from the list + if ( !GetPhysics()->GetAbsBounds().IntersectsBounds( hit->GetPhysics()->GetAbsBounds() ) ) { + return false; + } + + return true; + } + + // Compute the location on the plane where the entity would hit + float scale; + idVec3 dir = nextLocation - location; + plane.RayIntersection( location, dir, scale ); + + // Portal the entity + PortalEntity( hit, location + dir * scale * 1.01f ); + + // Add this entity to the destination portal's proximityEntity list + if (cameraTarget && cameraTarget->IsType(hhPortal::Type)) { + hhPortal *targetPortal = static_cast(cameraTarget); + targetPortal->CollideWithPortal( hit ); // CJR PCF 04/26/06: Previously CheckPortal + } + + return false; +} + +void hhPortal::PortalProjectile( hhProjectile *projectile, idVec3 collideLocation, idVec3 nextLocation ) { + idPlane plane; + plane.SetNormal(GetPhysics()->GetAxis()[0]); + plane.FitThroughPoint( GetPhysics()->GetOrigin() ); + + AttemptPortal( plane, projectile, collideLocation, nextLocation ); +} + +//========================================================================== +//========================================================================== +void hhPortal::CheckForBuddy() { + const char * buddyName; + idEntity * foundEntity; + hhPortal* buddy; + + // Check for a buddy portal name + buddyName = spawnArgs.GetString( "partner", NULL ); + if ( buddyName ) { + gameLocal.Warning( "Portal %s has 'partner' key. Please change this to 'buddy'", GetName() ); + } + else { + buddyName = spawnArgs.GetString( "buddy" ); + } + if ( !buddyName || !buddyName[ 0 ] ) { + return; + } + + // Get the actual entity + foundEntity = gameLocal.FindEntity( buddyName ); + if ( !foundEntity ) { + return; + } + + if ( foundEntity->IsType( hhPortal::Type ) ) { + + buddy = (hhPortal *) foundEntity; + + if (buddy->spawnArgs.FindKey("buddy")) { + gameLocal.Error( "Portals %s and %s both have 'buddy' key set. Use only on master", GetName(), buddy->GetName() ); + return; + } + + // Don't link up to buddy if a monster portal, just points to portal room + if ( !monsterportal || closeDelay == 0.0f ) { + //? OK, let's be lazy, assume only 2 portals are buddied. Make us the master + buddy->SetMasterPortal( this ); + //! Set the cameraTargets of each, then update each + buddy->spawnArgs.Set( "cameraTarget", name.c_str() ); + buddy->ProcessEvent( &EV_UpdateCameraTarget ); + } + + AddSlavePortal( buddy ); + } + + spawnArgs.Set( "cameraTarget", buddyName ); + ProcessEvent( &EV_UpdateCameraTarget ); +} + + +//============================= +// hhPortal::TriggerTargets +//============================= +void hhPortal::TriggerTargets() { + const char * key = ""; + idEntity * entity = NULL; + idList< idStr > entityNames; + + + // Find the right key based on our state + if ( portalState == PORTAL_OPENING ) { + key = "targetOpening"; + } + else if ( portalState == PORTAL_OPENED ) { + key = "targetOpened"; + } + else if ( portalState == PORTAL_CLOSING ) { + key = "targetClosing"; + } + else if ( portalState == PORTAL_CLOSED ) { + key = "targetClosed"; + } + + //gameLocal.Printf( "Keying %s\n", key ); + + // Loop through the entities, spawning each one + hhUtils::GetValues( spawnArgs, key, entityNames, true ); + + for ( int i = 0; i < entityNames.Num(); ++i ) { + //gameLocal.Printf( "Gonna trigger %s\n", entityNames[ i ].c_str() ); + entity = gameLocal.FindEntity( entityNames[ i ] ); + if ( entity ) { + entity->PostEventMS( &EV_Activate, 0, this ); + } + } +} + + +//========================================================================== +// +// hhPortal::CheckPortal +// +// If this entity can be portaled, typically called from the low-level physics clip functions +// +// CJR PCF 04/26/06: Changed this function to separate CheckPortal and CollideWithPortal +//========================================================================== + +bool hhPortal::CheckPortal( const idEntity *other, int contentMask ) { + if ( contentMask & CONTENTS_GAME_PORTAL ) { // Check if the other entity should clip against the portal + return false; + } + + if ( !other ) { + return true; + } + + if ( other->fl.noPortal ) { + return false; // Do not allow this entity to portal, make it collide with the portal instead + } + + return true; +} + +bool hhPortal::CheckPortal( const idClipModel *mdl, int contentMask ) { + if ( contentMask & CONTENTS_GAME_PORTAL ) { // Check if the other entity should clip against the portal + return false; + } + + if ( !mdl ) { + return true; + } + + return CheckPortal( mdl->GetEntity(), contentMask ); +} + +//========================================================================== +// +// hhPortal::CollideWithPortal +// +// Called from low-level clip functions, actually collide the entity with the portal +// +// CJR PCF 04/26/06: Formerly part of CheckPortal +//========================================================================== + +void hhPortal::CollideWithPortal( const idEntity *other ) { + if ( !other ) { + return; + } + + if ( other->IsType( hhProjectile::Type ) ) { // Projectiles move so fast that they should get portaled next time they think + // Only add this projectile to the collided list if it hits the front side of the portal + idPlane plane; + plane.SetNormal(GetPhysics()->GetAxis()[0]); + plane.FitThroughPoint( GetPhysics()->GetOrigin() ); + + int side = plane.Side( other->GetOrigin() ); + if ( side == PLANESIDE_FRONT ) { // It's on the front, so add this portal to the list + hhProjectile *projectile = (hhProjectile *)(other); + + // Check if the projectile will actually impact the portal + idVec3 end = projectile->GetOrigin() + projectile->GetPhysics()->GetLinearVelocity(); + + if ( plane.LineIntersection( projectile->GetOrigin(), end ) ) { // Projectile collides with the portal + projectile->SetCollidedPortal( this, projectile->GetPortalPoint(), projectile->GetPhysics()->GetLinearVelocity() ); + } + } + } else { + AddProximityEntity( other ); + } +} + +void hhPortal::CollideWithPortal( const idClipModel *mdl ) { + if ( !mdl ) { + return; + } + + CollideWithPortal( mdl->GetEntity() ); +} + +//========================================================================== +// +// hhPortal::AddProximityEntity +// +// Saves this entity on a list, and will check if it can be portalled +// the next time the portal thinks +//========================================================================== + +void hhPortal::AddProximityEntity( const idEntity *other) { + // Go through the list and guarantee that this entity isn't in multiple times + // note: cannot use IdList::AddUnique, because the lastPortalPoint might be different during this add + for( int i = 0; i < proximityEntities.Num(); i++ ) { + if ( proximityEntities[i].entity.GetEntity() == other ) { + return; + } + } + + // Add this entity to the potential portal list + proximityEntity_t prox; + prox.entity = other; + prox.lastPortalPoint = ((idEntity *)(other))->GetPortalPoint(); + + proximityEntities.Append( prox ); + + // If the entity is a player, then inform the player that they are close to this portal + // needed for weapon projectile firing + if ( other->IsType( hhPlayer::Type ) ) { + hhPlayer *player = (hhPlayer *)(other); + + player->SetPortalColliding( true ); + } +} + +//========================================================================== +// +// hhPortal::PortalEntity +// +// To rotate a vector from one portal space into another: +// transform vector into Source Portal Space (mul by axis transpose) +// flip X & Y (only flip X for angles, not for position) +// transform vector into Destination Portal Space +//========================================================================== + +void PortalRotate( idVec3 &vec, const idMat3 &sourceTranspose, const idMat3 &dest, const bool flipX ) { + vec *= sourceTranspose; + vec.y *= -1; + if ( flipX ) { + vec.x *= -1; + } + vec *= dest; +} + +bool hhPortal::PortalEntity( idEntity *ent, const idVec3 &point ) { + idMat3 sourceAxis; + idMat3 destAxis; + idMat3 newEntAxis; + + if ( !ent ) { + return(false); + } + + if ( cameraTarget ) { + sourceAxis = GetAxis().Transpose(); + destAxis = cameraTarget->GetAxis(); + + // Compute new location + idVec3 newLocation = point - GetOrigin(); + PortalRotate( newLocation, sourceAxis, destAxis, false ); + newLocation += cameraTarget->GetOrigin(); + + // Compute new axis + newEntAxis = ent->GetAxis(); + + // Rotate the vector into new portal space + PortalRotate( newEntAxis[0], sourceAxis, destAxis, true ); + PortalRotate( newEntAxis[1], sourceAxis, destAxis, true ); + PortalRotate( newEntAxis[2], sourceAxis, destAxis, true ); + + // Actually attempt to portal the entity + if ( PortalTeleport( ent, newLocation, newEntAxis, sourceAxis, destAxis ) ) { + if ( alertMonsters && !gameLocal.isMultiplayer && ent->IsType( idPlayer::Type ) ) { + gameLocal.SendMessageAI( this, GetOrigin(), 2000, MA_EnemyPortal ); + } + ent->Portalled( this ); // Inform the actor that it was just portalled + return(true); // Portal succeeded + } + } + + return(false); // Portal failed +} + +//========================================================================== +// +// hhPortal::PortalTeleport +// +//========================================================================== + +bool hhPortal::PortalTeleport( idEntity *ent, const idVec3 &origin, const idMat3 &axis, const idMat3 &sourceAxis, const idMat3 &destAxis ) { + idClipModel *clip = ent->GetPhysics()->GetClipModel(); + + if ( !clip ) { + return false; + } + + // Properly set velocity relative to the new portal + idVec3 vel = ent->GetPhysics()->GetLinearVelocity(); + PortalRotate( vel, sourceAxis, destAxis, true ); + ent->GetPhysics()->SetLinearVelocity( vel ); + + //rww - check if this new orientation is going to be in solid or not. if it is, try displacing the new origin based on the portal + idVec3 useOrigin = origin; + trace_t transCheck; + if (gameLocal.clip.TranslationWithExceptions(transCheck, useOrigin, useOrigin, NULL, clip, axis, ent->GetPhysics()->GetClipMask(), ent)) { + if (cameraTarget) { + bool safeSpot = true; + const float distExtrusion = 2.0f; + const float heightAdjust = (ent->GetPhysics()->GetBounds()[1].z-fabsf(ent->GetPhysics()->GetBounds()[0].z))/2.0f; + idVec3 testOrigin = cameraTarget->GetOrigin(); + testOrigin += cameraTarget->GetAxis()[0]*distExtrusion; + testOrigin -= cameraTarget->GetAxis()[2]*heightAdjust; + if (gameLocal.clip.TranslationWithExceptions(transCheck, testOrigin, testOrigin, NULL, clip, axis, ent->GetPhysics()->GetClipMask(), ent)) { + testOrigin += cameraTarget->GetAxis()[2]*(heightAdjust*2.0f); //then try going up further (could be upside-down or something) + + if (gameLocal.clip.TranslationWithExceptions(transCheck, testOrigin, testOrigin, NULL, clip, axis, ent->GetPhysics()->GetClipMask(), ent)) { + //damn, this portal is really busted. + safeSpot = false; + } + } + if (safeSpot) { + useOrigin = testOrigin; //got a safe spot + if (ent->IsType(hhPlayer::Type)) { + hhPlayer *plEnt = static_cast(ent); + if (plEnt->IsWallWalking()) { //if it's a wallwalking player, try to trace down on the other side for wallwalk + idVec3 downPoint = useOrigin - (axis[2]*64.0f); + if (gameLocal.clip.Translation(transCheck, useOrigin, downPoint, clip, axis, ent->GetPhysics()->GetClipMask(), ent)) { + if (gameLocal.GetMatterType(transCheck, NULL) == SURFTYPE_WALLWALK) { //if we hit wallwalk, use the trace endpoint + useOrigin = transCheck.endpos; + } + } + } + } + } + else { + testOrigin = cameraTarget->GetOrigin(); + testOrigin += cameraTarget->GetAxis()[0]*distExtrusion; + testOrigin -= cameraTarget->GetAxis()[2]*heightAdjust; + + useOrigin = testOrigin; + +#if !GOLD + if (developer.GetBool()) { + const char *entName = ent->GetName(); + if (!entName || !entName[0]) { + entName = ""; + } + gameLocal.Warning("Ent '%s' could not get a clean trace on the other side of gameportal at (%f %f %f).", entName, useOrigin.x, useOrigin.y, useOrigin.z); + hhUtils::DebugAxis( testOrigin, cameraTarget->GetAxis(), 32.0f, 5000 ); + } +#endif + } + } + } + + // If the actor is a player, then disable camera interpolation for this move. This must be done before linking + if( ent->IsType(hhPlayer::Type) ) { + hhPlayer* player = static_cast( ent ); + + + idVec3 viewDir = player->GetAxis()[0]; + PortalRotate( viewDir, sourceAxis, destAxis, true ); + player->TeleportNoKillBox( useOrigin, axis, viewDir, player->GetUntransformedViewAngles() ); + } else { + // Valid move. This code is done in SetOrientation for the player. + ent->SetOrigin( useOrigin ); + ent->SetAxis( axis ); + } + + // Re-link the actor into the clip tree + clip->Link( gameLocal.clip ); + + if ( ent->IsType( idActor::Type ) ) { + static_cast(ent)->LinkCombat(); + } + + // players telefrag anything at the new position + if ( ent->IsType( hhPlayer::Type ) ) { + gameLocal.KillBox( ent ); + } + + // Set the gravity correctly on the entity that was portaled, based upon the destination portal's gravity + ent->CancelEvents( &EV_ResetGravity ); + + if (!ent->IsType(hhVehicle::Type)) { + ent->SetGravity( cameraTarget->GetGravity() ); + } + + // If the actor is a player, then check for wallwalk. This should be done after linking and setting gravity + if( ent->IsType(hhPlayer::Type) ) { + hhPlayer* player = static_cast(ent); + if (player->GetPhysics() && player->GetPhysics()->IsType(hhPhysics_Player::Type)) { + static_cast(player->GetPhysics())->CheckWallWalk( true ); + } + } + + // Sound issues + if ( bGlowPortal ) { + StartSound( "snd_portal_entity", SND_CHANNEL_ANY ); // Play the portal sound at the origin portal + if(this->cameraTarget) { // Play the portal sound at the destination portal as well + const idSoundShader *def = declManager->FindSound(spawnArgs.GetString("snd_portal_entity"));//gameSoundWorld->FinishShader(spawnArgs.GetString("snd_portal_entity")); + this->cameraTarget->StartSoundShader( def, SND_CHANNEL_ANY ); + } + } + + return(true); +} + +//========================================================================== +// +// hhPortal::Event_Trigger +// +// Toggle portal state. +//========================================================================== + +void hhPortal::Event_Trigger(idEntity *activator) { + + // If we have a master portal, have him trigger everyone + if ( masterPortal.IsValid() ) { + masterPortal->Event_Trigger( activator ); + return; + } + + // If we have slave portals, trigger them first + if ( slavePortals.Num() > 0 ) { + for ( int i = 0; i < slavePortals.Num(); ++i ) { + slavePortals[ i ]->Trigger( activator ); + } + } + + Trigger( activator ); +} + + +void hhPortal::Trigger( idEntity *activator ) { + + hhFxInfo fxInfo; + + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone( true ); + + if(portalState == PORTAL_CLOSED) { + portalState = PORTAL_OPENING; + if (!bNoTeleport) { //rww - if teleporting, ensure things collide with me while i am opening + GetPhysics()->SetContents( CONTENTS_SOLID ); + } +#if GAMEPORTAL_PVS + if (areaPortal && !gameLocal.isClient) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_NONE ); + } +#endif + TriggerTargets(); // nla + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] = MS2SEC( gameLocal.time ); + + if ( bGlowPortal ) { + if (!gameLocal.isMultiplayer) { //rww - superfast portals don't use fx + BroadcastFxInfo( spawnArgs.GetString("fx_open"), GetOrigin(), GetAxis(), &fxInfo ); + } + + StartSound( "snd_open", SND_CHANNEL_ANY ); + + if ( spawnArgs.GetBool( "fast_open", "0" ) ) { + SetSkinByName( spawnArgs.GetString( "skin" ) ); + } else { + SetSkinByName( spawnArgs.GetString( "skin_onlyWarp" ) ); // Only show the warp when first opening + } + + int anim = GetAnimator()->GetAnim(spawnArgs.GetString("open_anim", "open")); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, 0 ); + PostEventMS( &EV_Opened, GetAnimator()->AnimLength( anim ) ); + SetShaderParm( SHADERPARM_MODE, 0 ); // ensure that sparking is off + + fl.neverDormant = true; // Don't allow the portal to go dormant while opening/closing + + } else { + PostEventMS( &EV_Opened, 500 ); + } + + PostEventMS( &EV_Show, 10 ); // Delay showing for a frame so the skin and registers are properly set beforehand + } else if(portalState == PORTAL_OPENED || portalState == PORTAL_OPENING) { + portalState = PORTAL_CLOSING; + TriggerTargets(); // nla + + renderEntity.shaderParms[SHADERPARM_TIMEOFFSET] = -MS2SEC( gameLocal.time ); + + if ( bGlowPortal ) { + if (!gameLocal.isMultiplayer) { //rww - superfast portals don't use fx + BroadcastFxInfo( spawnArgs.GetString("fx_close"), GetOrigin(), GetAxis(), &fxInfo ); + } + + StopSound( SND_CHANNEL_ANY ); + StartSound( "snd_close", SND_CHANNEL_ANY ); + + int anim = GetAnimator()->GetAnim("close"); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, 0.5 ); + PostEventMS( &EV_Closed, GetAnimator()->AnimLength( anim ) ); + + fl.neverDormant = true; // Don't allow the portal to go dormant while opening/closing + + } else { + PostEventMS( &EV_Closed, 400 ); + } + } +} + +//========================================================================== +// +// hhPortal::Event_Opened +// +//========================================================================== + +void hhPortal::Event_Opened( void ) { + portalState = PORTAL_OPENED; + if (!bNoTeleport) { //rww - if teleporting, ensure things collide with me while i am open + GetPhysics()->SetContents( CONTENTS_SOLID ); + } + TriggerTargets(); // nla + + if ( bGlowPortal ) { + StartSound( "snd_loop", SND_CHANNEL_ANY ); + int anim = GetAnimator()->GetAnim("opened"); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, 0 ); + + fl.neverDormant = false; // The portal can go dormant once opened/closed + + PostEventSec( &EV_PortalSpark, gameLocal.random.RandomFloat() ); + } + + UpdateVisuals(); + + // If we are open, and should close automatically, post an event to trigger us closed - nla + if ( closeDelay > 0.0f ) { + PostEventSec( &EV_Activate, closeDelay, NULL ); + + // Remove the portal now that it has done it's job + if (monsterportal) { +#ifdef _DEBUG + // If another portal is camera targetting me it will cause a crash so check for it + for( idEntity *ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if (ent->cameraTarget && ent->cameraTarget == this) { + assert(0); + } + } + +#endif + PostEventSec( &EV_Remove, closeDelay+5.0f ); + } + } +} + +//========================================================================== +// +// hhPortal::Event_Closed +// +//========================================================================== + +void hhPortal::Event_Closed( void ) { + renderEntity.shaderParms[SHADERPARM_MODE] = 1.0f; + portalState = PORTAL_CLOSED; + GetPhysics()->SetContents( 0 ); //rww - do not collide with things while closed +#if GAMEPORTAL_PVS + if (areaPortal && !gameLocal.isClient) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_ALL ); + } +#endif + TriggerTargets(); // nla + + fl.neverDormant = false; // The portal can go dormant once opened/closed + + Hide(); + UpdateVisuals(); + + //? Have an option to remove the portals? - nla + if ( spawnArgs.GetBool( "remove_on_close", "0" ) ) { + PostEventMS( &EV_Remove, 0 ); + } +} + +//========================================================================== +// +// hhPortal::Event_PortalSpark +// +//========================================================================== + +void hhPortal::Event_PortalSpark( void ) { + int spark; + float nextTime; + + if ( this->IsHidden() ) { // No sparking if hidden + SetShaderParm( SHADERPARM_MODE, 0 ); // set the material parm (one-based) + return; + } + + spark = gameLocal.random.RandomInt( spawnArgs.GetInt( "sparkCount" ) ); + + // Set the shader parm as needed + SetShaderParm( SHADERPARM_MODE, spark + 1 ); // set the material parm (one-based) + + if ( gameLocal.random.RandomFloat() < 0.1f ) { // 1/10th of a chance that another spark will happen quickly after this one + nextTime = 0.2f; + } else { // Normal random time between sparks. + nextTime = spawnArgs.GetFloat( "sparkTimeMin" ) + gameLocal.random.RandomFloat() * spawnArgs.GetFloat( "sparkTimeRnd" ); + } + + StartSound( "snd_portal_spark", SND_CHANNEL_ANY ); + + PostEventSec( &EV_PortalSpark, nextTime ); + PostEventSec( &EV_PortalSparkEnd, 0.1f ); // spark only lasts for 0.1 sec + +} + +//========================================================================== +// +// hhPortal::Event_PortalSparkEnd +// +//========================================================================== + +void hhPortal::Event_PortalSparkEnd( void ) { + SetShaderParm( SHADERPARM_MODE, 0 ); // Disable the spark parm +} + +//========================================================================== +// +// hhPortal::Event_ShowGlowPortal +// +// Simply sets the skin on the portal back to the default or to the +// specified "skin" +//========================================================================== + +void hhPortal::Event_ShowGlowPortal( void ) { + SetSkinByName( spawnArgs.GetString( "skin" ) ); +} + +//========================================================================== +// +// hhPortal::Event_HideGlowPortal +// +// Sets the skin on the portal to a specific skin that hides the glowy parts +//========================================================================== + +void hhPortal::Event_HideGlowPortal( void ) { + SetSkinByName( spawnArgs.GetString( "skin_onlyWarp" ) ); +} diff --git a/src/Prey/game_portal.h b/src/Prey/game_portal.h new file mode 100644 index 0000000..ea59f61 --- /dev/null +++ b/src/Prey/game_portal.h @@ -0,0 +1,125 @@ + +#ifndef __GAME_PORTAL_H__ +#define __GAME_PORTAL_H__ + +#if GAMEPORTAL_PVS + +// Allows us to create a PVS link between two areas +class hhArtificialPortal : public idEntity { + CLASS_PROTOTYPE( hhArtificialPortal ); +public: + void Spawn(); + void SetPortalState(bool openPVS, bool openSound); + + void Save(idSaveGame *savefile) const; + void Restore(idRestoreGame *savefile); + +protected: + void Event_SetPortalState(bool openPVS, bool openSound); + + qhandle_t areaPortal; // 0 = no portal +}; + +#endif + +typedef struct proximityEntity_s { + idEntityPtr entity; + idVec3 lastPortalPoint; +} proximityEntity_t; + +// jsh - Changed from idEntity to hhAnimatedEntity +class hhPortal : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhPortal ); + + typedef enum { + PORTAL_OPENING, + PORTAL_OPENED, + PORTAL_CLOSING, + PORTAL_CLOSED, + } portalStates_t; + + + hhPortal(void); + ~hhPortal(); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + void CheckPlayerDistances(void); + void Think( void ); + bool PortalEntity( idEntity *ent, const idVec3 &point ); + bool PortalTeleport( idEntity *ent, const idVec3 &origin, const idMat3 &axis, const idMat3 &sourceAxis, const idMat3 &destAxis ); + bool IsActive(void) { return(portalState < PORTAL_CLOSING); } + virtual void SetGravity( const idVec3& newGravity ) { portalGravity = newGravity; } + const idVec3& GetGravity( void ) const { return portalGravity; } + void PostSpawn( void ); + + bool AttemptPortal( idPlane &plane, idEntity *hit, idVec3 location, idVec3 nextLocation ); + + void PortalProjectile( hhProjectile *projectile, idVec3 collideLocation, idVec3 nextLocation ); + + virtual bool CheckPortal( const idEntity *other, int contentMask ); + virtual bool CheckPortal( const idClipModel *mdl, int contentMask ); + virtual void CollideWithPortal( const idEntity *other ); // CJR PCF 04/26/06 + virtual void CollideWithPortal( const idClipModel *mdl ); // CJR PCF 04/26/06 + + void AddProximityEntity( const idEntity *other); + +protected: + void Event_Opened( void ); + void Event_Closed( void ); + void Event_Trigger(idEntity *activator); + void Event_ResetGravity() { portalGravity = hhUtils::GetLocalGravity(GetOrigin(), GetPhysics()->GetBounds(), gameLocal.GetGravity() ); } + + void Event_PortalSpark( void ); + void Event_PortalSparkEnd( void ); + + void Event_ShowGlowPortal( void ); + void Event_HideGlowPortal( void ); + + // Actual logic for the trigger functionality. (Needed to break out for partering :) + void Trigger( idEntity *activator ); + + // Methods used to sync up portals + hhPortal * GetMasterPortal() { return( masterPortal.GetEntity() ); } + void SetMasterPortal( hhPortal *master ) { masterPortal = master; } + void AddSlavePortal( hhPortal *slave ) { slavePortals.Append( slave ); } + void CheckForBuddy(); + + void TriggerTargets(); + +private: +#if GAMEPORTAL_PVS + qhandle_t areaPortal; // 0 = no portal +#endif + + portalStates_t portalState; + + bool bNoTeleport; // Is purely a visual portal. Will not try to teleport anything near it + bool bGlowPortal; // This portal has visual/audio effects + + idVec3 portalGravity; // Local gravity to this portal. Will be changed by any zones the portal is within + + float closeDelay; // Secs to wait before automatically closing when opened - nla + float distanceToggle; //rww - toggles the associated game portal on and off based on nearest player distance, 0.0 means not enabled, otherwise value is the distance to turn on at. + float distanceCull; //rww - distance to shut off the vis gameportal but not the render portal + bool areaPortalCulling; //rww - if the portal is currently "culled" (off) due to distanceCull checking + bool monsterportal; + bool alertMonsters; + + idList > slavePortals; + idEntityPtr masterPortal; + + idList proximityEntities; + + idEntityPtr m_portalIdleFx; //rww - primarily for mp, the effect closed pop-open portals play +}; + +#endif /* __GAME_PORTAL_H__ */ diff --git a/src/Prey/game_portalframe.cpp b/src/Prey/game_portalframe.cpp new file mode 100644 index 0000000..d48755f --- /dev/null +++ b/src/Prey/game_portalframe.cpp @@ -0,0 +1,20 @@ +// hhPortalFrame +// +// Frame for our portal which accepts commands from GUIs which change shader parm5 +// based on the command, as well as trigger some given entities each level, and finally +// trigger all it's own targets upon the victory command + + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION(idEntity, hhPortalFrame) +END_CLASS + + +void hhPortalFrame::Spawn() { + GetPhysics()->SetContents(CONTENTS_SOLID); +} + diff --git a/src/Prey/game_portalframe.h b/src/Prey/game_portalframe.h new file mode 100644 index 0000000..708570a --- /dev/null +++ b/src/Prey/game_portalframe.h @@ -0,0 +1,14 @@ + +#ifndef __GAME_PORTALFRAME_H__ +#define __GAME_PORTALFRAME_H__ + + +class hhPortalFrame : public idEntity { +public: + CLASS_PROTOTYPE( hhPortalFrame ); + + void Spawn( void ); +}; + + +#endif /* __GAME_PORTALFRAME_H__ */ diff --git a/src/Prey/game_proxdoor.cpp b/src/Prey/game_proxdoor.cpp new file mode 100644 index 0000000..9a558ec --- /dev/null +++ b/src/Prey/game_proxdoor.cpp @@ -0,0 +1,1163 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +const idEventDef EV_PollForExit("", NULL); +const float proxDoorRefreshMS = 50.f; //refresh rate of door while open (controls how frequently to check if someone is still close enough) + + +ABSTRACT_DECLARATION( idEntity, hhProxDoorSection ) +END_CLASS + +void hhProxDoorSection::Spawn( void ) { + fl.networkSync = true; + proximity = 0.0f; + hasNetData = false; + proxyParent = NULL; +} + +void hhProxDoorSection::ClientPredictionThink( void ) { + Show(); + idEntity::ClientPredictionThink(); +} + +void hhProxDoorSection::WriteToSnapshot( idBitMsgDelta &msg ) const { + WriteBindToSnapshot(msg); + GetPhysics()->WriteToSnapshot(msg); +} + +void hhProxDoorSection::ReadFromSnapshot( const idBitMsgDelta &msg ) { + ReadBindFromSnapshot(msg); + GetPhysics()->ReadFromSnapshot(msg); + + if (msg.HasChanged()) { + Show(); + UpdateVisuals(); + Present(); + GetPhysics()->LinkClip(); + } +} + + + +//------------------------------------------------------------------------------------------------- +// hhProxDoor. +//------------------------------------------------------------------------------------------------- +CLASS_DECLARATION( idEntity, hhProxDoor ) + EVENT( EV_Touch, hhProxDoor::Event_Touch ) + EVENT( EV_PollForExit, hhProxDoor::Event_PollForExit ) + EVENT( EV_PostSpawn, hhProxDoor::Event_PostSpawn ) + EVENT( EV_Activate, hhProxDoor::Event_Activate ) +END_CLASS + +//-------------------------------- +// hhProxDoor::hhProxDoor +//-------------------------------- +hhProxDoor::hhProxDoor() { + areaPortal = 0; + proxState = PROXSTATE_Inactive; + doorTrigger = NULL; + aas_area_closed = false; + hasNetData = false; + sndTrigger = NULL; + nextSndTriggerTime = 0; + //HUMANHEAD PCF rww 06/06/06 - fix for snappy prox doors + lastAmount = 0.0f; + //HUMANHEAD END +} + +//-------------------------------- +// hhProxDoor::hhProxDoor +//-------------------------------- +hhProxDoor::~hhProxDoor() { + if (doorTrigger) { + doorTrigger->Unlink(); + delete doorTrigger; + doorTrigger = NULL; + } + if ( sndTrigger ) { + sndTrigger->Unlink(); + delete sndTrigger; + sndTrigger = NULL; + } +} + +//-------------------------------- +// hhProxDoor::Spawn +//-------------------------------- +void hhProxDoor::Spawn() { + proxState = PROXSTATE_Inactive; + doorSndState = PDOORSND_Closed; + + spawnArgs.GetBool( "startlocked", "0", doorLocked ); + SetShaderParm( SHADERPARM_MODE, GetDoorShaderParm( doorLocked, true ) ); // 2=locked, 1=unlocked, 0=never locked + + if( !spawnArgs.GetFloat("trigger_distance", "0.0", maxDistance) ) { + common->Warning( "No 'trigger_distance' specified for entityDef '%s'", this->GetEntityDefName() ); + } + if( !spawnArgs.GetFloat("movement_distance", "0.0", movementDistance) ) { + common->Warning( "No 'movement_distance' specified for entityDef '%s'", this->GetEntityDefName() ); + } + lastDistance = movementDistance + 1.0f; // Safe default -mdl + + if( !spawnArgs.GetFloat("stop_distance", "0.0", stopDistance) ) { + common->Warning( "No 'stop_distance' specificed for entityDef '%s'", this->GetEntityDefName() ); + } + + spawnArgs.GetBool( "openForMonsters", "1", openForMonsters ); + spawnArgs.GetFloat( "damage", "1", damage ); + + //Find any portal boundary we are on + areaPortal = gameRenderWorld->FindPortal( GetPhysics()->GetAbsBounds() ); + + if (gameLocal.isMultiplayer) { + fl.networkSync = true; + + if (gameLocal.isClient) { + doorTrigger = new idClipModel( idTraceModel(idBounds(vec3_origin).Expand(maxDistance)) ); + doorTrigger->Link( gameLocal.clip, this, 255, GetOrigin(), GetAxis() ); + doorTrigger->SetContents( CONTENTS_TRIGGER ); + + SetAASAreaState( doorLocked ); //close the area state if we are locked... + SetDoorState( PROXSTATE_Inactive ); + } + else { + PostEventMS( &EV_PostSpawn, 50 ); //rww - this must be delayed or it will happen on the first server frame and we DON'T want that. + } + } + else { //just call it now. some sp maps might target a door piece in the spawn function of an entity or something crazy like that. + Event_PostSpawn(); + } +} + +//-------------------------------- +// hhProxDoor::Event_PostSpawn +//-------------------------------- +void hhProxDoor::Event_PostSpawn( void ) { + int numSubObjs; + int i; + const char* objDef; + +//Parse and spawn our door sections + numSubObjs = spawnArgs.GetInt( "num_doorobjs", "0" ); + for( i = 0; i < numSubObjs; i++ ) { + if( !spawnArgs.GetString( va("doorobject%i", i+1), "", &objDef ) ) { + common->Warning( "failed to find doorobject%i", i+1 ); + } + else { + //Set our default rotation/origin. + idDict args; + args.SetVector( "origin", this->GetOrigin() ); + args.SetMatrix( "rotation", this->GetAxis() ); + hhProxDoorSection* ent = static_cast ( gameLocal.SpawnObject(objDef, &args) ); + if( !ent ) { + common->Warning( "failed to spawn doorobject%i for entityDef '%s'", i+1, GetEntityDefName() ); + } + else { + ent->proxyParent = this; + doorPieces.Append( ent ); + } + } + } +//Spawn our trigger and link it up +// if( doorLocked && !spawnArgs.GetBool("locktrigger") ) { + //we would never be able to open the door, so don't spawn a trigger +// } +// else { + doorTrigger = new idClipModel( idTraceModel(idBounds(vec3_origin).Expand(maxDistance)) ); + doorTrigger->Link( gameLocal.clip, this, 255, GetOrigin(), GetAxis() ); + doorTrigger->SetContents( CONTENTS_TRIGGER ); +// } + + SpawnSoundTrigger(); + + SetAASAreaState( doorLocked ); //close the area state if we are locked... + SetDoorState( PROXSTATE_Inactive ); +} + +void hhProxDoor::SpawnSoundTrigger() { + idBounds bounds,localbounds; + int i; + int best; + + // Since models bounds are overestimated, we need to use the bounds from the + // clipmodel, which was set before the over-estimation + localbounds = GetPhysics()->GetBounds(); + + // find the thinnest axis, which will be the one we expand + best = 0; + for ( i = 1 ; i < 3 ; i++ ) { + if ( localbounds[1][ i ] - localbounds[0][ i ] < localbounds[1][ best ] - localbounds[0][ best ] ) { + best = i; + } + } + localbounds[1][ best ] += 50; + localbounds[0][ best ] -= 50; + + // Now transform into absolute coordintates + if ( GetPhysics()->GetAxis().IsRotated() ) { + bounds.FromTransformedBounds( localbounds, GetPhysics()->GetOrigin(), GetPhysics()->GetAxis() ); + } + else { + bounds[0] = localbounds[0] + GetPhysics()->GetOrigin(); + bounds[1] = localbounds[1] + GetPhysics()->GetOrigin(); + } + bounds[0] -= GetPhysics()->GetOrigin(); + bounds[1] -= GetPhysics()->GetOrigin(); + + // create a trigger clip model + sndTrigger = new idClipModel( idTraceModel( bounds ) ); + sndTrigger->Link( gameLocal.clip, this, 254, GetPhysics()->GetOrigin(), mat3_identity ); + sndTrigger->SetContents( CONTENTS_TRIGGER ); +} + +void hhProxDoor::Save(idSaveGame *savefile) const { + int i; + + savefile->WriteInt( proxState ); + savefile->WriteInt( doorSndState ); + + savefile->WriteInt( doorPieces.Num() ); // idList + for (i=0; iWriteObject( doorPieces[i] ); + doorPieces[i].Save(savefile); + } + + savefile->WriteClipModel( doorTrigger ); + savefile->WriteFloat( entDistanceSq ); + savefile->WriteFloat( maxDistance ); + savefile->WriteFloat( movementDistance ); + savefile->WriteFloat( stopDistance ); + savefile->WriteInt( areaPortal ); + if ( areaPortal ) { + savefile->WriteInt( gameRenderWorld->GetPortalState( areaPortal ) ); + } + savefile->WriteBool( doorLocked ); + savefile->WriteFloat( lastAmount ); + savefile->WriteBool( openForMonsters ); + savefile->WriteBool( aas_area_closed ); + savefile->WriteFloat( lastDistance ); + savefile->WriteClipModel( sndTrigger ); + savefile->WriteInt( nextSndTriggerTime ); +} + +void hhProxDoor::Restore( idRestoreGame *savefile ) { + int i, num; + + savefile->ReadInt( (int &)proxState ); + savefile->ReadInt( (int &)doorSndState ); + + doorPieces.Clear(); + savefile->ReadInt( num ); // idList + doorPieces.SetNum( num ); + for (i=0; iReadObject( reinterpret_cast(doorPieces[i]) ); + doorPieces[i].Restore(savefile); + } + + savefile->ReadClipModel( doorTrigger ); + savefile->ReadFloat( entDistanceSq ); + savefile->ReadFloat( maxDistance ); + savefile->ReadFloat( movementDistance ); + savefile->ReadFloat( stopDistance ); + savefile->ReadInt( (int &)areaPortal ); + if ( areaPortal ) { + int portalState; + savefile->ReadInt( portalState ); + gameLocal.SetPortalState( areaPortal, portalState ); + } + savefile->ReadBool( doorLocked ); + savefile->ReadFloat( lastAmount ); + savefile->ReadBool( openForMonsters ); + savefile->ReadBool( aas_area_closed ); + savefile->ReadFloat( lastDistance ); + + SetAASAreaState( aas_area_closed ); + + spawnArgs.GetFloat( "damage", "1", damage ); + savefile->ReadClipModel( sndTrigger ); + savefile->ReadInt( nextSndTriggerTime ); +} + +void hhProxDoor::ClientPredictionThink( void ) { + Think(); +} + +void hhProxDoor::WriteToSnapshot( idBitMsgDelta &msg ) const { + WriteBindToSnapshot(msg); + GetPhysics()->WriteToSnapshot(msg); + + msg.WriteBits(doorPieces.Num(), 8); + for (int i = 0; i < doorPieces.Num(); i++) { + msg.WriteBits(doorPieces[i].GetSpawnId(), 32); + } + + msg.WriteBits(proxState, 8); + + msg.WriteFloat(lastAmount); + + msg.WriteBits(aas_area_closed, 1); + + //don't need this, predicting door movement. + //msg.WriteBits(doorSndState, 8); +} + +void hhProxDoor::ReadFromSnapshot( const idBitMsgDelta &msg ) { + ReadBindFromSnapshot(msg); + GetPhysics()->ReadFromSnapshot(msg); + + int num = msg.ReadBits(8); + doorPieces.SetNum(num); + for (int i = 0; i < num; i++) { + int spawnId = msg.ReadBits(32); + if (!spawnId) { + doorPieces[i] = NULL; + } + else { + doorPieces[i].SetSpawnId(spawnId); + } + } + + EProxState newProxState = (EProxState)msg.ReadBits(8); + if (proxState != newProxState) { + SetDoorState(newProxState); + } + + lastAmount = msg.ReadFloat(); + + bool closed = !!msg.ReadBits(1); + if (aas_area_closed != closed) { + SetAASAreaState(closed); + } + + /* + EPDoorSound newSndState = (EPDoorSound)msg.ReadBits(8); + if (newSndState != doorSndState) { + UpdateSoundState(newSndState); + } + */ + + hasNetData = true; +} + +//-------------------------------- +// hhProxDoor::Think +//-------------------------------- +void hhProxDoor::Ticker( void ) { + int i; + float bestDistSq, bestDist = idMath::INFINITY, amount; + + //HUMANHEAD PCF mdl 04/27/06 - Locked doors are forced inactive + if ( doorLocked && proxState == PROXSTATE_Active ) { + proxState = PROXSTATE_GoingInactive; + } + + if ( proxState == PROXSTATE_GoingInactive ) { + float fullyOpen = (movementDistance - stopDistance) / movementDistance; + //HUMANHEAD PCF mdl 04/27/06 - Added 30 HZ multiplier + float step = (0.2f * ( 1.0f / ( 1000.0f / 60.0f ) ) ) * (60.0f * USERCMD_ONE_OVER_HZ); + float low = idMath::ClampFloat( 0.0f, 1.0f, lastAmount - step ); + float high = idMath::ClampFloat( 0.0f, 1.0f, lastAmount + step ); + amount = idMath::ClampFloat( low, high, 0.0f ); + //HUMANHEAD PCF mdl 04/27/06 - Removed old lock code that was here + for( i = 0; i < doorPieces.Num(); i++ ) { + if (doorPieces[i].IsValid()) { //rww - added in case something decides to remove one of the pieces externally. + if (gameLocal.isClient && areaPortal && gameRenderWorld->GetPortalState(areaPortal) != PS_BLOCK_NONE) { + amount = 0.0f; //rww - hax for portal state sync'ing on client and server + } + doorPieces[ i ]->SetProximity( amount ); + } + } + + if ( amount == 0.0f ) { + SetDoorState( PROXSTATE_Inactive ); + UpdateSoundState( PDOORSND_Closed ); + } else { + UpdateSoundState( PDOORSND_Closing ); + } + + } else { + + bestDist = movementDistance; + bestDistSq = PollClosestEntity(); + if( bestDistSq >= 0.f ) { + bestDist = idMath::Sqrt( bestDistSq ); + } + + if ( bestDistSq == -1.0f ) { + SetDoorState( PROXSTATE_GoingInactive ); + return; + } + + // Set the default amount to no change + amount = lastAmount; + if( bestDist < movementDistance ) { + float fullyOpen = (movementDistance - stopDistance) / movementDistance; + amount = idMath::ClampFloat( 0.f, fullyOpen, (1.f - ( bestDist / movementDistance )) ); + //HUMANHEAD PCF mdl 04/27/06 - Added 30 HZ multiplier + float step = (0.2f * ( 1.0f / ( 1000.0f / 60.0f ) ) ) * (60.0f * USERCMD_ONE_OVER_HZ); + float low = idMath::ClampFloat( 0.0f, 1.0f, lastAmount - step ); + float high = idMath::ClampFloat( 0.0f, 1.0f, lastAmount + step ); + amount = idMath::ClampFloat( low, high, amount ); + if ( doorLocked ) { + for( i = 0; i < doorPieces.Num(); i++ ) { + if (doorPieces[i].IsValid()) { + doorPieces[ i ]->SetProximity( 0.0f ); + } + } + } else { + for( i = 0; i < doorPieces.Num(); i++ ) { + if (doorPieces[i].IsValid()) { //rww - added in case something decides to remove one of the pieces externally. + if (gameLocal.isClient && areaPortal && gameRenderWorld->GetPortalState(areaPortal) != PS_BLOCK_NONE) { + amount = 0.0f; //rww - hax for portal state sync'ing on client and server + } + doorPieces[ i ]->SetProximity( amount ); + } + } + } + if( lastAmount == -1 && amount >= 0.f ) { //was closed, just starting to open + UpdateSoundState( PDOORSND_Opened ); + } + else if( lastAmount > amount ) { //closing + UpdateSoundState( PDOORSND_Closing ); + } + else if( lastAmount < amount ) { //opening + UpdateSoundState( PDOORSND_Opening ); + } + else if( amount == fullyOpen ) { //fully open + UpdateSoundState( PDOORSND_FullyOpened ); + } + else { //stop sounds, since we aren't moving + UpdateSoundState( PDOORSND_Stopped ); + } + } + else { + SetDoorState( PROXSTATE_GoingInactive ); + UpdateSoundState( PDOORSND_Closing ); + } + } + + // If we're closing and 75% closed or more, crush anything unlucky enough to be inside us (don't do this on the client -rww) + if ( !gameLocal.isClient && lastAmount < 0.25f && lastAmount != -1.0f && amount != -1.0f && amount < lastAmount ) { + CrushEntities(); + } + lastAmount = amount; + lastDistance = bestDist; +} + +void hhProxDoor::UpdateSoundState( EPDoorSound newState ) { + if( doorSndState == newState ) { + return; + } + switch( newState ) { + case PDOORSND_Opening: + StopSound( SND_CHANNEL_BODY ); + StartSound( "snd_closing", SND_CHANNEL_BODY ); + break; + + case PDOORSND_Closing: + StopSound( SND_CHANNEL_BODY ); + StartSound( "snd_opening", SND_CHANNEL_BODY ); + break; + + case PDOORSND_Closed: + StopSound( SND_CHANNEL_BODY2 ); + StopSound( SND_CHANNEL_BODY ); + StartSound( "snd_closed", SND_CHANNEL_BODY2 ); + break; + + case PDOORSND_Opened: + StopSound( SND_CHANNEL_BODY2 ); + StartSound( "snd_opened", SND_CHANNEL_BODY2 ); + break; + + case PDOORSND_Stopped: + StopSound( SND_CHANNEL_BODY ); + break; + + case PDOORSND_FullyOpened: + StopSound( SND_CHANNEL_BODY ); + StopSound( SND_CHANNEL_BODY2 ); + StartSound( "snd_fullyopened", SND_CHANNEL_BODY2 ); + break; + } + doorSndState = newState; +} + +//-------------------------------- +// hhProxDoor::OpenPortal +//-------------------------------- +void hhProxDoor::OpenPortal( void ) { + if( areaPortal && !gameLocal.isClient ) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_NONE ); + } + SetAASAreaState( false ); +} + +//-------------------------------- +// hhProxDoor::ClosePortal +//-------------------------------- +void hhProxDoor::ClosePortal( void ) { + if( areaPortal && !gameLocal.isClient ) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_VIEW ); + } + SetAASAreaState( IsLocked() ); +} + +//-------------------------------- +// hhProxDoor::SetAASAreaState +//-------------------------------- +void hhProxDoor::SetAASAreaState( bool closed ) { + aas_area_closed = closed; + if( !openForMonsters ) { + aas_area_closed = true; + } + gameLocal.SetAASAreaState( GetPhysics()->GetAbsBounds(), AREACONTENTS_CLUSTERPORTAL | AREACONTENTS_OBSTACLE, aas_area_closed ); +} + +//-------------------------------- +// hhProxDoor::Event_PollForExit +//-------------------------------- +void hhProxDoor::Event_PollForExit() { + if( PollClosestEntity() == -1.f ) { + SetDoorState( PROXSTATE_GoingInactive ); + return; + } + CancelEvents( &EV_PollForExit ); + PostEventMS( &EV_PollForExit, proxDoorRefreshMS ); +} + +//-------------------------------- +// hhProxDoor::PollClosestEntity +//-------------------------------- +float hhProxDoor::PollClosestEntity() { + int num; + int i; + idEntity* ents[MAX_GENTITIES]; + float bestLen; + float distSq; + + if (!doorTrigger) { + return 0.0f; + } + + num = gameLocal.clip.EntitiesTouchingBounds( doorTrigger->GetAbsBounds().Expand( pm_bboxwidth.GetFloat() ), MASK_SHOT_BOUNDINGBOX, ents, MAX_GENTITIES ); + + float thinkDistanceSq = (maxDistance + pm_bboxwidth.GetFloat()) * (maxDistance + pm_bboxwidth.GetFloat()); + bestLen = thinkDistanceSq; + idEntity* bestEnt = NULL; + + for( i = 0; i < num; i++ ) { + if( ents[ i ] && ents[ i ] != this && ents[ i ]->fl.touchTriggers && !ents[ i ]->IsType(hhProxDoorSection::Type) ) { + if( ents[ i ]->IsType(hhPlayer::Type) || ents[ i ]->IsType(hhSpiritProxy::Type) || (openForMonsters && ents[ i ]->IsType(idAI::Type) && ents[ i ]->GetHealth() > 0 ) ) { + if (ents[ i ]->IsType(hhPlayer::Type) && !fl.allowSpiritWalkTouch) { //rww - we don't want spirits to open proxy doors. + hhPlayer *pl = static_cast(ents[i]); + if (pl->IsSpiritWalking()) { + continue; + } + } + distSq = ( ents[ i ]->GetOrigin() - GetOrigin() ).LengthSqr(); + if( distSq < bestLen ) { + bestLen = distSq; + bestEnt = ents[ i ]; + } + } + } + } + if( bestLen < thinkDistanceSq ) { + entDistanceSq = bestLen; + } + else { + entDistanceSq = -1.f; + } + return entDistanceSq; +} + +//-------------------------------- +// hhProxDoor::SetDoorState +//-------------------------------- +void hhProxDoor::SetDoorState( EProxState doorState ) { + int i; + + //HUMANHEAD PCF mdl 04/27/06 - Don't allow locked doors to become active + if ( doorLocked && doorState == PROXSTATE_Active ) { + return; + } + + switch( doorState ) { + case PROXSTATE_Active: + BecomeActive( TH_TICKER ); + CancelEvents( &EV_PollForExit ); + PostEventMS( &EV_PollForExit, 500 ); + OpenPortal(); + break; + + case PROXSTATE_GoingInactive: + break; + + case PROXSTATE_Inactive: + // Guarantee the door is closed + for( i = 0; i < doorPieces.Num(); i++ ) { + if (doorPieces[i].IsValid()) { + doorPieces[ i ]->SetProximity( 0.0 ); + } + } + ClosePortal(); + CancelEvents( &EV_PollForExit ); + BecomeInactive( TH_TICKER ); + break; + } + + proxState = doorState; +} + +//-------------------------------- +// hhProxDoor::IsLocked +//-------------------------------- +bool hhProxDoor::IsLocked() { + return doorLocked; +} + +//-------------------------------- +// hhProxDoor::Lock +//-------------------------------- +void hhProxDoor::Lock( int f ) { + doorLocked = f > 0 ? true : false; + //HUMANHEAD PCF mdl 04/27/06 - Changed PROXSTATE_Inactive to PROXSTATE_GoingInactive + SetDoorState( doorLocked ? PROXSTATE_GoingInactive : PROXSTATE_Active ); + SetShaderParm( SHADERPARM_MODE, GetDoorShaderParm( doorLocked, false ) ); // 2=locked, 1=unlocked, 0=never locked + StopSound( SND_CHANNEL_ANY ); +} + +//-------------------------------- +// hhProxDoor::CrushEntities +//-------------------------------- +void hhProxDoor::CrushEntities() { + int num; + int i; + idEntity* ents[MAX_GENTITIES]; + + num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), MASK_SHOT_BOUNDINGBOX|CONTENTS_RENDERMODEL, ents, MAX_GENTITIES ); + + const char *damageType = spawnArgs.GetString("damageType"); + for( i = 0; i < num; i++ ) { + if( ents[ i ] != this && !ents[ i ]->IsType(hhProxDoorSection::Type) && !ents[ i ]->IsType(hhPlayer::Type) && !ents[ i ]->IsType(idAFAttachment::Type) ) { // Check for the player and idAFAttachment (head) in the case of spirit walk + ents[ i ]->Damage( this, this, vec3_origin, damageType, damage, INVALID_JOINT ); + ents[ i ]->SquishedByDoor( this ); + } + } +} + +//-------------------------------- +// hhProxDoor::Event_Touch +//-------------------------------- +void hhProxDoor::Event_Touch( idEntity *other, trace_t *trace ) { + if ( sndTrigger && trace->c.id == sndTrigger->GetId() ) { + if (other && other->IsType(hhPlayer::Type) && IsLocked() && gameLocal.time > nextSndTriggerTime) { + StartSound("snd_locked", SND_CHANNEL_ANY, 0, false, NULL ); + nextSndTriggerTime = gameLocal.time + 10000; + } + return; + } + if( proxState == PROXSTATE_Active ) { + return; + } + + if ( !other ) { + gameLocal.Warning("hhProxDoor: Event_Touch given NULL for other\n"); + return; + } + + float dist = ( other->GetOrigin() - GetOrigin() ).Length(); + if (dist > movementDistance) { + return; + } + + if( !IsLocked() ) { + SetDoorState( PROXSTATE_Active ); + } +} + +//-------------------------------- +// hhProxDoor::Event_Activate +//-------------------------------- +void hhProxDoor::Event_Activate( idEntity* activator ) { + if( spawnArgs.GetBool("locktrigger") ) { + Lock(!doorLocked); + SetAASAreaState( doorLocked ); + } +} + + +//------------------------------------------------------------------------------------------------- +// hhProxDoorTranslator. +//------------------------------------------------------------------------------------------------- +CLASS_DECLARATION( hhProxDoorSection, hhProxDoorTranslator ) +END_CLASS + +//-------------------------------- +// hhProxDoorTranslator::Spawn +//-------------------------------- +void hhProxDoorTranslator::Spawn( void ) { + idVec3 sectionOffset; + + sectionType = PROXSECTION_Translator; + + if( !spawnArgs.GetVector("section_offset", "0 0 0", sectionOffset) ) { + common->Warning( "No section offset found for '%s'", this->GetEntityDefName() ); + } + targetOrigin = sectionOffset; + baseOrigin = GetOrigin(); + + fl.networkSync = true; +} + +//-------------------------------- +// hhProxDoorTranslator::Save +//-------------------------------- +void hhProxDoorTranslator::Save(idSaveGame *savefile) const { + savefile->WriteVec3( baseOrigin ); + savefile->WriteVec3( targetOrigin ); +} + +//-------------------------------- +// hhProxDoorTranslator::Restore +//-------------------------------- +void hhProxDoorTranslator::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3( baseOrigin ); + savefile->ReadVec3( targetOrigin ); +} + +//-------------------------------- +// hhProxDoorTranslator::ClientPredictionThink +//-------------------------------- +void hhProxDoorTranslator::ClientPredictionThink( void ) { + hhProxDoorSection::ClientPredictionThink(); +} + +//-------------------------------- +// hhProxDoorTranslator::WriteToSnapshot +//-------------------------------- +void hhProxDoorTranslator::WriteToSnapshot( idBitMsgDelta &msg ) const { + //hhProxDoorSection::WriteToSnapshot(msg); +#ifdef _SYNC_PROXDOORS + msg.WriteFloat(baseOrigin.x); + msg.WriteFloat(baseOrigin.y); + msg.WriteFloat(baseOrigin.z); + msg.WriteFloat(targetOrigin.x); + msg.WriteFloat(targetOrigin.y); + msg.WriteFloat(targetOrigin.z); + + if (GetPhysics()->IsType(idPhysics_Static::Type)) { + idPhysics_Static *phys = static_cast(GetPhysics()); + staticPState_t *state = phys->GetPState(); + idCQuat q = state->axis.ToCQuat(); + msg.WriteFloat(q.x); + msg.WriteFloat(q.y); + msg.WriteFloat(q.z); + } + + msg.WriteFloat(proximity); +#else + WriteBindToSnapshot(msg); + msg.WriteBits(proxyParent.GetSpawnId(), 32); +#endif +} + +//-------------------------------- +// hhProxDoorTranslator::ReadFromSnapshot +//-------------------------------- +void hhProxDoorTranslator::ReadFromSnapshot( const idBitMsgDelta &msg ) { + //hhProxDoorSection::ReadFromSnapshot(msg); +#ifdef _SYNC_PROXDOORS + baseOrigin.x = msg.ReadFloat(); + baseOrigin.y = msg.ReadFloat(); + baseOrigin.z = msg.ReadFloat(); + targetOrigin.x = msg.ReadFloat(); + targetOrigin.y = msg.ReadFloat(); + targetOrigin.z = msg.ReadFloat(); + + if (GetPhysics()->IsType(idPhysics_Static::Type)) { + idPhysics_Static *phys = static_cast(GetPhysics()); + staticPState_t *state = phys->GetPState(); + idCQuat q; + q.x = msg.ReadFloat(); + q.y = msg.ReadFloat(); + q.z = msg.ReadFloat(); + state->axis = q.ToMat3(); + } + + float prox = msg.ReadFloat(); + SetProximity(prox); +#else + ReadBindFromSnapshot(msg); + proxyParent.SetSpawnId(msg.ReadBits(32)); + if (proxyParent.IsValid() && proxyParent->IsType(hhProxDoor::Type)) { + hhProxDoor *parentPtr = static_cast(proxyParent.GetEntity()); + + if (parentPtr->hasNetData) { + if (!hasNetData) { + idVec3 parentOrigin; + idMat3 parentAxis; + + parentOrigin = proxyParent->GetOrigin();//proxyParent->spawnArgs.GetVector("origin", "0 0 0", parentOrigin); + parentAxis = proxyParent->GetAxis();//proxyParent->spawnArgs.GetMatrix("rotation", "1 0 0 0 1 0 0 0 1", parentAxis); + + SetOrigin(parentOrigin); + SetAxis(parentAxis); + + baseOrigin = GetOrigin(); + + spawnArgs.SetVector("origin", parentOrigin); + spawnArgs.SetMatrix("rotation", parentAxis); + + hasNetData = true; + } + } + else { + hasNetData = false; + } + } +#endif +} + +//-------------------------------- +// hhProxDoorTranslator::SetProximity +//-------------------------------- +void hhProxDoorTranslator::SetProximity( float prox ) { + SetOrigin( baseOrigin + (targetOrigin * prox) ); + proximity = prox; +} + + +//------------------------------------------------------------------------------------------------- +// hhProxDoorRotator. +//------------------------------------------------------------------------------------------------- +CLASS_DECLARATION( hhProxDoorSection, hhProxDoorRotator ) + EVENT( EV_PostSpawn, hhProxDoorRotator::Event_PostSpawn ) +END_CLASS + +//-------------------------------- +// hhProxDoorRotator::Spawn +//-------------------------------- +void hhProxDoorRotator::Spawn( void ) { + sectionType = PROXSECTION_Rotator; + + if (gameLocal.isMultiplayer) { + if (!gameLocal.isClient) { + PostEventMS( &EV_PostSpawn, 0 ); + fl.networkSync = true; + } + } + else { //just call it now. some sp maps might target a door piece in the spawn function of an entity or something crazy like that. + Event_PostSpawn(); + } +} + +void hhProxDoorRotator::Event_PostSpawn( void ) { + idVec3 sectionOffset; + idVec3 rotVector; + float rotAngle; + + if( !spawnArgs.GetVector("section_offset", "0 0 0", sectionOffset) ) { + common->Warning( "No section offset found for '%s'", this->GetEntityDefName() ); + } + if( !spawnArgs.GetVector("rot_vector", "0 0 0", rotVector) ) { + common->Warning( "No rotation vector found for '%s'", this->GetEntityDefName() ); + } + if( !spawnArgs.GetFloat("rot_angle", "0 0 0", rotAngle) ) { + common->Warning( "No rotation angle found for '%s'", this->GetEntityDefName() ); + } + + idDict args; + args.SetVector( "origin", GetOrigin() + (sectionOffset * GetAxis()) ); + args.SetMatrix( "rotation", GetAxis() ); + args.SetVector( "rot_vector", rotVector ); + args.SetFloat( "rot_angle", rotAngle ); + bindParent = static_cast( gameLocal.SpawnEntityType(hhProxDoorRotMaster::Type, &args) ); + if( !bindParent.IsValid() ) { + common->Warning( "Failed to spawn bindParent for '%s'", GetEntityDefName() ); + } + else { + bindParent->proxyParent = this; + } + Bind( bindParent.GetEntity(), true ); +} + +//-------------------------------- +// hhProxDoorRotator::Save +//-------------------------------- +void hhProxDoorRotator::Save(idSaveGame *savefile) const { + bindParent.Save(savefile); +} + +//-------------------------------- +// hhProxDoorRotator::Restore +//-------------------------------- +void hhProxDoorRotator::Restore( idRestoreGame *savefile ) { + bindParent.Restore(savefile); +} + +//-------------------------------- +// hhProxDoorRotator::ClientPredictionThink +//-------------------------------- +void hhProxDoorRotator::ClientPredictionThink( void ) { + hhProxDoorSection::ClientPredictionThink(); +} + +//-------------------------------- +// hhProxDoorRotator::WriteToSnapshot +//-------------------------------- +void hhProxDoorRotator::WriteToSnapshot( idBitMsgDelta &msg ) const { +// hhProxDoorSection::WriteToSnapshot(msg); +#ifdef _SYNC_PROXDOORS + WriteBindToSnapshot(msg); + if (GetPhysics()->IsType(idPhysics_Static::Type)) { + idPhysics_Static *phys = static_cast(GetPhysics()); + staticPState_t *state = phys->GetPState(); + + msg.WriteFloat(state->origin.x); + msg.WriteFloat(state->origin.y); + msg.WriteFloat(state->origin.z); + msg.WriteFloat(state->localOrigin.x); + msg.WriteFloat(state->localOrigin.y); + msg.WriteFloat(state->localOrigin.z); + idCQuat q = state->localAxis.ToCQuat(); + msg.WriteFloat(q.x); + msg.WriteFloat(q.y); + msg.WriteFloat(q.z); + } +#else + msg.WriteBits(proxyParent.GetSpawnId(), 32); + WriteBindToSnapshot(msg); + msg.WriteBits(bindParent.GetSpawnId(), 32); +#endif + //not needed without prediction + //msg.WriteBits(bindParent.GetSpawnId(), 32); +} + +//-------------------------------- +// hhProxDoorRotator::ReadFromSnapshot +//-------------------------------- +void hhProxDoorRotator::ReadFromSnapshot( const idBitMsgDelta &msg ) { +// hhProxDoorSection::ReadFromSnapshot(msg); +#ifdef _SYNC_PROXDOORS + ReadBindFromSnapshot(msg); + if (GetPhysics()->IsType(idPhysics_Static::Type)) { + idPhysics_Static *phys = static_cast(GetPhysics()); + staticPState_t *state = phys->GetPState(); + + state->origin.x = msg.ReadFloat(); + state->origin.y = msg.ReadFloat(); + state->origin.z = msg.ReadFloat(); + state->localOrigin.x = msg.ReadFloat(); + state->localOrigin.y = msg.ReadFloat(); + state->localOrigin.z = msg.ReadFloat(); + idCQuat q; + q.x = msg.ReadFloat(); + q.y = msg.ReadFloat(); + q.z = msg.ReadFloat(); + state->localAxis = q.ToMat3(); + } +#else + proxyParent.SetSpawnId(msg.ReadBits(32)); + if (proxyParent.IsValid() && proxyParent->IsType(hhProxDoor::Type)) { + hhProxDoor *parentPtr = static_cast(proxyParent.GetEntity()); + + if (parentPtr->hasNetData) { + if (!hasNetData) { + idVec3 parentOrigin; + idMat3 parentAxis; + + parentOrigin = proxyParent->GetOrigin();//proxyParent->spawnArgs.GetVector("origin", "0 0 0", parentOrigin); + parentAxis = proxyParent->GetAxis();//proxyParent->spawnArgs.GetMatrix("rotation", "1 0 0 0 1 0 0 0 1", parentAxis); + + SetOrigin(parentOrigin); + SetAxis(parentAxis); + + spawnArgs.SetVector("origin", parentOrigin); + spawnArgs.SetMatrix("rotation", parentAxis); + + hasNetData = true; + } + } + else { + hasNetData = false; + } + } + ReadBindFromSnapshot(msg); + bindParent.SetSpawnId(msg.ReadBits(32)); +#endif + //not needed without prediction + /* + int spawnId = msg.ReadBits(32); + if (!spawnId) { + bindParent = NULL; + } + else { + bindParent.SetSpawnId(spawnId); + } + */ +} + +//-------------------------------- +// hhProxDoorRotator::SetProximity +//-------------------------------- +void hhProxDoorRotator::SetProximity( float prox ) { + if( bindParent.IsValid() ) { + bindParent->SetProximity( prox ); + } + proximity = prox; +} + +//------------------------------------------------------------------------------------------------- +// hhProxDoorRotMaster. +//------------------------------------------------------------------------------------------------- +CLASS_DECLARATION( hhProxDoorSection, hhProxDoorRotMaster ) +END_CLASS + +//-------------------------------- +// hhProxDoorRotMaster::Spawn +//-------------------------------- +void hhProxDoorRotMaster::Spawn( void ) { + idVec3 rot_vector; + spawnArgs.GetVector("rot_vector", "0 0 1", rot_vector); + baseAxis = GetAxis(); + rotVector = GetAxis() * rot_vector; + + spawnArgs.GetFloat("rot_angle", "0.0", rotAngle); + + fl.networkSync = true; +} + +//-------------------------------- +// hhProxDoorRotMaster::Save +//-------------------------------- +void hhProxDoorRotMaster::Save(idSaveGame *savefile) const { + savefile->WriteVec3( rotVector ); + savefile->WriteFloat( rotAngle ); + savefile->WriteMat3( baseAxis ); +} + +//-------------------------------- +// hhProxDoorRotMaster::Restore +//-------------------------------- +void hhProxDoorRotMaster::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3( rotVector ); + savefile->ReadFloat( rotAngle ); + savefile->ReadMat3( baseAxis ); +} + +//-------------------------------- +// hhProxDoorRotMaster::ClientPredictionThink +//-------------------------------- +void hhProxDoorRotMaster::ClientPredictionThink( void ) { + hhProxDoorSection::ClientPredictionThink(); +} + +//-------------------------------- +// hhProxDoorRotMaster::WriteToSnapshot +//-------------------------------- +void hhProxDoorRotMaster::WriteToSnapshot( idBitMsgDelta &msg ) const { + //hhProxDoorSection::WriteToSnapshot(msg); +#ifdef _SYNC_PROXDOORS + msg.WriteFloat(rotVector.x); + msg.WriteFloat(rotVector.y); + msg.WriteFloat(rotVector.z); + + msg.WriteFloat(rotAngle); + + idCQuat q = baseAxis.ToCQuat(); + msg.WriteFloat(q.x); + msg.WriteFloat(q.y); + msg.WriteFloat(q.z); + + if (GetPhysics()->IsType(idPhysics_Static::Type)) { + idPhysics_Static *phys = static_cast(GetPhysics()); + staticPState_t *state = phys->GetPState(); + msg.WriteFloat(state->origin.x); + msg.WriteFloat(state->origin.y); + msg.WriteFloat(state->origin.z); + } + msg.WriteFloat(proximity); +#else + WriteBindToSnapshot(msg); + msg.WriteBits(proxyParent.GetSpawnId(), 32); +#endif +} + +//-------------------------------- +// hhProxDoorRotMaster::ReadFromSnapshot +//-------------------------------- +void hhProxDoorRotMaster::ReadFromSnapshot( const idBitMsgDelta &msg ) { + //hhProxDoorSection::ReadFromSnapshot(msg); +#ifdef _SYNC_PROXDOORS + rotVector.x = msg.ReadFloat(); + rotVector.y = msg.ReadFloat(); + rotVector.z = msg.ReadFloat(); + + rotAngle = msg.ReadFloat(); + + idCQuat q; + q.x = msg.ReadFloat(); + q.y = msg.ReadFloat(); + q.z = msg.ReadFloat(); + baseAxis = q.ToMat3(); + + if (GetPhysics()->IsType(idPhysics_Static::Type)) { + idPhysics_Static *phys = static_cast(GetPhysics()); + staticPState_t *state = phys->GetPState(); + state->origin.x = msg.ReadFloat(); + state->origin.y = msg.ReadFloat(); + state->origin.z = msg.ReadFloat(); + } + float prox = msg.ReadFloat(); + SetProximity(prox); +#else + ReadBindFromSnapshot(msg); + proxyParent.SetSpawnId(msg.ReadBits(32)); + if (proxyParent.IsValid() && proxyParent->IsType(hhProxDoorRotator::Type)) { + hhProxDoorRotator *parentPtr = static_cast(proxyParent.GetEntity()); + if (parentPtr->hasNetData) { + if (!hasNetData) { + idVec3 sectionOffset, rot_vector; + idVec3 parentOrigin; + idMat3 parentAxis; + + proxyParent->spawnArgs.GetVector("origin", "0 0 0", parentOrigin); + proxyParent->spawnArgs.GetMatrix("rotation", "1 0 0 0 1 0 0 0 1", parentAxis); + + proxyParent->spawnArgs.GetVector("section_offset", "0 0 0", sectionOffset); + proxyParent->spawnArgs.GetVector("rot_vector", "0 0 0", rot_vector); + proxyParent->spawnArgs.GetFloat("rot_angle", "0.0", rotAngle); + SetOrigin(parentOrigin+(sectionOffset*parentAxis)); + SetAxis(parentAxis); + + baseAxis = GetAxis(); + rotVector = GetAxis() * rot_vector; + + hasNetData = true; + } + } + else { + parentPtr->hasNetData = false; + } + } +#endif +} + +//-------------------------------- +// hhProxDoorRotMaster::SetProximity +//-------------------------------- +void hhProxDoorRotMaster::SetProximity( float prox ) { + idRotation desRotation( vec3_origin, rotVector, prox*rotAngle ); + SetAxis( baseAxis * desRotation.ToMat3() ); + + //rww - proxy door pieces keep getting stuck, so make sure this bastard keeps running physics + if (!IsActive(TH_PHYSICS)) { + BecomeActive(TH_PHYSICS); + } + proximity = prox; +} diff --git a/src/Prey/game_proxdoor.h b/src/Prey/game_proxdoor.h new file mode 100644 index 0000000..dda01ad --- /dev/null +++ b/src/Prey/game_proxdoor.h @@ -0,0 +1,204 @@ +#ifndef __PREY_PROXDOOR_H__ +#define __PREY_PROXDOOR_H__ + +class hhProxDoorSection; +typedef idEntityPtr doorSectionPtr_t; //rww - so that the list of door sections can be safe. + +/*********************************************************************** + hhProxDoor. + The main door control. Has a model associated with it (the frame). +***********************************************************************/ +class hhProxDoor : public idEntity { + CLASS_PROTOTYPE( hhProxDoor ); + + public: + hhProxDoor(); + virtual ~hhProxDoor(); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void SpawnSoundTrigger(); + + //rww - netcode + virtual void ClientPredictionThink( void ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual void Ticker( void ); + + //Events. + void Event_Touch( idEntity *other, trace_t *trace ); + void Event_PollForExit( void ); + virtual void Event_PostSpawn( void ); + void Event_Activate( idEntity* activator ); + + //Portal Control. + void OpenPortal( void ); + void ClosePortal( void ); + + //AAS Control. + void SetAASAreaState( bool closed ); + + //Lock Control + bool IsLocked( void ); + void Lock( int f ); + + bool hasNetData; //rww - net stuff + +protected: + enum EProxState { + PROXSTATE_Active, + PROXSTATE_Inactive, + PROXSTATE_GoingInactive, + }; + + enum EPDoorSound { + PDOORSND_Opening, + PDOORSND_Closing, + PDOORSND_Closed, + PDOORSND_Opened, + PDOORSND_FullyOpened, + PDOORSND_Stopped, + }; + + void SetDoorState( EProxState doorState ); + void UpdateSoundState( EPDoorSound newState ); + float PollClosestEntity(); + void CrushEntities(); + +protected: + EProxState proxState; //our state + EPDoorSound doorSndState; //state of our door sound + float lastAmount; //last amount [mainly used for sound] + idList doorPieces; //all of the pieces that make up the door + idClipModel* doorTrigger; //trigger that wakes us up + idClipModel* sndTrigger; //trigger for playing locked sound + int nextSndTriggerTime; // next time to play door locked sound + float entDistanceSq; //squared distance of the closest entity + float maxDistance; //distance of trigger + float movementDistance; //distance to start movement at + float lastDistance; //previous distance + float stopDistance; //distance to stop movement at + float damage; // Amount of crush damage we do + qhandle_t areaPortal; // 0 = no portal, > 0 = our portal we are by + bool doorLocked; + bool openForMonsters; + bool aas_area_closed; + +}; + +/*********************************************************************** + hhProxDoorSection. + Base class for all of the pieces used to make up a hhProxDoor. +***********************************************************************/ +class hhProxDoorSection : public idEntity { + ABSTRACT_PROTOTYPE( hhProxDoorSection ); + +public: + void Spawn( void ); + + void Save( idSaveGame *savefile ) const { savefile->WriteInt(sectionType); savefile->WriteFloat( proximity ); } + void Restore( idRestoreGame *savefile ) { savefile->ReadInt((int&)sectionType); savefile->ReadFloat( proximity ); } + + //rww - netcode + virtual void ClientPredictionThink( void ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual void SetProximity( float prox ) = 0; + + idEntityPtr proxyParent; //rww - for horrible network hacks + + bool hasNetData; //rww - net stuff + + float proximity; +protected: + enum EProxSection { + PROXSECTION_None, + PROXSECTION_Rotator, + PROXSECTION_Translator, + PROXSECTION_RotatorMaster, + }; + + EProxSection sectionType; +}; + +/*********************************************************************** + hhProxDoorRotator. + A rotating door piece. Uses an offset DoorRotMaster to bind to. +***********************************************************************/ +class hhProxDoorRotator : public hhProxDoorSection { + CLASS_PROTOTYPE( hhProxDoorRotator ); + +public: + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - netcode + virtual void ClientPredictionThink( void ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual void SetProximity( float prox ); + + virtual void Event_PostSpawn( void ); + +protected: + idEntityPtr bindParent; +}; + +/*********************************************************************** + hhProxDoorTranslator. + A translating door piece. +***********************************************************************/ +class hhProxDoorTranslator : public hhProxDoorSection { + CLASS_PROTOTYPE( hhProxDoorTranslator ); + +public: + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - netcode + virtual void ClientPredictionThink( void ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual void SetProximity( float prox ); + +protected: + idVec3 baseOrigin; + idVec3 targetOrigin; + + +}; + +/*********************************************************************** + hhProxDoorRotMaster + The master that DoorRotators bind to. +***********************************************************************/ +class hhProxDoorRotMaster : public hhProxDoorSection { + CLASS_PROTOTYPE( hhProxDoorRotMaster ); + +public: + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - netcode + virtual void ClientPredictionThink( void ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual void SetProximity( float prox ); + +protected: + idVec3 rotVector; + float rotAngle; + idMat3 baseAxis; +}; + + +#endif //__PREY_PROXDOOR_H__ \ No newline at end of file diff --git a/src/Prey/game_rail.cpp b/src/Prey/game_rail.cpp new file mode 100644 index 0000000..3d30b9f --- /dev/null +++ b/src/Prey/game_rail.cpp @@ -0,0 +1,85 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/* TODO: + could optionally allow multiple bindcontroller bones, for coop +*/ + +//========================================================================== +// +// hhRailRide +// +//========================================================================== + +CLASS_DECLARATION(hhAnimated, hhRailRide) + EVENT( EV_Activate, hhRailRide::Event_Activate ) + EVENT( EV_BindAttach, hhRailRide::Event_Attach ) + EVENT( EV_BindDetach, hhRailRide::Event_Detach ) +END_CLASS + +void hhRailRide::Spawn() { + fl.takedamage = spawnArgs.GetBool("dropOnPain"); + float tension = spawnArgs.GetFloat("tension", "0.01"); + float yawLimit = spawnArgs.GetFloat("yawlimit", "180"); + float pitchLimit = spawnArgs.GetFloat("pitchlimit", "0.0"); + const char *handName = spawnArgs.GetString("def_hand"); + const char *animName = spawnArgs.GetString("boundanim"); + + // spawn a bind controller at railbone + const char *bonename = spawnArgs.GetString("railbone"); + bindController = static_cast( gameLocal.SpawnObject(spawnArgs.GetString("def_bindController")) ); + assert(bindController); + bindController->MoveToJoint(this, bonename); + bindController->BindToJoint(this, bonename, true); + bindController->SetTension(tension); + bindController->SetRiderParameters(animName, handName, yawLimit, pitchLimit); +} + +void hhRailRide::Save(idSaveGame *savefile) const { + savefile->WriteObject( bindController ); +} + +void hhRailRide::Restore( idRestoreGame *savefile ) { + savefile->ReadObject( reinterpret_cast( bindController ) ); +} + +bool hhRailRide::Pain(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) { + bindController->Detach(); + + return( true ); +} + +void hhRailRide::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + bindController->Detach(); +} + +bool hhRailRide::ValidRider(idEntity *entity) { + return (entity && entity->spawnArgs.GetBool("RailAttachable")); +} + +void hhRailRide::Attach(idEntity *rider, bool loose) { + if (ValidRider(rider)) { + bindController->Attach(rider, loose); + } +} + +void hhRailRide::Detach() { + bindController->Detach(); +} + +void hhRailRide::Event_Activate( idEntity *activator ) { + Attach(activator, false); + hhAnimated::Event_Activate(activator); // allow animation to start +} + +void hhRailRide::Event_Attach(idEntity *rider, bool loose) { + Attach(rider, loose); +} + +void hhRailRide::Event_Detach() { + Detach(); +} + + diff --git a/src/Prey/game_rail.h b/src/Prey/game_rail.h new file mode 100644 index 0000000..4577627 --- /dev/null +++ b/src/Prey/game_rail.h @@ -0,0 +1,41 @@ + +#ifndef __HH_GAME_RAIL_H__ +#define __HH_GAME_RAIL_H__ + +class hhBindController; + +/* +=================================================================================== + + hhRailRide + + An animating object that controls player position and orientation. + Player can look around within some restricted cone of vision. + +=================================================================================== +*/ + +class hhRailRide : public hhAnimated { + +public: + CLASS_PROTOTYPE( hhRailRide ); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual bool Pain(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + void Attach(idEntity *rider, bool loose); + void Detach(); + bool ValidRider(idEntity *entity); + +protected: + void Event_Activate( idEntity *activator ); + void Event_Attach(idEntity *rider, bool loose); + void Event_Detach(); + + hhBindController * bindController; +}; + + +#endif \ No newline at end of file diff --git a/src/Prey/game_railshuttle.cpp b/src/Prey/game_railshuttle.cpp new file mode 100644 index 0000000..81ff2e8 --- /dev/null +++ b/src/Prey/game_railshuttle.cpp @@ -0,0 +1,620 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// +// hhRailShuttle +// +//========================================================================== + +#define VCOCKPIT_PARM_POWER 4 +#define VCOCKPIT_PARM_FIRE 5 +#define VCOCKPIT_PARM_JUMP 6 +#define VCOCKPIT_PARM_MOVE 7 + +const idEventDef EV_SetOrbitRadius("setOrbitRadius", "fd"); +const idEventDef EV_RailShuttle_Jump("railShuttleJump"); +const idEventDef EV_RailShuttle_FinalJump("railShuttleFinalJump"); +const idEventDef EV_RailShuttle_IsJumping("railShuttleIsJumping", NULL, 'f'); +const idEventDef EV_RailShuttle_Disengage("railShuttleDisengage"); +const idEventDef EV_HideArcs(""); +const idEventDef EV_RestorePower(""); +const idEventDef EV_GetCockpit( "getCockpit", "", 'e' ); //rdr + +CLASS_DECLARATION(hhVehicle, hhRailShuttle) + EVENT( EV_SetOrbitRadius, hhRailShuttle::Event_SetOrbitRadius ) + EVENT( EV_PostSpawn, hhRailShuttle::Event_PostSpawn ) + EVENT( EV_RailShuttle_Jump, hhRailShuttle::Event_Jump ) + EVENT( EV_RailShuttle_FinalJump, hhRailShuttle::Event_FinalJump ) + EVENT( EV_RailShuttle_IsJumping, hhRailShuttle::Event_IsJumping ) + EVENT( EV_RailShuttle_Disengage, hhRailShuttle::Event_Disengage ) + EVENT( EV_HideArcs, hhRailShuttle::Event_HideArcs ) + EVENT( EV_RestorePower, hhRailShuttle::Event_RestorePower ) + EVENT( EV_GetCockpit, hhRailShuttle::Event_GetCockpit ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +void hhRailShuttle::Spawn() { + fl.networkSync = true; + + turretPitchLimit = spawnArgs.GetFloat("turretPitchLimit"); + turretYawLimit = spawnArgs.GetFloat("turretYawLimit"); + + defaultRadius = spawnArgs.GetFloat("sphereRadius", "1000"); + sphereRadius.Init(gameLocal.time, 0, defaultRadius, defaultRadius); + + sphereCenter = spawnArgs.GetVector("sphereCenter"); + sphereAngles = ang_zero; + sphereVelocity = ang_zero; + spherePitchMin = spawnArgs.GetFloat("spherePitchMin"); + spherePitchMax = spawnArgs.GetFloat("spherePitchMax"); + + SetPhysics( NULL ); // No collision, no expensive shuttle physics + GetPhysics()->SetContents(0); + + linearFriction = spawnArgs.GetFloat("friction_linear"); + terminalVelocity = spawnArgs.GetFloat("terminalVelocity"); + + bJumping = false; + bDisengaged = false; + jumpStartTime = 0; + jumpInitSpeed = spawnArgs.GetFloat("jumpInitSpeed"); + jumpAccel = spawnArgs.GetFloat("jumpAccel"); + jumpAccelTime = spawnArgs.GetFloat("jumpAccelTime"); + jumpReturnForce = spawnArgs.GetFloat("jumpReturnForce"); + jumpBounceCount = spawnArgs.GetInt("jumpBounceCount"); + jumpBounceFactor = spawnArgs.GetFloat("jumpBounceFactor"); + jumpThrustMovementScale = spawnArgs.GetFloat("jumpThrustMovementScale"); + jumpBounceMovementScale = spawnArgs.GetFloat("jumpBounceMovementScale"); + bWallMovementSoundPlaying = false; + bAirMovementSoundPlaying = false; + bBounced = false; + + localViewAngles = ang_zero; + turret = NULL; + canopy = NULL; + leftArc = NULL; + rightArc = NULL; + + //HUMANHEAD PCF mdl 04/29/06 - Added bUpdateViewAngles to prevent view angles from changing on load + bUpdateViewAngles = false; + + PostEventMS(&EV_PostSpawn, 0); +} + +void hhRailShuttle::Event_PostSpawn() { + // Spawn the turret + turret = gameLocal.SpawnObject(spawnArgs.GetString("def_turret")); + if (turret.IsValid()) { + turret->SetOrigin( GetOrigin() + spawnArgs.GetVector("offset_turret") * GetAxis()); + turret->SetAxis(GetAxis()); + turret->Bind(this, false); + turret->GetPhysics()->SetContents(0); + turret->Hide(); + } + // Spawn the canopy + canopy = gameLocal.SpawnObject(spawnArgs.GetString("def_turret")); + if (canopy.IsValid()) { + canopy->SetOrigin( GetOrigin() + spawnArgs.GetVector("offset_turret") * GetAxis()); + canopy->SetAxis(GetAxis()); + canopy->Bind(this, true); + canopy->GetPhysics()->SetContents(0); //temp: need to make it turn solid after player enters + canopy->Hide(); + } + + // Spawn the arc beams + idVec3 leftPos1 = GetOrigin() + spawnArgs.GetVector("offset_leftArcStart") * GetAxis(); + idVec3 leftPos2 = GetOrigin() + spawnArgs.GetVector("offset_leftArcEnd") * GetAxis(); + idVec3 leftDir = leftPos2 - leftPos1; + leftDir.Normalize(); + leftArc = hhBeamSystem::SpawnBeam(leftPos1, spawnArgs.GetString("beam_arc"), mat3_identity, true); + HH_ASSERT( leftArc.IsValid() ); + leftArc->SetOrigin(leftPos1); + leftArc->SetAxis(leftDir.ToMat3()); + leftArc->Bind(turret.GetEntity(), true); + leftArc->Activate( false ); + leftArc->fl.neverDormant = true; + leftArc->ToggleBeamLength(true); + leftArc->SetTargetLocation( leftPos2 ); + + idVec3 rightPos1 = GetOrigin() + spawnArgs.GetVector("offset_rightArcStart") * GetAxis(); + idVec3 rightPos2 = GetOrigin() + spawnArgs.GetVector("offset_rightArcEnd") * GetAxis(); + idVec3 rightDir = rightPos2 - rightPos1; + rightDir.Normalize(); + rightArc = hhBeamSystem::SpawnBeam(rightPos1, spawnArgs.GetString("beam_arc"), mat3_identity, true); + HH_ASSERT( rightArc.IsValid() ); + rightArc->SetOrigin(rightPos1); + rightArc->SetAxis(rightDir.ToMat3()); + rightArc->Bind(turret.GetEntity(), true); + rightArc->Activate( false ); + rightArc->fl.neverDormant = true; + rightArc->ToggleBeamLength(true); + rightArc->SetTargetLocation( rightPos2 ); +} + +void hhRailShuttle::Save(idSaveGame *savefile) const { + savefile->WriteFloat( defaultRadius ); + + savefile->WriteFloat( sphereRadius.GetStartTime() ); // idInterpolate + savefile->WriteFloat( sphereRadius.GetDuration() ); + savefile->WriteFloat( sphereRadius.GetStartValue() ); + savefile->WriteFloat( sphereRadius.GetEndValue() ); + + savefile->WriteAngles( sphereAngles ); + savefile->WriteAngles( sphereVelocity ); + savefile->WriteAngles( sphereAcceleration ); + savefile->WriteVec3( sphereCenter ); + savefile->WriteFloat( spherePitchMin ); + savefile->WriteFloat( spherePitchMax ); + savefile->WriteFloat( turretYawLimit ); + savefile->WriteFloat( turretPitchLimit ); + savefile->WriteFloat( linearFriction ); + savefile->WriteFloat( terminalVelocity ); + savefile->WriteAngles( localViewAngles ); + savefile->WriteAngles( oldViewAngles ); + + turret.Save( savefile ); + canopy.Save( savefile ); + leftArc.Save( savefile ); + rightArc.Save( savefile ); + + savefile->WriteBool( bDisengaged ); + savefile->WriteBool( bJumping ); + savefile->WriteBool( bBounced ); + savefile->WriteInt( jumpStartTime ); + savefile->WriteInt( jumpStage ); + savefile->WriteFloat( jumpSpeed ); + savefile->WriteFloat( jumpPosition ); + savefile->WriteInt( jumpNumBounces ); + savefile->WriteFloat( jumpInitSpeed ); + savefile->WriteFloat( jumpAccel ); + savefile->WriteFloat( jumpAccelTime ); + savefile->WriteFloat( jumpReturnForce ); + savefile->WriteInt( jumpBounceCount ); + savefile->WriteFloat( jumpBounceFactor ); + savefile->WriteFloat( jumpThrustMovementScale ); + savefile->WriteFloat( jumpBounceMovementScale ); + savefile->WriteBool( bWallMovementSoundPlaying ); + savefile->WriteBool( bAirMovementSoundPlaying ); +} + +void hhRailShuttle::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( defaultRadius ); + + float set; + savefile->ReadFloat( set ); // idInterpolate + sphereRadius.SetStartTime( set ); + savefile->ReadFloat( set ); + sphereRadius.SetDuration( set ); + savefile->ReadFloat( set ); + sphereRadius.SetStartValue( set ); + savefile->ReadFloat( set ); + sphereRadius.SetEndValue( set ); + + savefile->ReadAngles( sphereAngles ); + savefile->ReadAngles( sphereVelocity ); + savefile->ReadAngles( sphereAcceleration ); + savefile->ReadVec3( sphereCenter ); + savefile->ReadFloat( spherePitchMin ); + savefile->ReadFloat( spherePitchMax ); + savefile->ReadFloat( turretYawLimit ); + savefile->ReadFloat( turretPitchLimit ); + savefile->ReadFloat( linearFriction ); + savefile->ReadFloat( terminalVelocity ); + savefile->ReadAngles( localViewAngles ); + savefile->ReadAngles( oldViewAngles ); + + turret.Restore( savefile ); + canopy.Restore( savefile ); + leftArc.Restore( savefile ); + rightArc.Restore( savefile ); + + savefile->ReadBool( bDisengaged ); + savefile->ReadBool( bJumping ); + savefile->ReadBool( bBounced ); + savefile->ReadInt( jumpStartTime ); + savefile->ReadInt( jumpStage ); + savefile->ReadFloat( jumpSpeed ); + savefile->ReadFloat( jumpPosition ); + savefile->ReadInt( jumpNumBounces ); + savefile->ReadFloat( jumpInitSpeed ); + savefile->ReadFloat( jumpAccel ); + savefile->ReadFloat( jumpAccelTime ); + savefile->ReadFloat( jumpReturnForce ); + savefile->ReadInt( jumpBounceCount ); + savefile->ReadFloat( jumpBounceFactor ); + savefile->ReadFloat( jumpThrustMovementScale ); + savefile->ReadFloat( jumpBounceMovementScale ); + savefile->ReadBool( bWallMovementSoundPlaying ); + savefile->ReadBool( bAirMovementSoundPlaying ); + + SetPhysics( NULL ); // No collision, no expensive shuttle physics + GetPhysics()->SetContents(0); + + //HUMANHEAD PCF mdl 04/29/06 - Added bUpdateViewAngles to prevent view angles from changing on load + bUpdateViewAngles = true; +} + +void hhRailShuttle::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhVehicle::WriteToSnapshot(msg); +} + +void hhRailShuttle::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhVehicle::ReadFromSnapshot(msg); +} + +void hhRailShuttle::ClientPredictionThink( void ) { + Think(); + + UpdateVisuals(); + Present(); +} + +void hhRailShuttle::BecomeConsole() { + if (turret.IsValid()) { + turret->Hide(); + } + if (canopy.IsValid()) { + canopy->Hide(); + } + if (leftArc.IsValid()) { + leftArc->Activate( false ); + } + if (rightArc.IsValid()) { + rightArc->Activate( false ); + } + hhVehicle::BecomeConsole(); +} + +void hhRailShuttle::BecomeVehicle() { + if (turret.IsValid()) { + turret->Show(); + } + if (canopy.IsValid()) { + canopy->Show(); + } + + hhVehicle::BecomeVehicle(); +} + +// Static function to determine if a pilot is suitable +bool hhRailShuttle::ValidPilot( idActor *act ) { + if (act && act->health > 0) { + if (act->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast(act); + if( !player->IsSpiritOrDeathwalking() && !player->IsPossessed() ) { + return true; + } + } + else if (act->IsType(idAI::Type)) { + idAI *ai = static_cast(act); + if (ai->spawnArgs.GetBool("canPilotShuttle")) { + return true; + } + } + } + return false; +} + +bool hhRailShuttle::WillAcceptPilot( idActor *act ) { + return IsConsole() && hhRailShuttle::ValidPilot( act ); +} + +void hhRailShuttle::AcceptPilot( hhPilotVehicleInterface* pilotInterface ) { + hhVehicle::AcceptPilot(pilotInterface); + + // supress model in player views, but allow it in mirrors and remote views + if (turret.IsValid() && GetPilot()->IsType(hhPlayer::Type)) { + turret->GetRenderEntity()->suppressSurfaceInViewID = GetPilot()->entityNumber + 1; + } + + oldViewAngles = GetPilot()->viewAxis.ToAngles(); +} + +idVec3 hhRailShuttle::GetFireOrigin() { + return turret->GetOrigin(); +} + +idMat3 hhRailShuttle::GetFireAxis() { + return turret->GetAxis(); +} + +void hhRailShuttle::ProcessPilotInput( const usercmd_t* cmds, const idAngles* viewAngles ) { + float movementScale; + + if ( cmds ) { + ProcessButtons( *cmds ); + + //Check vehicle here because we could have exited the vehicle in ProcessButtons + if( !IsVehicle() ) { + return; + } + + ProcessImpulses( *cmds ); + + // Apply booster to allow key taps to be very low thrust + thrustScale = idMath::ClampFloat( thrustMin, thrustMax, thrustScale * thrustAccel ); + + // Apply our forces as orbit velocity + if ((cmds->rightmove || cmds->upmove || cmds->forwardmove) && HasPower(1) && !bDisengaged) { + sphereAcceleration.yaw = cmds->rightmove * thrustFactor * thrustScale * (60.0f * USERCMD_ONE_OVER_HZ); + sphereAcceleration.pitch = (cmds->upmove ? cmds->upmove : cmds->forwardmove) * thrustFactor * thrustScale * (60.0f * USERCMD_ONE_OVER_HZ); + sphereAcceleration.roll = 0; + sphereAcceleration += -sphereVelocity * linearFriction * (60.0f * USERCMD_ONE_OVER_HZ); + SetCockpitParm( VCOCKPIT_PARM_MOVE, -MS2SEC( gameLocal.time ) ); + if (bJumping) { + PlayAirMovementSound(); + } + else { + PlayWallMovementSound(); + } + } + else { + sphereAcceleration = -sphereVelocity * linearFriction * (60.0f * USERCMD_ONE_OVER_HZ); + thrustScale = thrustMin; + if (bJumping) { + StopAirMovementSound(); + } + else { + StopWallMovementSound(); + } + } + + sphereVelocity += sphereAcceleration * (60.0f * USERCMD_ONE_OVER_HZ); + sphereVelocity.pitch = idMath::ClampFloat(-terminalVelocity, terminalVelocity, sphereVelocity.pitch); + sphereVelocity.yaw = idMath::ClampFloat(-terminalVelocity, terminalVelocity, sphereVelocity.yaw); + + movementScale = bJumping ? ( bBounced ? jumpBounceMovementScale : jumpThrustMovementScale ) : 1.f; + sphereAngles += sphereVelocity * movementScale * (60.0f * USERCMD_ONE_OVER_HZ); + if (sphereAngles.pitch <= spherePitchMin || sphereAngles.pitch >= spherePitchMax) { + sphereAngles.pitch = idMath::ClampFloat(spherePitchMin, spherePitchMax, sphereAngles.pitch); + sphereVelocity.pitch = 0; + } + sphereAngles = sphereAngles.Normalize180(); + + float radius = sphereRadius.GetCurrentValue(gameLocal.time) - UpdateJump( cmds ); + + idMat3 sphereAxis = sphereAngles.ToMat3(); + SetOrigin(sphereCenter - sphereAxis[0] * radius); + SetAxis(sphereAxis); + + memcpy( &oldCmds, cmds, sizeof(usercmd_t) ); + } + + // Apply viewAngles to turret + if ( viewAngles ) { + //HUMANHEAD PCF mdl 04/29/06 - Added bUpdateViewAngles to prevent view angles from changing on load + if ( bUpdateViewAngles ) { + // Update the old view angles after loading from a savegame + bUpdateViewAngles = false; + } else { + idAngles deltaViewAngles; + deltaViewAngles = *viewAngles - oldViewAngles; + localViewAngles += deltaViewAngles; + + // Clamp localViewAngles + if ( !IsNoClipping() ) { + localViewAngles.pitch = idMath::AngleNormalize180( localViewAngles.pitch ); + localViewAngles.pitch = idMath::ClampFloat(-turretPitchLimit, turretPitchLimit, localViewAngles.pitch); + localViewAngles.pitch = idMath::AngleNormalize180( localViewAngles.pitch ); + + localViewAngles.yaw = idMath::AngleNormalize180( localViewAngles.yaw ); + localViewAngles.yaw = idMath::ClampFloat(-turretYawLimit, turretYawLimit, localViewAngles.yaw); + localViewAngles.yaw = idMath::AngleNormalize180( localViewAngles.yaw ); + } + + idAngles turretAngles = sphereAngles + localViewAngles; + turret->SetAxis( turretAngles.ToMat3() ); + } + + memcpy( &oldViewAngles, viewAngles, sizeof(idAngles) ); + } +} + +/* +float hhRailShuttle::UpdateJump() { + float jumpDist; + + jumpDist = 0; + if( bJumping ) { + float elapsedTime = MS2SEC( gameLocal.time - jumpStartTime ); + float jumpDuration = spawnArgs.GetFloat( "jumpDuration" ); + if( elapsedTime < jumpDuration ) { + jumpDist = sin( elapsedTime * idMath::PI / jumpDuration ) * spawnArgs.GetFloat( "jumpDistance" ); + } + else { + bJumping = false; + } + } + return jumpDist; +} +*/ + +float hhRailShuttle::UpdateJump( const usercmd_t* cmds ) { + float elapsedTime; + + if( !bJumping) { + return 0; + } + + elapsedTime = MS2SEC( gameLocal.time - jumpStartTime ); + if( jumpStage == 1 ) { + jumpSpeed += jumpAccel * (60.0f * USERCMD_ONE_OVER_HZ); + if( elapsedTime > jumpAccelTime ) { + jumpStage = 2; + } + } + jumpSpeed -= jumpReturnForce * (60.0f * USERCMD_ONE_OVER_HZ); + jumpPosition += jumpSpeed * (60.0f * USERCMD_ONE_OVER_HZ); + if( jumpPosition < 0 ) { + jumpPosition = 0; + if( ++jumpNumBounces > jumpBounceCount ) { + bJumping = false; + StopAirMovementSound(); + StartSound( "snd_jumpland", SND_CHANNEL_ANY ); + } + else { + bBounced = true; + jumpSpeed = -jumpSpeed * jumpBounceFactor; + StartSound( "snd_jumpbounce", SND_CHANNEL_ANY ); + if( (cmds->buttons & BUTTON_ATTACK_ALT) && sphereRadius.IsDone(gameLocal.time) && HasPower(1) && !bDisengaged ) { + Jump(); + } + } + } + SetCockpitParm( VCOCKPIT_PARM_JUMP, jumpPosition ); + return jumpPosition; +} + +void hhRailShuttle::PlayWallMovementSound() { + if (!bWallMovementSoundPlaying) { + StartSound("snd_wallmovestart", SND_CHANNEL_ANY, 0, true); + StartSound("snd_wallmove", SND_CHANNEL_MISC5, 0, true); + bWallMovementSoundPlaying = true; + } +} + +void hhRailShuttle::StopWallMovementSound() { + if (bWallMovementSoundPlaying) { + StopSound(SND_CHANNEL_MISC5, true); + if (!bJumping) { + StartSound("snd_wallmovestop", SND_CHANNEL_ANY, 0, true); + } + bWallMovementSoundPlaying = false; + } +} + +void hhRailShuttle::PlayAirMovementSound() { + if (!bAirMovementSoundPlaying) { + StartSound("snd_thrust", SND_CHANNEL_THRUSTERS, 0, true); + bAirMovementSoundPlaying = true; + } +} + +void hhRailShuttle::StopAirMovementSound() { + if (bAirMovementSoundPlaying) { + StopSound(SND_CHANNEL_THRUSTERS, true); + bAirMovementSoundPlaying = false; + } +} + +void hhRailShuttle::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if( !HasPower(1) || bDisengaged ) { + return; + } + if (!InGodMode()) { + if (idStr::Icmp(damageDefName, "damage_railbeam") == 0) { + ConsumePower(currentPower); + PostEventMS(&EV_RestorePower, SEC2MS(spawnArgs.GetFloat("powerOutageDuration"))); + StartSound( "snd_damagestart", SND_CHANNEL_ANY ); + StartSound( "snd_damageloop", SND_CHANNEL_MISC3, 0, true ); + + idEntity *ent = gameLocal.FindEntity( spawnArgs.GetString( "triggerDamage" ) ); + if( ent ) { + ent->PostEventMS( &EV_Activate, 0, inflictor ); + } + + SetCockpitParm( VCOCKPIT_PARM_POWER, -MS2SEC( gameLocal.time ) ); + } + } + //rww - don't want this thing to ever take real damage + //hhVehicle::Damage( inflictor, attacker, dir, damageDefName, 1.0f, location ); +} + +void hhRailShuttle::Event_SetOrbitRadius(float radius, int lerpTime) { + defaultRadius = radius; + float currentRadius = sphereRadius.GetCurrentValue(gameLocal.time); + sphereRadius.Init(gameLocal.time, lerpTime, currentRadius, defaultRadius); + bJumping = false; // Cancel any jumping +} + +void hhRailShuttle::Event_Jump() { + if (!bJumping && sphereRadius.IsDone(gameLocal.time) && HasPower(1) && !bDisengaged) { + Jump(); + } +} + +void hhRailShuttle::Event_FinalJump() { + jumpInitSpeed = spawnArgs.GetFloat("endingInitSpeed"); + jumpAccel = spawnArgs.GetFloat("endingAccel"); + jumpAccelTime = spawnArgs.GetFloat("endingAccelTime"); + jumpReturnForce = spawnArgs.GetFloat("endingReturnForce"); + Jump(); +} + +void hhRailShuttle::Event_IsJumping() { + idThread::ReturnFloat(bJumping); +} + +void hhRailShuttle::Event_Disengage() { + bDisengaged = true; +} + +void hhRailShuttle::Jump() { + bJumping = true; + bBounced = false; + jumpStartTime = gameLocal.time; + jumpStage = 1; + jumpSpeed = jumpInitSpeed; + jumpPosition = 0; + jumpNumBounces = 0; + StopWallMovementSound(); + if( !bDisengaged ) { + StartSound( "snd_jump", SND_CHANNEL_MISC2 ); + } +} + +void hhRailShuttle::Event_FireCannon() { + if( HasPower(1) && !bDisengaged && fireController && fireController->LaunchProjectiles(vec3_zero) ) { + StartSound( "snd_cannon", SND_CHANNEL_ANY ); + fireController->MuzzleFlash(); + fireController->WeaponFeedback(); + SetCockpitParm( VCOCKPIT_PARM_FIRE, -MS2SEC( gameLocal.time ) ); + + idEntity *ent = gameLocal.FindEntity( spawnArgs.GetString( "triggerFire" ) ); + if( ent ) { + ent->PostEventMS( &EV_Activate, 0, this ); + } + + leftArc->Activate( true ); + rightArc->Activate( true ); + + CancelEvents(&EV_HideArcs); + PostEventMS(&EV_HideArcs, spawnArgs.GetFloat("arcDuration")); + } +} + +void hhRailShuttle::Event_HideArcs() { + leftArc->Activate( false ); + rightArc->Activate( false ); +} + +void hhRailShuttle::Event_RestorePower() { + StopSound( SND_CHANNEL_MISC3, true ); + StartSound( "snd_damagestop", SND_CHANNEL_ANY ); + GivePower(100); +} + +void hhRailShuttle::SetCockpitParm( int parmNumber, float value ) { + if( GetPilotInterface() && GetPilotInterface()->IsType( hhPlayerVehicleInterface::Type ) ) { + hhControlHand *hand = static_cast(GetPilotInterface())->GetHandEntity(); + if( hand ) { + hand->SetShaderParm( parmNumber, value ); + } + } +} + +void hhRailShuttle::Event_GetCockpit() { + if( GetPilotInterface() && GetPilotInterface()->IsType( hhPlayerVehicleInterface::Type ) ) { + hhControlHand *hand = static_cast(GetPilotInterface())->GetHandEntity(); + if( hand ) { + idThread::ReturnEntity( hand ); + } else { + idThread::ReturnEntity( NULL ); + } + } +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/game_railshuttle.h b/src/Prey/game_railshuttle.h new file mode 100644 index 0000000..ecf2101 --- /dev/null +++ b/src/Prey/game_railshuttle.h @@ -0,0 +1,106 @@ +//========================================================================== +// +// hhShuttle +// +//========================================================================== + +class hhRailShuttle : public hhVehicle { + CLASS_PROTOTYPE( hhRailShuttle ); + +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + virtual void Event_FireCannon() {}; + void Event_HideArcs() {}; + void Event_PostSpawn() {}; + void Event_SetOrbitRadius(float radius, int lerpTime) {}; + void Event_Jump() {}; + void Event_FinalJump() {}; + void Event_IsJumping() {}; + void Event_Disengage() {}; + void Event_RestorePower() {}; + void Event_GetCockpit() {}; + idEntity * GetTurret() { return NULL; } + virtual bool WillAcceptPilot( idActor *act ) { return false; } +#else + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + + // Static pilot assessor, for queries when you don't have a vehicle instantiated + static bool ValidPilot( idActor *act ); + virtual bool WillAcceptPilot( idActor *act ); + virtual void AcceptPilot( hhPilotVehicleInterface* pilotInterface ); + virtual void ProcessPilotInput( const usercmd_t* cmds, const idAngles* viewAngles ); + virtual void BecomeConsole(); + virtual void BecomeVehicle(); + idEntity * GetTurret() { return turret.GetEntity(); } + + virtual idVec3 GetFireOrigin(); // Called by firecontroller + virtual idMat3 GetFireAxis(); // Called by firecontroller + +protected: + void PlayWallMovementSound(); + void StopWallMovementSound(); + void PlayAirMovementSound(); + void StopAirMovementSound(); + virtual void Event_FireCannon(); + void Event_HideArcs(); + void Event_PostSpawn(); + void Event_SetOrbitRadius(float radius, int lerpTime); + void Event_Jump(); + void Event_FinalJump(); + void Event_IsJumping(); + void Event_Disengage(); + void Jump(); + void Event_RestorePower(); + float UpdateJump( const usercmd_t* cmds ); + void SetCockpitParm( int parmNumber, float value ); + void Event_GetCockpit(); + +protected: + float defaultRadius; + idInterpolate sphereRadius; + idAngles sphereAngles; + idAngles sphereVelocity; + idAngles sphereAcceleration; + idVec3 sphereCenter; + float spherePitchMin; + float spherePitchMax; + float turretYawLimit; + float turretPitchLimit; + float linearFriction; + float terminalVelocity; + idAngles localViewAngles; + idAngles oldViewAngles; + idEntityPtr turret; + idEntityPtr canopy; + idEntityPtr leftArc; + idEntityPtr rightArc; + bool bDisengaged; + bool bJumping; + bool bBounced; + int jumpStartTime; + int jumpStage; + float jumpSpeed; + float jumpPosition; + int jumpNumBounces; + float jumpInitSpeed; + float jumpAccel; + float jumpAccelTime; + float jumpReturnForce; + int jumpBounceCount; + float jumpBounceFactor; + float jumpThrustMovementScale; + float jumpBounceMovementScale; + bool bWallMovementSoundPlaying; + bool bAirMovementSoundPlaying; + //HUMANHEAD PCF mdl 04/29/06 - Added bUpdateViewAngles to prevent view angles from changing on load + bool bUpdateViewAngles; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; diff --git a/src/Prey/game_renderentity.cpp b/src/Prey/game_renderentity.cpp new file mode 100644 index 0000000..ae8211a --- /dev/null +++ b/src/Prey/game_renderentity.cpp @@ -0,0 +1,141 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_ModelDefHandleIsValid( "" ); + +CLASS_DECLARATION( idEntity, hhRenderEntity ) + EVENT( EV_ModelDefHandleIsValid, hhRenderEntity::Event_ModelDefHandleIsValid ) +END_CLASS + +/* +============== +hhRenderEntity::hhRenderEntity +============== +*/ +hhRenderEntity::hhRenderEntity() { + combatModel = NULL; +} + +/* +============== +hhRenderEntity::~hhRenderEntity +============== +*/ +hhRenderEntity::~hhRenderEntity() { + SAFE_DELETE_PTR( combatModel ); +} + +void hhRenderEntity::Save(idSaveGame *savefile) const { + savefile->WriteClipModel( combatModel ); +} + +void hhRenderEntity::Restore( idRestoreGame *savefile ) { + savefile->ReadClipModel( combatModel ); + + if( combatModel ) { + const renderEntity_t *renderEntity = gameRenderWorld->GetRenderEntity( GetModelDefHandle() ); + if( renderEntity ) { + combatModel->Link( gameLocal.clip, this, 0, renderEntity->origin, renderEntity->axis, GetModelDefHandle() ); + } + } +} + +/* +============== +hhRenderEntity::Think +============== +*/ +void hhRenderEntity::Think() { + idEntity::Think(); + + LinkCombatModel( this, GetModelDefHandle() ); +} + +/* +============== +hhRenderEntity::InitCombatModel +============== +*/ +void hhRenderEntity::InitCombatModel( const int renderModelHandle ) { + if ( combatModel ) { + combatModel->Unlink(); + combatModel->LoadModel( renderModelHandle ); + combatModel->Link( gameLocal.clip );//Force a reclip because our origin and axis hasn't changed + } else { + combatModel = new idClipModel( renderModelHandle ); + HH_ASSERT( combatModel ); + } +} + +/* +============== +hhRenderEntity::LinkCombatModel +============== +*/ +void hhRenderEntity::LinkCombatModel( idEntity* self, const int renderModelHandle ) { + if( combatModel && self && renderModelHandle != -1 ) { + const renderEntity_t *renderEntity = gameRenderWorld->GetRenderEntity( renderModelHandle ); + if( !renderEntity ) { + return; + } + + if( combatModel->GetOrigin() != renderEntity->origin || combatModel->GetAxis() != renderEntity->axis || combatModel->GetBounds() != renderEntity->bounds ) { + combatModel->Link( gameLocal.clip, self, 0, renderEntity->origin, renderEntity->axis, renderModelHandle ); + } + } +} + +/* +============== +hhRenderEntity::Present + +AOBMERGE: PLEASE REMOVE THIS WHEN WE GET idRenderEntity +============== +*/ +void hhRenderEntity::Present( void ) { + PROFILE_SCOPE("Present", PROFMASK_NORMAL); + + if ( !gameLocal.isNewFrame ) { + return; + } + + // don't present to the renderer if the entity hasn't changed + if ( !( thinkFlags & TH_UPDATEVISUALS ) ) { + return; + } + BecomeInactive( TH_UPDATEVISUALS ); + + // camera target for remote render views + if ( cameraTarget && gameLocal.InPlayerPVS( this ) ) { + renderEntity.remoteRenderView = cameraTarget->GetRenderView(); + } + + // if set to invisible, skip + if ( !renderEntity.hModel || IsHidden() ) { + return; + } + + // add to refresh list + if ( modelDefHandle == -1 ) { + modelDefHandle = gameRenderWorld->AddEntityDef( &renderEntity ); + //HUMANHEAD: aob - needed for combat models + PostEventMS( &EV_ModelDefHandleIsValid, 0 ); + //HUMANHEAD END + } else { + gameRenderWorld->UpdateEntityDef( modelDefHandle, &renderEntity ); + } +} + +/* +============== +hhRenderEntity::Event_ModelDefHandleIsValid +============== +*/ +void hhRenderEntity::Event_ModelDefHandleIsValid() { + if( spawnArgs.GetBool("useCombatModel") ) { + InitCombatModel( GetModelDefHandle() ); + LinkCombatModel( this, GetModelDefHandle() ); + } +} \ No newline at end of file diff --git a/src/Prey/game_renderentity.h b/src/Prey/game_renderentity.h new file mode 100644 index 0000000..f867d88 --- /dev/null +++ b/src/Prey/game_renderentity.h @@ -0,0 +1,31 @@ +#ifndef __GAME_RENDERENTITY_H +#define __GAME_RENDERENTITY_H + +extern const idEventDef EV_ModelDefHandleIsValid; + +class hhRenderEntity : public idEntity { + CLASS_PROTOTYPE( hhRenderEntity ); + + protected: + hhRenderEntity(); + virtual ~hhRenderEntity(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think(); + + virtual void InitCombatModel( const int renderModelHandle ); + virtual void LinkCombatModel( idEntity* self, const int renderModelHandle ); + + public: + virtual void Present(); + + protected: + void Event_ModelDefHandleIsValid(); + + protected: + idClipModel* combatModel; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_safeDeathVolume.cpp b/src/Prey/game_safeDeathVolume.cpp new file mode 100644 index 0000000..cf2bf21 --- /dev/null +++ b/src/Prey/game_safeDeathVolume.cpp @@ -0,0 +1,136 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//----------------------------------------------------------------------- +// +// hhSafeResurrectionVolume +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION( idEntity, hhSafeResurrectionVolume ) + EVENT( EV_Enable, hhSafeResurrectionVolume::Event_Enable ) + EVENT( EV_Disable, hhSafeResurrectionVolume::Event_Disable ) +END_CLASS + +/* +=============== +hhSafeResurrectionVolume::Spawn +=============== +*/ +void hhSafeResurrectionVolume::Spawn() { + PostEventMS( (spawnArgs.GetBool("enabled", "1")) ? &EV_Enable : &EV_Disable, 0); +} + +/* +=============== +hhSafeResurrectionVolume::PickRandomPoint +=============== +*/ +void hhSafeResurrectionVolume::PickRandomPoint( idVec3& origin, idMat3& axis ) { + idEntity* entity = PickRandomTarget(); + + if ( !entity ) { // Error message that means something to the designers - cjr + gameLocal.Error( "hhSafeResurrectionZone::PickRandomPoint: No targets found within resurrection zone.\n" ); + } + + origin = entity->GetOrigin(); + axis = entity->GetAxis(); +} + +/* +=============== +hhSafeResurrectionVolume::DetermineContents +=============== +*/ +int hhSafeResurrectionVolume::DetermineContents() const { + return CONTENTS_DEATHVOLUME; +} + +/* +=============== +hhSafeResurrectionVolume::Event_Enable +=============== +*/ +void hhSafeResurrectionVolume::Event_Enable() { + GetPhysics()->EnableClip(); + GetPhysics()->SetContents( DetermineContents() ); +} + +/* +=============== +hhSafeResurrectionVolume::Event_Disable +=============== +*/ +void hhSafeResurrectionVolume::Event_Disable() { + GetPhysics()->DisableClip(); + GetPhysics()->SetContents( 0 ); +} + +//----------------------------------------------------------------------- +// +// hhSafeDeathVolume +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION( hhSafeResurrectionVolume, hhSafeDeathVolume ) + EVENT( EV_Touch, hhSafeDeathVolume::Event_Touch ) +END_CLASS + +/* +=============== +hhSafeDeathVolume::Spawn +=============== +*/ +void hhSafeDeathVolume::Spawn() { + GetPhysics()->SetContents( DetermineContents() ); +} + +/* +=============== +hhSafeDeathVolume::DetermineContents +=============== +*/ +int hhSafeDeathVolume::DetermineContents() const { + return CONTENTS_DEATHVOLUME | CONTENTS_TRIGGER; +} + +/* +=============== +hhSafeDeathVolume::IsValid +=============== +*/ +bool hhSafeDeathVolume::IsValid( const hhPlayer* player ) { + if( !player || player->IsDeathWalking() || player->InVehicle() ) { + return false; + } + + return true; +} + +/* +=============== +hhSafeDeathVolume::Event_Touch +=============== +*/ +void hhSafeDeathVolume::Event_Touch( idEntity *other, trace_t *trace ) { + if( !other ) { + return; + } + + if( !other->IsType(hhPlayer::Type) ) { + return; + } + + hhPlayer* player = static_cast( other ); + if( !IsValid(player) ) { + return; + } + + if( player->IsSpiritOrDeathwalking() ) { + player->StopSpiritWalk(); + } else { + player->Kill( false, false ); + } +} \ No newline at end of file diff --git a/src/Prey/game_safeDeathVolume.h b/src/Prey/game_safeDeathVolume.h new file mode 100644 index 0000000..b3311dc --- /dev/null +++ b/src/Prey/game_safeDeathVolume.h @@ -0,0 +1,43 @@ +#ifndef __HH_SAFE_DEATH_VOLUME_H +#define __HH_SAFE_DEATH_VOLUME_H + +//----------------------------------------------------------------------- +// +// hhSafeResurrectionVolume +// +//----------------------------------------------------------------------- +class hhSafeResurrectionVolume : public idEntity { + CLASS_PROTOTYPE( hhSafeResurrectionVolume ); + + public: + void Spawn(); + + void PickRandomPoint( idVec3& origin, idMat3& axis ); + + protected: + virtual int DetermineContents() const; + + void Event_Enable(); + void Event_Disable(); +}; + +//----------------------------------------------------------------------- +// +// hhSafeResurrectionVolume +// +//----------------------------------------------------------------------- +class hhSafeDeathVolume : public hhSafeResurrectionVolume { + CLASS_PROTOTYPE( hhSafeDeathVolume ); + + public: + void Spawn(); + + protected: + virtual bool IsValid( const hhPlayer* player ); + virtual int DetermineContents() const; + + protected: + void Event_Touch( idEntity *other, trace_t *trace ); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_securityeye.cpp b/src/Prey/game_securityeye.cpp new file mode 100644 index 0000000..7ec97cc --- /dev/null +++ b/src/Prey/game_securityeye.cpp @@ -0,0 +1,801 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +/********************************************************************** + +hhSecurityEyeBase + +**********************************************************************/ + +CLASS_DECLARATION( idEntity, hhSecurityEyeBase ) + EVENT( EV_Enable, hhSecurityEyeBase::Event_Enable ) + EVENT( EV_Disable, hhSecurityEyeBase::Event_Disable ) + EVENT( EV_Activate, hhSecurityEyeBase::Event_Activate ) +END_CLASS + +void hhSecurityEyeBase::Spawn() { + SpawnEye(); +} + +void hhSecurityEyeBase::Save(idSaveGame *savefile) const { + m_pEye.Save(savefile); +} + +void hhSecurityEyeBase::Restore( idRestoreGame *savefile ) { + m_pEye.Restore(savefile); +} + +void hhSecurityEyeBase::TransferArg(idDict &Args, const char *key) { + const idKeyValue *kv = NULL; + do { + kv = spawnArgs.MatchPrefix(key, kv); + if (kv) { + Args.Set(kv->GetKey().c_str(), kv->GetValue().c_str()); + } + } while (kv != NULL); +} + +void hhSecurityEyeBase::SpawnEye() { + + idVec3 origin = GetOrigin(); + idMat3 axis = GetAxis(); + idVec3 offset = spawnArgs.GetVector("offset_eye"); + + idDict Args; + Args.SetVector( "origin", origin + offset*axis ); + Args.SetMatrix( "rotation", axis ); + + TransferArg(Args, "minPitch"); + TransferArg(Args, "maxPitch"); + TransferArg(Args, "minYaw"); + TransferArg(Args, "maxYaw"); + TransferArg(Args, "startPitch"); + TransferArg(Args, "startYaw"); + TransferArg(Args, "pitchRate"); + TransferArg(Args, "yawRate"); + TransferArg(Args, "fov"); + TransferArg(Args, "pathScanRate"); + TransferArg(Args, "usePathScan"); + TransferArg(Args, "triggerOnce"); + TransferArg(Args, "lengthBeam"); + TransferArg(Args, "enabled"); + TransferArg(Args, "health"); + TransferArg(Args, "pathScanNode"); + TransferArg(Args, "call"); + TransferArg(Args, "callRef"); + TransferArg(Args, "callRefActivator"); + TransferArg(Args, "triggerBehavior"); + TransferArg(Args, "target"); + + m_pEye = gameLocal.SpawnObject( spawnArgs.GetString("def_eye"), &Args ); + HH_ASSERT( m_pEye.IsValid() && m_pEye.GetEntity() ); + + m_pEye->fl.noRemoveWhenUnbound = true; + m_pEye->Bind(this, true); + + if (m_pEye->IsType(hhSecurityEye::Type)) { + static_cast(m_pEye.GetEntity())->SetBase(this); + } + if (spawnArgs.GetBool("nobase")) { + Hide(); + } +} + +void hhSecurityEyeBase::Event_Activate(idEntity *pActivator) { + if (m_pEye.IsValid()) { + m_pEye->ProcessEvent(&EV_Activate, pActivator); // Pass activate messages to eye + } +} +void hhSecurityEyeBase::Event_Enable() { + if (m_pEye.IsValid()) { + m_pEye->ProcessEvent(&EV_Enable); // Pass enable messages to eye + } +} +void hhSecurityEyeBase::Event_Disable() { + if (m_pEye.IsValid()) { + m_pEye->ProcessEvent(&EV_Disable); // Pass disable messages to eye + } +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +/********************************************************************** + +hhSecurityEye + +**********************************************************************/ +const idEventDef EV_Notify("notify", "e"); + +CLASS_DECLARATION( idEntity, hhSecurityEye ) + EVENT( EV_Enable, hhSecurityEye::Event_Enable ) + EVENT( EV_Disable, hhSecurityEye::Event_Disable ) + EVENT( EV_Notify, hhSecurityEye::Event_Notify ) + EVENT( EV_Activate, hhSecurityEye::Event_Activate ) +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +/* +================ +hhSecurityEye::Spawn +================ +*/ +void hhSecurityEye::Spawn() { + m_pBase = NULL; + m_pTrigger = NULL; + m_bTriggerOnce = spawnArgs.GetBool("triggerOnce"); + m_fCachedScanFovCos = idMath::Cos( DEG2RAD(spawnArgs.GetFloat("fov")) ); + m_lengthBeam = spawnArgs.GetFloat("lengthBeam", "4096"); + m_offsetTrigger = spawnArgs.GetVector("offset_trigger"); + + SetupRotationParms(); + SpawnTrigger(); + m_bUsePathScan = InitPathList(); + + GetPhysics()->SetContents(CONTENTS_SHOOTABLE|CONTENTS_SHOOTABLEBYARROW); + fl.takedamage = false; // CJR: Set to be non-destructible at 3DR's request + + m_iPVSArea = gameLocal.pvs.GetPVSArea( GetOrigin() ); + + if( spawnArgs.GetBool("enabled", "1") ) { + StartScanning(); + } else { + EnterIdleState(); + } + + BecomeActive( TH_TICKER ); +} + +void hhSecurityEye::Save(idSaveGame *savefile) const { + savefile->WriteInt( state ); + savefile->WriteObject(m_pTrigger); + savefile->WriteBool(m_bTriggerOnce); + savefile->WriteFloat(m_fCachedScanFovCos); + savefile->WriteInt(m_iPVSArea); + savefile->WriteAngles(m_StartAngles); + savefile->WriteAngles(m_MaxLookAngles); + savefile->WriteAngles(m_MinLookAngles); + savefile->WriteBool(m_bPitchDirection); + savefile->WriteBool(m_bYawDirection); + savefile->WriteStringList(m_PathScanNodes); + savefile->WriteInt(m_iPathScanNodeIndex); + savefile->WriteBool(m_bUsePathScan); + savefile->WriteFloat(m_fPathScanRate); + savefile->WriteObject(m_pBase); + m_Target.Save(savefile); + + savefile->WriteFloat( currentYaw.GetStartTime() ); // idInterpolate + savefile->WriteFloat( currentYaw.GetDuration() ); + savefile->WriteFloat( currentYaw.GetStartValue() ); + savefile->WriteFloat( currentYaw.GetEndValue() ); + + savefile->WriteFloat( currentPitch.GetStartTime() ); // idInterpolate + savefile->WriteFloat( currentPitch.GetDuration() ); + savefile->WriteFloat( currentPitch.GetStartValue() ); + savefile->WriteFloat( currentPitch.GetEndValue() ); + + savefile->WriteAngles(m_RotationRate); + savefile->WriteFloat(m_lengthBeam); + savefile->WriteVec3(m_offsetTrigger); + + savefile->WriteAngles(m_LookAngles); +} + +void hhSecurityEye::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadInt( reinterpret_cast ( state ) ); + savefile->ReadObject(reinterpret_cast (m_pTrigger)); + savefile->ReadBool(m_bTriggerOnce); + savefile->ReadFloat(m_fCachedScanFovCos); + savefile->ReadInt(m_iPVSArea); + savefile->ReadAngles(m_StartAngles); + savefile->ReadAngles(m_MaxLookAngles); + savefile->ReadAngles(m_MinLookAngles); + savefile->ReadBool(m_bPitchDirection); + savefile->ReadBool(m_bYawDirection); + savefile->ReadStringList(m_PathScanNodes); + savefile->ReadInt(m_iPathScanNodeIndex); + savefile->ReadBool(m_bUsePathScan); + savefile->ReadFloat(m_fPathScanRate); + savefile->ReadObject(reinterpret_cast (m_pBase)); + m_Target.Restore(savefile); + + savefile->ReadFloat( set ); // idInterpolate + currentYaw.SetStartTime( set ); + savefile->ReadFloat( set ); + currentYaw.SetDuration( set ); + savefile->ReadFloat( set ); + currentYaw.SetStartValue( set ); + savefile->ReadFloat( set ); + currentYaw.SetEndValue( set ); + + savefile->ReadFloat( set ); // idInterpolate + currentPitch.SetStartTime( set ); + savefile->ReadFloat( set ); + currentPitch.SetDuration( set ); + savefile->ReadFloat( set ); + currentPitch.SetStartValue( set ); + savefile->ReadFloat( set ); + currentPitch.SetEndValue( set ); + + savefile->ReadAngles(m_RotationRate); + savefile->ReadFloat(m_lengthBeam); + savefile->ReadVec3(m_offsetTrigger); + + savefile->ReadAngles(m_LookAngles); +} + +/* +================ +hhSecurityEye::~hhSecurityEye +================ +*/ +hhSecurityEye::~hhSecurityEye() { + SAFE_REMOVE( m_pTrigger ); + + //Used in case you use the remove command from the console + StopSound( SND_CHANNEL_ANY ); +} + +/* +===================== +hhSecurityEye::DormantBegin +===================== +*/ +void hhSecurityEye::DormantBegin() { + idEntity::DormantBegin(); + + if( state == StatePathScanning || state == StateAreaScanning ) { + DisableTrigger(); + } +} + +/* +===================== +hhSecurityEye::DormantEnd +===================== +*/ +void hhSecurityEye::DormantEnd() { + idEntity::DormantEnd(); + + if( state == StatePathScanning || state == StateAreaScanning ) { + EnableTrigger(); + } +} + +/* +===================== +hhSecurityEye::SetupRotationParms +===================== +*/ +void hhSecurityEye::SetupRotationParms() { + m_bPitchDirection = 1; + m_bYawDirection = 1; + m_StartAngles.Set( spawnArgs.GetFloat("startPitch"), spawnArgs.GetFloat("startYaw"), 0.0f ); + m_LookAngles = m_StartAngles; + + m_MaxLookAngles.Set( spawnArgs.GetFloat("maxPitch"), spawnArgs.GetFloat("maxYaw"), 0.0f ); + m_MinLookAngles.Set( spawnArgs.GetFloat("minPitch"), spawnArgs.GetFloat("minYaw"), 0.0f ); + m_RotationRate.Set ( spawnArgs.GetFloat("pitchRate"), spawnArgs.GetFloat("yawRate"), 0.0f ); + + currentPitch.Init(gameLocal.time, 0, m_StartAngles.pitch, m_StartAngles.pitch); + currentYaw.Init(gameLocal.time, 0, m_StartAngles.yaw, m_StartAngles.yaw); +} + +// Turn to angles using standard rates, angles should be in -180..180 local space +void hhSecurityEye::TurnTo(idAngles &ang) { + float curYaw, curPitch, delta; + int duration; + + if (m_RotationRate.yaw) { + curYaw = currentYaw.GetCurrentValue(gameLocal.time); + delta = idMath::Fabs(curYaw - ang.yaw); + duration = SEC2MS(delta / m_RotationRate.yaw); + currentYaw.Init(gameLocal.time, duration, curYaw, ang.yaw); + } + + if (m_RotationRate.pitch) { + curPitch = currentPitch.GetCurrentValue(gameLocal.time); + delta = idMath::Fabs(curYaw - ang.yaw); + duration = SEC2MS(delta / m_RotationRate.pitch); + currentPitch.Init(gameLocal.time, duration, curPitch, ang.pitch); + } +} + +void hhSecurityEye::LookAtLinear(idVec3 &pos) { + float curYaw, curPitch, delta; + int durationYaw, durationPitch, duration; + idVec3 localDir; + idVec3 dir = pos - GetOrigin(); + dir.Normalize(); + GetBaseAxis().ProjectVector(dir, localDir); + + idAngles localAngles; + localAngles.roll = 0.0f; + localAngles.yaw = idMath::AngleNormalize180( localDir.ToYaw() ); + localAngles.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ); + + curYaw = currentYaw.GetCurrentValue(gameLocal.time); + delta = idMath::Fabs(curYaw - localAngles.yaw); + durationYaw = SEC2MS(delta / m_fPathScanRate); + + curPitch = currentPitch.GetCurrentValue(gameLocal.time); + delta = idMath::Fabs(curYaw - localAngles.yaw); + durationPitch = SEC2MS(delta / m_fPathScanRate); + + // Both converge together at the same time + duration = max(durationYaw, durationPitch); + currentYaw.Init(gameLocal.time, duration, curYaw, localAngles.yaw); + currentPitch.Init(gameLocal.time, duration, curPitch, localAngles.pitch); +} + +// Returns base rotation, all the local angles are relative to this +idMat3 hhSecurityEye::GetBaseAxis() { + assert(m_pBase); + return m_pBase->GetAxis(); +} + +// Set rotation of eye based on local angles +void hhSecurityEye::UpdateRotation() { + idAngles localAngles; + localAngles.Set(currentPitch.GetCurrentValue(gameLocal.time), currentYaw.GetCurrentValue(gameLocal.time), 0.0f); + SetAxis( localAngles.ToMat3() ); +} + +void hhSecurityEye::SetBase(idEntity *ent) { + m_pBase = ent; +} + + +/* +===================== +hhSecurityEye::GetRenderView +===================== +*/ +renderView_t* hhSecurityEye::GetRenderView() { + + renderView = idEntity::GetRenderView(); + + idVec3 ViewOffset = spawnArgs.GetVector("offset_view"); + idMat3 ViewAxis = GetAxis(); + idVec3 ViewOrigin = GetOrigin() + ViewOffset*ViewAxis; + + gameLocal.CalcFov( spawnArgs.GetFloat("fov", "90"), renderView->fov_x, renderView->fov_y ); //HUMANHEAD premerge bjk + renderView->viewaxis = ViewAxis; + renderView->vieworg = ViewOrigin; + + return renderView; +} + +/* +================ +hhSecurityEye::GetTriggerArgs +================ +*/ +idDict* hhSecurityEye::GetTriggerArgs( idDict* pArgs ) { + assert( pArgs ); + + const idKeyValue* pKeyValue = NULL; + + pArgs->Set( "call", spawnArgs.GetString("call") ); + pArgs->Set( "callRef", spawnArgs.GetString("callRef") ); + pArgs->Set( "callRefActivator", spawnArgs.GetString("callRefActivator") ); + + pArgs->Set( "triggerBehavior", spawnArgs.GetString("triggerBehavior") ); + for( int iIndex = spawnArgs.GetNumKeyVals() - 1; iIndex >= 0 ; --iIndex ) { + pKeyValue = spawnArgs.GetKeyVal( iIndex ); + if ( !pKeyValue->GetKey().Cmpn( "trigger_class", 13 ) ) { + pArgs->Set( pKeyValue->GetKey(), pKeyValue->GetValue() ); + } + else if ( !pKeyValue->GetKey().Cmpn( "target", 6 ) ) { + pArgs->Set( pKeyValue->GetKey(), pKeyValue->GetValue() ); + } + } + + //Tripwire args + pArgs->SetFloat( "lengthBeam", m_lengthBeam ); + pArgs->SetBool( "rigidBeamLength", 1 ); + pArgs->Set( "beam", spawnArgs.GetString("beam") ); + //Tripwire args end + + return pArgs; +} + +/* +================ +hhSecurityEye::SpawnTrigger +================ +*/ +void hhSecurityEye::SpawnTrigger() { + + idVec3 origin = GetPhysics()->GetOrigin(); + idMat3 axis = GetPhysics()->GetAxis(); + + idDict Args; + Args.SetVector( "origin", origin + m_offsetTrigger*axis ); + Args.SetMatrix( "rotation", axis ); + m_pTrigger = static_cast( gameLocal.SpawnObject(spawnArgs.GetString("def_trigger"), GetTriggerArgs( &Args )) ); + HH_ASSERT( m_pTrigger ); + + m_pTrigger->SetOwner( this ); + m_pTrigger->Bind( this, true ); +} + +/* +============ +hhSecurityEye::InitPathList +============ +*/ +bool hhSecurityEye::InitPathList() { + const idKeyValue* pKeyValue = NULL; + + m_PathScanNodes.Clear(); + for( int iIndex = spawnArgs.GetNumKeyVals() - 1; iIndex >= 0; --iIndex ) { + pKeyValue = spawnArgs.GetKeyVal( iIndex ); + if ( !pKeyValue->GetKey().Cmpn( "pathScanNode", 12 ) && pKeyValue->GetValue().Length() ) { + m_PathScanNodes.Append( pKeyValue->GetValue() ); + } + } + + m_iPathScanNodeIndex = 0; + m_fPathScanRate = spawnArgs.GetFloat("pathScanRate"); + + return ( m_PathScanNodes.Num() && spawnArgs.GetBool( "usePathScan" ) ); +} + +/* +================ +hhSecurityEye::Ticker +================ +*/ +void hhSecurityEye::Ticker() { + switch( state ) { + case StateIdle: + if (currentYaw.IsDone(gameLocal.time) && currentPitch.IsDone(gameLocal.time)) { + BecomeInactive( TH_TICKER ); + } + break; + case StateTracking: + break; + + case StateAreaScanning: + if (currentPitch.IsDone(gameLocal.time)) { + m_bPitchDirection ^= 1; + if( !m_bPitchDirection ) { + m_LookAngles.pitch = m_MinLookAngles.pitch; + } else { + m_LookAngles.pitch = m_MaxLookAngles.pitch; + } + TurnTo( m_LookAngles ); + } + + if (currentYaw.IsDone(gameLocal.time)) { + m_bYawDirection ^= 1; + if( !m_bYawDirection ) { + m_LookAngles.yaw = m_MinLookAngles.yaw; + } else { + m_LookAngles.yaw = m_MaxLookAngles.yaw; + } + TurnTo( m_LookAngles ); + } + break; + + case StatePathScanning: + idEntity* pEntity = NULL; + + if (currentYaw.IsDone(gameLocal.time) && currentPitch.IsDone(gameLocal.time)) { + pEntity = VerifyPathScanNode( m_iPathScanNodeIndex ); + if( pEntity ) { + idVec3 origin = pEntity->GetPhysics()->GetOrigin(); + LookAtLinear( origin ); + + m_iPathScanNodeIndex = (m_iPathScanNodeIndex + 1) % m_PathScanNodes.Num(); + } + } + break; + } + + UpdateRotation(); +} + +/* +============ +hhSecurityEye::DetermineEntityPosition +============ +*/ +idVec3 hhSecurityEye::DetermineEntityPosition( idEntity* pEntity ) { + if( pEntity && pEntity->IsType( idActor::Type ) ) { + return static_cast(pEntity)->GetEyePosition(); + } + + return pEntity->GetPhysics()->GetOrigin(); +} + +/* +================ +hhSecurityEye::CanSeeTarget +================ +*/ +bool hhSecurityEye::CanSeeTarget( idEntity* pEntity ) { + float fDist; + trace_t TraceInfo; + idVec3 Dir; + pvsHandle_t PVSHandle; + idVec3 EyePos; + idMat3 EyeAxis; + idVec3 TraceEnd; + + PVSHandle = gameLocal.pvs.SetupCurrentPVS( m_iPVSArea ); + + if ( !pEntity || ( pEntity->fl.notarget ) ) { + goto CANT_SEE; + } + + // if there is no way we can see this player + if( !gameLocal.pvs.InCurrentPVS( PVSHandle, pEntity->GetPVSAreas(), pEntity->GetNumPVSAreas() ) ) { + goto CANT_SEE; + } + + TraceEnd = (pEntity->IsType(idActor::Type)) ? static_cast(pEntity)->GetEyePosition() : pEntity->GetPhysics()->GetOrigin(); + Dir = TraceEnd - GetPhysics()->GetOrigin(); + fDist = Dir.Normalize(); + + if( fDist > m_lengthBeam ) { + goto CANT_SEE; + } + + EyeAxis = GetAxis(); + EyePos = GetOrigin() + m_offsetTrigger * EyeAxis; + + if( Dir * EyeAxis[0] < m_fCachedScanFovCos ) { + goto CANT_SEE; + } + + gameLocal.clip.TracePoint( TraceInfo, EyePos, TraceEnd, MASK_VISIBILITY, this ); + if( TraceInfo.fraction == 1.0f || TraceInfo.c.entityNum == pEntity->entityNumber ) { + gameLocal.pvs.FreeCurrentPVS( PVSHandle ); + return true; + } + +CANT_SEE: + gameLocal.pvs.FreeCurrentPVS( PVSHandle ); + + return false; +} + +/* +===================== +hhSecurityEye::EnableTrigger +===================== +*/ +void hhSecurityEye::EnableTrigger() { + if( m_pTrigger ) { + m_pTrigger->ProcessEvent( &EV_Enable ); + } +} + +/* +===================== +hhSecurityEye::DisableTrigger +===================== +*/ +void hhSecurityEye::DisableTrigger() { + if( m_pTrigger ) { + m_pTrigger->ProcessEvent( &EV_Disable ); + } +} + +/* +============ +hhSecurityEye::Killed +============ +*/ +void hhSecurityEye::Killed( idEntity *pInflictor, idEntity *pAttacker, int iDamage, const idVec3 &Dir, int iLocation ) { + if( state != StateDead ) { + //Only trigger targets when active + if( state != StateIdle ) { + ActivateTargets( pAttacker ); + } + + EnterDeadState(); + } +} + +/* +============ +hhSecurityEye::StartScanning +============ +*/ +void hhSecurityEye::StartScanning() { + if( !m_bUsePathScan ) { + EnterAreaScanningState(); + } else { + EnterPathScanningState(); + } +} + +/* +============ +hhSecurityEye::VerifyLookTarget +============ +*/ +idEntity* hhSecurityEye::VerifyPathScanNode( int& iIndex ) { + return gameLocal.FindEntity( m_PathScanNodes[iIndex].c_str() ); +} + +/* +============ +hhSecurityEye::Event_Enable +============ +*/ +void hhSecurityEye::Event_Enable() { + switch( state ) { + case StateIdle: + StartScanning(); + break; + } +} + +/* +============ +hhSecurityEye::Event_Disable +============ +*/ +void hhSecurityEye::Event_Disable() { + switch( state ) { + case StateTracking: + case StatePathScanning: + case StateAreaScanning: + EnterIdleState(); + break; + } +} + +/* +============ +hhSecurityEye::Event_Notify +============ +*/ +void hhSecurityEye::Event_Notify( idEntity* pActivator ) { + //Not sure of the behavior wanted if m_bTriggerOnce is true + if( pActivator && pActivator->IsType(idActor::Type) ) { + const idActor* pActor = static_cast( pActivator ); + } + + switch( state ) { + case StateAreaScanning: + case StatePathScanning: + HH_ASSERT( pActivator ); + m_Target = pActivator; + EnterTrackingState(); + break; + case StateTracking: + break; + } +} + +/* +============ +hhSecurityEye::Event_Activate +============ +*/ +void hhSecurityEye::Event_Activate( idEntity* pActivator ) { + switch( state ) { + case StateIdle: + StartScanning(); + break; + case StatePathScanning: + case StateAreaScanning: + case StateTracking: + m_Target = NULL; + EnterIdleState(); + break; + } +} + +/* +================ +hhSecurityEye::EnterDeadState +================ +*/ +void hhSecurityEye::EnterDeadState() { + state = StateDead; + + hhFxInfo fxInfo; + + DisableTrigger(); + + StopSound( SND_CHANNEL_ANY ); + + fxInfo.SetNormal( GetAxis()[0] ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_detonate", m_pBase->GetOrigin(), GetAxis(), &fxInfo ); + + idVec3 Position = spawnArgs.GetVector("offset_debris"); + Position = GetPhysics()->GetOrigin() + Position * GetPhysics()->GetAxis(); + idVec3 Forward = GetAxis()[0]; + idVec3 Velocity = Forward*10; + hhUtils::SpawnDebrisMass(spawnArgs.GetString("def_debrisspawner"), Position, &Forward, &Velocity, 1); + + // Cause the base to flicker when the ball is destroyed + if( m_pBase ) { + m_pBase->SetShaderParm( SHADERPARM_MODE, 1 ); // Activate the flicker effect + m_pBase->SetShaderParm( SHADERPARM_TIMEOFFSET, -MS2SEC( gameLocal.time )); + } + + Hide(); + GetPhysics()->DisableClip(); + int length = 0; + StartSound("snd_explode", SND_CHANNEL_ANY, 0, false, &length); + PostEventMS( &EV_Remove, length ); + + BecomeActive( TH_TICKER ); +} + +/* +================ +hhSecurityEye::EnterIdleState +================ +*/ +void hhSecurityEye::EnterIdleState() { + state = StateIdle; + + DisableTrigger(); + + TurnTo( m_StartAngles ); + + StopSound( SND_CHANNEL_ANY ); +} + +/* +================ +hhSecurityEye::EnterAreaScanningState +================ +*/ +void hhSecurityEye::EnterAreaScanningState() { + state = StateAreaScanning; + + m_LookAngles = m_StartAngles; + TurnTo( m_StartAngles ); + + EnableTrigger(); + + StartSound("snd_move", SND_CHANNEL_ANY); + + BecomeActive( TH_TICKER ); +} + +/* +================ +hhSecurityEye::EnterPathScanningState +================ +*/ +void hhSecurityEye::EnterPathScanningState() { + state = StatePathScanning; + + HH_ASSERT( m_PathScanNodes.Num() ); + + EnableTrigger(); + + StartSound("snd_move", SND_CHANNEL_ANY); + + BecomeActive( TH_TICKER ); +} + +/* +================ +hhSecurityEye::EnterTrackingState +================ +*/ +void hhSecurityEye::EnterTrackingState() { + state = StateTracking; + + DisableTrigger(); + StopSound( SND_CHANNEL_ANY ); + + BecomeActive( TH_TICKER ); +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/game_securityeye.h b/src/Prey/game_securityeye.h new file mode 100644 index 0000000..2983d1b --- /dev/null +++ b/src/Prey/game_securityeye.h @@ -0,0 +1,134 @@ +#ifndef __HH_SECURITYEYE_H +#define __HH_SECURITYEYE_H + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +extern const idEventDef EV_Notify; + +/********************************************************************** + +hhSecurityEyeBase + +**********************************************************************/ +class hhSecurityEyeBase : public idEntity { + CLASS_PROTOTYPE( hhSecurityEyeBase ); + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void SpawnEye(); + void TransferArg(idDict &Args, const char *key); + +protected: + void Event_Activate( idEntity* pActivator ); + void Event_Enable(); + void Event_Disable(); + +protected: + idEntityPtr m_pEye; +}; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + +/********************************************************************** + +hhSecurityEye + +**********************************************************************/ +class hhSecurityEye : public idEntity { + CLASS_PROTOTYPE( hhSecurityEye ); +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_Enable() {}; + void Event_Disable() {}; + void Event_Notify( idEntity* pActivator ) {}; + void Event_Activate( idEntity* pActivator ) {}; +#else + public: + hhSecurityEye() : m_pTrigger( NULL ) { } + virtual ~hhSecurityEye(); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void SetBase(idEntity *ent); + + virtual renderView_t* GetRenderView(); + + protected: + void Ticker(); + idDict* GetTriggerArgs( idDict* pArgs ); + void SpawnTrigger(); + void SpawnBase(); + bool InitPathList(); + + virtual void DormantBegin(); + virtual void DormantEnd(); + + void EnableTrigger(); + void DisableTrigger(); + + idVec3 DetermineEntityPosition( idEntity* pEntity ); + bool CanSeeTarget( idEntity* pEntity ); + virtual void Killed( idEntity *pInflictor, idEntity *pAttacker, int iDamage, const idVec3 &Dir, int iLocation ); + + void StartScanning(); + idEntity* VerifyPathScanNode( int& iIndex ); + + protected: + void Event_Enable(); + void Event_Disable(); + void Event_Notify( idEntity* pActivator ); + void Event_Activate( idEntity* pActivator ); + + protected: + enum States { + StateIdle = 0, + StateAreaScanning, + StatePathScanning, + StateTracking, // Tracking state is now just a stopover state that means it was triggered. + // It is needed so that the next time it's triggered, it will become inactive + StateDead + } state; + + void EnterDeadState(); + void EnterIdleState(); + void EnterAreaScanningState(); + void EnterPathScanningState(); + void EnterTrackingState(); + + void UpdateRotation(); + void LookAtLinear(idVec3 &pos); + void TurnTo(idAngles &ang); + void SetupRotationParms(); + idMat3 GetBaseAxis(); + + protected: + hhTriggerTripwire* m_pTrigger; + + bool m_bTriggerOnce; + float m_fCachedScanFovCos; + int m_iPVSArea; + + idAngles m_StartAngles; + idAngles m_MaxLookAngles; + idAngles m_MinLookAngles; + idAngles m_LookAngles; + bool m_bPitchDirection; + bool m_bYawDirection; + + idList m_PathScanNodes; + int m_iPathScanNodeIndex; + bool m_bUsePathScan; + float m_fPathScanRate; + + idEntity *m_pBase; + idEntityPtr m_Target; + + idInterpolate currentYaw; // These are local angles, relative to base rotation + idInterpolate currentPitch; // These are local angles, relative to base rotation + idAngles m_RotationRate; + float m_lengthBeam; + idVec3 m_offsetTrigger; +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_shuttle.cpp b/src/Prey/game_shuttle.cpp new file mode 100644 index 0000000..50dc5e9 --- /dev/null +++ b/src/Prey/game_shuttle.cpp @@ -0,0 +1,1057 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/* + method: + walk up to ship console, activate gui + gui sends command to vehicle to take control + vehicle animates out of console around you + control hand is added + + model specs: + min: -64 -50 -64 + max: 64 50 64 + + needs to be centered about the origin (in all axes) + needs to fit a pilot collision box inside in his "piloting" animation + needs to be collapsable into the bottom console +*/ + + +//========================================================================== +// +// hhTractorBeam +// +//========================================================================== + +CLASS_DECLARATION(idClass, hhTractorBeam) +END_CLASS + +hhTractorBeam::hhTractorBeam() { + bAllow = true; + owner = NULL; + bindController = NULL; + traceController = NULL; + traceTarget = NULL; + beam = NULL; + targetLocalOffset = vec3_origin; + targetID = 0; + bActive = false; + beamClientPredictTime = 0; +} + +void hhTractorBeam::Save(idSaveGame *savefile) const { + owner.Save(savefile); + beam.Save(savefile); + traceTarget.Save(savefile); + bindController.Save(savefile); + traceController.Save(savefile); + + savefile->WriteStaticObject(feedbackForce); + savefile->WriteInt(tractorCost); + savefile->WriteBool(bAllow); + savefile->WriteBool(bActive); + savefile->WriteVec3(offsetTraceStart); + savefile->WriteVec3(offsetTraceEnd); + savefile->WriteVec3(offsetEquilibrium); + savefile->WriteVec3(targetLocalOffset); + savefile->WriteInt(targetID); +} + +void hhTractorBeam::Restore( idRestoreGame *savefile ) { + owner.Restore(savefile); + beam.Restore(savefile); + traceTarget.Restore(savefile); + bindController.Restore(savefile); + traceController.Restore(savefile); + + savefile->ReadStaticObject(feedbackForce); + savefile->ReadInt(tractorCost); + savefile->ReadBool(bAllow); + savefile->ReadBool(bActive); + savefile->ReadVec3(offsetTraceStart); + savefile->ReadVec3(offsetTraceEnd); + savefile->ReadVec3(offsetEquilibrium); + savefile->ReadVec3(targetLocalOffset); + savefile->ReadInt(targetID); +} + +void hhTractorBeam::WriteToSnapshot( idBitMsgDelta &msg ) const { + //don't need to sync the beam and other components that are purely client-side + + msg.WriteBits(owner.GetSpawnId(), 32); + msg.WriteBits(traceTarget.GetSpawnId(), 32); + msg.WriteBits(targetID, 32); + + msg.WriteBits(bActive, 1); + msg.WriteBits(bAllow, 1); +} + +void hhTractorBeam::ReadFromSnapshot( const idBitMsgDelta &msg ) { + owner.SetSpawnId(msg.ReadBits(32)); + traceTarget.SetSpawnId(msg.ReadBits(32)); + targetID = msg.ReadBits(32); + + bool beamActive = !!msg.ReadBits(1); + if (beamActive != bActive && beamClientPredictTime <= gameLocal.time) { + if (beamActive) { + Activate(); + } + else { + Deactivate(); + } + } + bAllow = !!msg.ReadBits(1); +} + +void hhTractorBeam::SpawnComponents(hhShuttle *shuttle) { + SetOwner(shuttle); + + idVec3 ownerOrigin = owner->GetOrigin(); + idMat3 ownerAxis = owner->GetAxis(); + idDict *ownerArgs = &owner->spawnArgs; + + tractorCost = ownerArgs->GetInt("tractorcost"); + offsetTraceStart = ownerArgs->GetVector("offset_tractortracestart"); + offsetTraceEnd = ownerArgs->GetVector("offset_tractortraceend"); + offsetEquilibrium = ownerArgs->GetVector("offset_tractorequilibrium"); + + float tension = ownerArgs->GetFloat("tension", "1.0"); + float slack; + if (gameLocal.isMultiplayer) { + slack = ownerArgs->GetFloat("slack_mp", "1.0"); + } + else { + slack = ownerArgs->GetFloat("slack", "1.0"); + } + float yawLimit = ownerArgs->GetFloat("yawlimit", "180"); + const char *handName = ownerArgs->GetString("def_tractorhand"); + const char *animName = ownerArgs->GetString("boundanim"); + + if (gameLocal.isClient) { + bindController = static_cast( gameLocal.SpawnClientObject(ownerArgs->GetString("def_bindController"), NULL) ); + } + else { + bindController = static_cast( gameLocal.SpawnObject(ownerArgs->GetString("def_bindController")) ); + } + HH_ASSERT( bindController.IsValid() ); + bindController->fl.networkSync = false; + + bindController->SetTension(tension); + bindController->SetSlack(slack); + bindController->SetRiderParameters(animName, handName, yawLimit, 0.0f); + bindController->SetOrigin(ownerOrigin + offsetEquilibrium * ownerAxis); + bindController->Bind(owner.GetEntity(), true); +// bindController->fl.robustDormant = true; // This beam could be going through walls, so set it to a robust dormancy check + bindController->fl.neverDormant = true; + bindController->SetShuttle(true); + + if (gameLocal.isClient) { + traceController = gameLocal.SpawnClientObject( ownerArgs->GetString("def_traceController"), NULL ); + } + else { + traceController = gameLocal.SpawnObject( ownerArgs->GetString("def_traceController") ); + } + HH_ASSERT( traceController.IsValid() ); + traceController->fl.networkSync = false; + + traceController->SetOrigin(ownerOrigin + offsetTraceEnd * ownerAxis); + traceController->Bind(owner.GetEntity(), true); +// traceController->fl.robustDormant = true; // This beam could be going through walls, so set it to a robust dormancy check + traceController->fl.neverDormant = true; + + idVec3 offsetBeamPosition = ownerArgs->GetVector("offset_tractorbeamposition"); + idVec3 pos = ownerOrigin + offsetBeamPosition * ownerAxis; + beam = hhBeamSystem::SpawnBeam(pos, ownerArgs->GetString("beam_tractor"), mat3_identity, true); + HH_ASSERT( beam.IsValid() ); + beam->SetOrigin(pos); + beam->SetAxis(ownerAxis); + beam->Bind(owner.GetEntity(), false); + beam->Activate( false ); + beam->fl.robustDormant = true; // This beam could be going through walls, so set it to a robust dormancy check + + float feedbacktension = ownerArgs->GetFloat("feedbacktension"); + feedbackForce.SetRestoreFactor(feedbacktension); + float feedbackslack = ownerArgs->GetFloat("feedbackslack"); + feedbackForce.SetRestoreSlack(feedbackslack); +} + +bool hhTractorBeam::Exists() { + return (owner.IsValid() && bindController.IsValid() && traceController.IsValid() && beam.IsValid()); +} + +bool hhTractorBeam::IsAllowed() { + return (Exists() && bAllow && /*!owner->IsDocked() &&*/ owner->HasPower(tractorCost) && owner->noTractorTime <= gameLocal.time); +} + +bool hhTractorBeam::IsActive() { + return (Exists() && bActive); +} + +bool hhTractorBeam::IsLocked() { + return (IsActive() && GetTarget() != NULL); +} + +idEntity *hhTractorBeam::GetTarget() { + if (!bindController.IsValid() || !bindController.GetEntity()) { //rww - in the case of no tractor beam (testing concept for mp), this controller may not exist, so check it. + return NULL; + } + return bindController->GetRider(); +} + +void hhTractorBeam::RequestState(bool wantsActive) { + if (wantsActive && !IsActive() && IsAllowed()) { + Activate(); + } + else if (!wantsActive && IsActive()) { + Deactivate(); + } +} + +void hhTractorBeam::Activate() { + bActive = true; + beamClientPredictTime = gameLocal.time + USERCMD_MSEC*4; + owner->StartSound("snd_tractor", SND_CHANNEL_TRACTORBEAM); + owner->StartSound("snd_tractor_start", SND_CHANNEL_ANY); +} + +void hhTractorBeam::Deactivate() { + if ( !gameLocal.isMultiplayer && GetTarget() && GetTarget()->IsType( hhMonsterAI::Type ) ) { + idVec3 velocity = GetTarget()->GetPhysics()->GetLinearVelocity(); + hhMonsterAI *targetAI = static_cast( GetTarget() ); + if ( targetAI && owner.IsValid() ) { + targetAI->SetEnemy( owner.GetEntity()->GetPilot() ); + } + if ( velocity.Length() > GetTarget()->spawnArgs.GetFloat( "tractor_kill_speed", "800" ) ) { + if ( targetAI ) { + targetAI->Damage( NULL, NULL, idVec3( 0,0,0 ), "damage_monsterfall", 1, INVALID_JOINT ); + if ( targetAI->GetAFPhysics() ) { + targetAI->GetAFPhysics()->SetLinearVelocity( velocity * GetTarget()->spawnArgs.GetFloat( "tractor_speed_scale", "15" ) ); + } + } + } + } + bActive = false; + beamClientPredictTime = gameLocal.time + USERCMD_MSEC*4; + if (beam.IsValid()) { + beam->Activate( false ); + beam->SetTargetEntity(NULL); + } + if (owner.IsValid()) { + owner->StopSound(SND_CHANNEL_TRACTORBEAM); + owner->StartSound("snd_tractor_stop", SND_CHANNEL_ANY); + } + if (GetTarget()) { + GetTarget()->fl.isTractored = false; //rww + } + if (bindController.IsValid()) { + bindController->Detach(); + } + feedbackForce.SetEntity(NULL); +} + +bool hhTractorBeam::ValidTarget(idEntity *ent) { + bool b = ent && + !(ent->IsType(idActor::Type) && static_cast(ent)->InVehicle()) && + ent->GetPhysics() && + ent->GetPhysics()->GetContents() != 0 && + !ent->IsHidden() && + ent->fl.canBeTractored; + + if (gameLocal.isMultiplayer && b) { //rww - if mp and a valid target thus far, perform mp-only check(s) + if (ent->IsType(hhPlayer::Type)) { + hhPlayer *pl = static_cast(ent); + if (pl->IsDead()) { //if he died, let go. + b = false; + } + else if (pl->IsWallWalking()) { //don't allow grabbing wallwalking players in mp + b = false; + } + } + else if (ent->IsType(hhMPDeathProxy::Type)) { //don't grab temporary client-based corpses + b = false; + } + } + + return b; +} + +void hhTractorBeam::ReleaseTarget() { + // Release the target, but stay active + beam->SetTargetEntity(NULL); + if (GetTarget()) { + GetTarget()->fl.isTractored = false; //rww + } + bindController->Detach(); + feedbackForce.SetEntity(NULL); +} + +void hhTractorBeam::LaunchTarget(float speed) { + idEntity *target = bindController->GetRider(); + ReleaseTarget(); + if (target) { + idVec3 vel = owner->GetAxis()[0] * speed; + target->GetPhysics()->SetLinearVelocity(vel); + } +} + +idEntity *hhTractorBeam::TraceForTarget(trace_t &results) { + idVec3 start = owner->GetOrigin() + offsetTraceStart * owner->GetAxis(); + idVec3 end; + if (GetTarget() && results.c.entityNum != ENTITYNUM_NONE) { + end = GetTarget()->GetOrigin(); + } + else { + end = traceController->GetOrigin(); + } + gameLocal.clip.TracePoint( results, start, end, MASK_TRACTORBEAM, owner.GetEntity() ); + + idEntity *validEntity = NULL; + if (results.fraction < 1.0f) { + idEntity *entity = gameLocal.GetTraceEntity(results); + if (ValidTarget(entity)) { + if (entity && (entity == GetTarget() || !entity->fl.isTractored)) { //rww - if i'm not grabbing it already, don't grab it if it's tractored already. + validEntity = entity; + } + } + } + + if (beam.IsValid() && !GetTarget()) { + beam->SetTargetLocation(results.endpos); //rww - in any case, set the beam's target pos to the trace endpos + } + + return validEntity; +} + +void hhTractorBeam::AttachTarget(idEntity *target, trace_t &results) { + if (target->IsType(idAFEntity_Base::Type) && // Active ragdoll + static_cast(target)->IsActiveAF()) { + idAFEntity_Base *af = static_cast(target); + //int joint = CLIPMODEL_ID_TO_JOINT_HANDLE( results.c.id ); + targetID = af->BodyForClipModelId(results.c.id); + targetLocalOffset = vec3_origin; // apply to center of mass + } + else if (target->IsType(idActor::Type)) { // Non-ragdolled actor + targetID = 0; + targetLocalOffset = target->GetPhysics()->GetBounds().GetCenter();// - target->GetOrigin(); + + //rww - for mp kill credits + if (gameLocal.isMultiplayer) { + if (target->IsType(hhPlayer::Type) && owner->GetPilot() && owner->GetPilot()->IsType(hhPlayer::Type)) { + hhPlayer *plTarget = static_cast(target); + plTarget->airAttacker = owner->GetPilot(); + plTarget->airAttackerTime = gameLocal.time + 300; + } + else if (target->IsType(hhSpiritProxy::Type)) { //in mp, snap player back to their body when their prox is tractored + target->Damage(owner.GetEntity(), owner.GetEntity(), vec3_origin, "damage_generic", 0.0f, -1); //call damage, but don't actually do damage. + } + } + } + else { // Everything else + targetID = results.c.id; + targetLocalOffset = ( results.c.point - target->GetOrigin(targetID) ) * target->GetAxis(targetID).Transpose(); + } + bindController->Attach(target, true, targetID, targetLocalOffset); + beam->SetTargetEntity(target, targetID, targetLocalOffset); + beam->SetArcVector(owner->GetAxis()[0]); + feedbackForce.SetEntity(owner.GetEntity()); + + target->fl.isTractored = true; //rww +} + +void hhTractorBeam::Update() { + trace_t results; + + if (Exists()) { + if (IsActive()) { + if (!IsAllowed() || !owner->ConsumePower(tractorCost)) { + // Ran out of power or no longer allowed + Deactivate(); + } + + if (IsLocked()) { + if (!ValidTarget(GetTarget())) { + // Locked target is no longer valid + ReleaseTarget(); + } + else if (gameLocal.isMultiplayer && GetTarget() && GetTarget()->IsType(hhPlayer::Type)) { + //rww - just hint the player in the direction of the shuttle that has them picked up for mp (help with disorientation) + float frametime = ((float)(gameLocal.time - gameLocal.previousTime))/USERCMD_MSEC; + hhPlayer *targetPl = static_cast(GetTarget()); + + + idQuat plOri = targetPl->GetViewAngles().ToQuat(); + idVec3 targetPos = owner->GetOrigin(); + targetPos.z -= 80.0f; + + idVec3 t = (targetPos-targetPl->GetOrigin()); + t.Normalize(); + idQuat idealOri = t.ToMat3().ToQuat(); + + idQuat finalOri; + finalOri.Slerp(plOri, idealOri, 0.02f*frametime); + + targetPl->SetOrientation(targetPl->GetOrigin(), targetPl->GetPhysics()->GetAxis(), finalOri.ToMat3()[0], finalOri.ToAngles()); + } + } + } + + // Do the trace as long as we're not already locked, so the GUI can use it to show the cursor + if (!IsAllowed() || IsLocked()) { + //traceTarget = NULL; + //rww - changed to continue tracing even after grabbing something + + results.c.entityNum = 0; //try tracing to the target + traceTarget = TraceForTarget(results); + if (traceTarget != GetTarget()) { + results.c.entityNum = ENTITYNUM_NONE; //try tracing forward along beam + traceTarget = TraceForTarget(results); + if (traceTarget != GetTarget()) { + ReleaseTarget(); + if (beam.IsValid()) { + beam->SetTargetLocation(results.endpos); + } + } + } + } + else { + traceTarget = TraceForTarget(results); + + if (IsActive()) { + if (traceTarget != NULL) { + AttachTarget(traceTarget.GetEntity(), results); + } + //rww - instead of doing this and giving a misleading trace result, setting to the actual trace.endpos + /* + else { + // No target found, but leave beam out there + beam->SetTargetEntity(traceController.GetEntity()); + } + */ + } + } + + if (IsActive()) { + // Update beam based on shuttle orientation + if (!beam->IsActivated()) { + beam->Activate( true ); + } + beam->SetArcVector( owner->GetAxis()[0] ); + } + + // Update the feedback force + if (IsLocked()) { + idVec3 p = GetTarget()->GetOrigin(targetID) + targetLocalOffset * GetTarget()->GetAxis(targetID); + idVec3 a = owner->GetOrigin() - p; + a.Normalize(); + a *= offsetEquilibrium.Length(); + idVec3 idealPosition = p + a; + feedbackForce.SetTarget(idealPosition); + feedbackForce.Evaluate(gameLocal.time); + } + } +} + + + +//========================================================================== +// +// hhShuttle +// +//========================================================================== +const idEventDef EV_Vehicle_TractorBeam( "tractorBeam" ); +const idEventDef EV_Vehicle_TractorBeamDone( "tractorBeamDone" ); + +CLASS_DECLARATION(hhVehicle, hhShuttle) + EVENT( EV_PostSpawn, hhShuttle::Event_PostSpawn ) + EVENT( EV_Vehicle_FireCannon, hhShuttle::Event_FireCannon ) + EVENT( EV_Vehicle_TractorBeam, hhShuttle::Event_ActivateTractorBeam ) + EVENT( EV_Vehicle_TractorBeamDone, hhShuttle::Event_DeactivateTractorBeam ) +END_CLASS + + +hhShuttle::~hhShuttle() { + if (renderEntity.gui[0]) { + gameLocal.FocusGUICleanup(renderEntity.gui[0]); //HUMANHEAD rww + uiManager->DeAlloc(renderEntity.gui[0]); + renderEntity.gui[0] = NULL; + } +} + +void hhShuttle::Spawn() { + bSputtering = false; + malfunctionThrustFactor = spawnArgs.GetFloat( "malfunctionThrustFactor" ); + + idealTipState = TIP_STATE_NONE; + curTipState = TIP_STATE_NONE; + nextTipChange = gameLocal.time; + + // Fade console in + SetShaderParm(4, -MS2SEC(gameLocal.time)); // time + SetShaderParm(5, -1.0f); // Dir + + teleportDest = GetOrigin() + idVec3(-64, 0, 0) * GetPhysics()->GetAxis(); + teleportAngles = idAngles( 0.0f, GetPhysics()->GetAxis().ToAngles().yaw, 0.0f ); + + terminalVelocitySquared = spawnArgs.GetFloat( "terminalvelocity" ); + terminalVelocitySquared *= terminalVelocitySquared; + + noTractorTime = 0; //rww + + fl.clientEvents = true; //rww - allow events to be posted on client for this entity + + PostEventMS( &EV_PostSpawn, 0 ); + + fl.networkSync = true; +} + +void hhShuttle::Event_PostSpawn() { + // Spawn all sub-entities after initial population for multiplayer's sake + + // Spawn components for tractor beam + const idKeyValue *kv1 = spawnArgs.FindKey("attackFunc"); + const idKeyValue *kv2 = spawnArgs.FindKey("altAttackFunc"); + if ((kv1 && !kv1->GetValue().Icmp("tractorBeam")) || (kv2 && !kv2->GetValue().Icmp("tractorBeam")) ) { + tractor.SpawnComponents( this ); + } + + // Spawn Model based thrusters + const char *thrusterDef = spawnArgs.GetString("def_thruster"); + if (thrusterDef && *thrusterDef) { + // Spawn thrusters with appropriate direction vectors + idVec3 offset; + char suffix[THRUSTER_DIRECTIONS] = { 'F', 'B', 'L', 'R', 'U', 'D' }; + int directions[THRUSTER_DIRECTIONS] = { MASK_POSX, MASK_NEGX, MASK_POSY, MASK_NEGY, MASK_POSZ, MASK_NEGZ }; + for (int i=0; iWriteStaticObject( tractor ); + for (int i=0; iWriteFloat( terminalVelocitySquared ); + savefile->WriteVec3( teleportDest ); + savefile->WriteAngles( teleportAngles ); + savefile->WriteFloat( malfunctionThrustFactor ); + savefile->WriteBool( bSputtering ); + savefile->WriteInt(noTractorTime); + savefile->WriteInt( idealTipState ); + savefile->WriteInt( curTipState ); + savefile->WriteInt( nextTipChange ); +} + +void hhShuttle::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject( tractor ); + for (int i=0; iReadFloat( terminalVelocitySquared ); + savefile->ReadVec3( teleportDest ); + savefile->ReadAngles( teleportAngles ); + savefile->ReadFloat( malfunctionThrustFactor ); + savefile->ReadBool( bSputtering ); + savefile->ReadInt(noTractorTime); + savefile->ReadInt( idealTipState ); + savefile->ReadInt( curTipState ); + savefile->ReadInt( nextTipChange ); +} + +void hhShuttle::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhVehicle::WriteToSnapshot(msg); + + /* + int i; + for (i = 0; i < THRUSTER_DIRECTIONS; i++) { + msg.WriteBits(thrusters[i].GetSpawnId(), 32); + } + */ + + msg.WriteFloat(renderEntity.shaderParms[4]); + msg.WriteFloat(renderEntity.shaderParms[5]); + + msg.WriteBits(bSputtering, 1); + msg.WriteBits(noTractorTime, 32); + + tractor.WriteToSnapshot(msg); +} + +void hhShuttle::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhVehicle::ReadFromSnapshot(msg); + + /* + int i; + for (i = 0; i < THRUSTER_DIRECTIONS; i++) { + int spawnId = msg.ReadBits(32); + if (!spawnId) { + thrusters[i] = NULL; + } + else { + thrusters[i].SetSpawnId(spawnId); + } + } + */ + + SetShaderParm(4, msg.ReadFloat()); + SetShaderParm(5, msg.ReadFloat()); + + bSputtering = !!msg.ReadBits(1); + noTractorTime = msg.ReadBits(32); + + tractor.ReadFromSnapshot(msg); +} + +void hhShuttle::ClientPredictionThink( void ) { + if (fl.hidden) { + return; + } + + Think(); + + UpdateVisuals(); + Present(); +} + + +// These for model based thrusters ------------------------------------------------ + +hhVehicleThruster *hhShuttle::SpawnThruster(idVec3 &offset, idVec3 &dir, const char *thrusterName, bool master) { + idVec3 pos = GetOrigin() + offset * GetAxis(); + idVec3 direction = dir * GetAxis(); + idMat3 axis = direction.ToMat3(); + + idDict args; + args.SetVector("origin", pos); + args.SetBool("soundmaster", master); + hhVehicleThruster *thruster; + if (gameLocal.isClient) { + thruster = static_cast(gameLocal.SpawnClientObject(thrusterName, &args)); + } + else { + thruster = static_cast(gameLocal.SpawnObject(thrusterName, &args)); + } + if (thruster) { + thruster->fl.networkSync = false; + + thruster->SetOrigin(pos); + thruster->SetAxis(axis); + thruster->Bind(this, true); + thruster->SetOwner(this); + thruster->Hide(); + thruster->SetSmoker(master, offset, dir); + } + return thruster; +} + +void hhShuttle::FireThrusters( const idVec3& impulse ) { + idVec3 curVel = physicsObj.GetLinearVelocity(); + idVec3 excess( vec3_zero ); + idVec3 Iu( vec3_zero ); + idVec3 Vu( vec3_zero ); + float impulseLength = 0.0f; + idVec3 finalImpulse = impulse; + + // Subtract portion of impulse out that is in direction of current terminal velocity + //FIXME: not sure if this should be in the physics code + if( !IsNoClipping() && curVel.LengthSqr() > terminalVelocitySquared ) { + Iu = finalImpulse; + impulseLength = Iu.Normalize(); + Vu = curVel; + Vu.Normalize(); + excess = (impulseLength * (Iu * Vu)) * Vu; + finalImpulse -= excess; + } + + // Apply wacky controls when dying + if (health <= 0) { + finalImpulse += hhUtils::RandomVector() * 127.0f * malfunctionThrustFactor; + } + + if (finalImpulse.Length() > VECTOR_EPSILON) { + ConsumePower(thrusterCost); + } + + // Play sputter sound if trying to move without enough power + if (!HasPower(thrusterCost)) { + if (finalImpulse != vec3_origin && !bSputtering) { + StartSound("snd_sputter", SND_CHANNEL_ANY); + bSputtering = true; + } + else { + bSputtering = false; + } + + finalImpulse.Zero(); + } + + ApplyImpulse( finalImpulse ); +} + +void hhShuttle::ApplyBoost( float magnitude ) { + ApplyImpulse( GetAxis()[0] * magnitude ); +} + +// Update all tick based effects +void hhShuttle::UpdateEffects( const idVec3& localThrust ) { + // Update thrusters on/off state based on direction of velocity vector + int directionMask = localThrust.DirectionMask(); + if ( GetPilot() && GetPilot()->IsType( idAI::Type ) ) { + idVec3 vel = GetPhysics()->GetLinearVelocity(); + if ( -GetAxis()[2] * vel > 0 ) { + thrusters[THRUSTER_TOP]->SetThruster( true ); + thrusters[THRUSTER_TOP]->Update( localThrust ); + } else { + thrusters[THRUSTER_TOP]->SetThruster( false ); + thrusters[THRUSTER_TOP]->Update( localThrust ); + } + if ( GetAxis()[2] * vel > 0 ) { + thrusters[THRUSTER_BOTTOM]->SetThruster( true ); + thrusters[THRUSTER_BOTTOM]->Update( localThrust ); + } else { + thrusters[THRUSTER_BOTTOM]->SetThruster( false ); + thrusters[THRUSTER_BOTTOM]->Update( localThrust ); + } + if ( GetAxis()[1] * vel > 0 ) { + thrusters[THRUSTER_RIGHT]->SetThruster( true ); + thrusters[THRUSTER_RIGHT]->Update( localThrust ); + } else { + thrusters[THRUSTER_RIGHT]->SetThruster( false ); + thrusters[THRUSTER_RIGHT]->Update( localThrust ); + } + if ( -GetAxis()[1] * vel > 0 ) { + thrusters[THRUSTER_LEFT]->SetThruster( true ); + thrusters[THRUSTER_LEFT]->Update( localThrust ); + } else { + thrusters[THRUSTER_LEFT]->SetThruster( false ); + thrusters[THRUSTER_LEFT]->Update( localThrust ); + } + } else { + for (int thrusterIndex=0; thrusterIndexSetThruster( INDEX_IN_MASK(directionMask, thrusterIndex) ); + thrusters[thrusterIndex]->Update( localThrust ); + } + } + } +} + +// Static function to determine if a pilot is suitable +bool hhShuttle::ValidPilot( idActor *act ) { + if (act && act->health > 0) { + if (act->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast(act); + if( !player->IsSpiritOrDeathwalking() && !player->IsPossessed() ) { + return true; + } + } + else if (act->IsType(idAI::Type)) { + idAI *ai = static_cast(act); + if (ai->spawnArgs.GetBool("canPilotShuttle")) { + return true; + } + } + } + return false; +} + +bool hhShuttle::WillAcceptPilot( idActor *act ) { + return IsConsole() && hhShuttle::ValidPilot( act ) && CanBecomeVehicle(act); +} + +void hhShuttle::AcceptPilot( hhPilotVehicleInterface* pilotInterface ) { + hhVehicle::AcceptPilot( pilotInterface ); + + // Fade the console out + SetShaderParm(4, -MS2SEC(gameLocal.time)); // time + SetShaderParm(5, 1.0f); // dir + + // Alert the hud that control has been taken (currently unused) + if (renderEntity.gui[0]) { + renderEntity.gui[0]->Trigger(gameLocal.time); + } +} + +void hhShuttle::EjectPilot() { + + // Fade ship out + SetShaderParm(4, -MS2SEC(gameLocal.time)); // time + SetShaderParm(5, -1.0f); // dir + + tractor.RequestState(false); + + for (int ix=0; ixSetDying(false); + thrusters[ix]->SetThruster(false); + thrusters[ix]->Update(vec3_origin); + } + } + + hhVehicle::EjectPilot(); +} + +void hhShuttle::ProcessPilotInput( const usercmd_t* cmds, const idAngles* viewAngles ) { + hhVehicle::ProcessPilotInput( cmds, viewAngles ); + + if( cmds && IsVehicle() ) { + UpdateEffects( idVec3(cmds->forwardmove, -cmds->rightmove, cmds->upmove) ); + } +} + +/* +bool hhShuttle::UsesCrosshair() const { + return (!IsDocked() || dock->AllowsFiring()) && hhVehicle::UsesCrosshair(); +} +*/ + +void hhShuttle::Killed(idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location) { + for(int ix=0; ixSetDying(true); + } + } + + hhVehicle::Killed(inflictor, attacker, damage, dir, location); +} + +void hhShuttle::DrawHUD( idUserInterface* _hud ) { + if( !_hud ) { + return; + } + + float spawnPower = spawnArgs.GetInt( "maxPower" ); + bool bDocked = dock.IsValid(); + bool bDockedInTransporter = bDocked && dock->IsType(hhShuttleTransport::Type); + bool bDockLocked = bDocked && dock->IsLocked(); + bool bNeedsRecharge = (health < spawnHealth) || (currentPower < spawnPower); + _hud->SetStateBool( "docked", bDocked ); + _hud->SetStateBool( "dockedtransporter", bDockedInTransporter ); + _hud->SetStateBool( "docklocked", bDockLocked ); + _hud->SetStateBool( "recharging", bNeedsRecharge && bDocked && dock->Recharges() ); + _hud->SetStateBool( "crosshair", fireController && fireController->UsesCrosshair() ); + _hud->SetStateBool( "lowhealth", health/(float)spawnHealth < 0.20f ); + _hud->SetStateBool( "lowpower", currentPower/spawnPower < 0.20f ); + + float velfrac = physicsObj.GetLinearVelocity().LengthSqr() / terminalVelocitySquared; +// float velfrac = controller.GetThrustBooster(); + velfrac = idMath::ClampFloat( 0.0f, 1.0f, velfrac ); + _hud->SetStateFloat( "velocityfraction", velfrac ); + + bool bHasTractor = tractor.Exists(); + bool bTractorActive = tractor.IsActive(); + bool bTractorLocked = tractor.IsLocked(); + bool bTractorSighted = tractor.IsAllowed() && (tractor.GetTraceTarget() != NULL); + bool bTractorAllowed = tractor.IsAllowed(); + _hud->SetStateBool( "hastractor", bHasTractor ); + _hud->SetStateBool( "tractoractive", bTractorActive ); + _hud->SetStateBool( "tractorsighted", bTractorSighted ); + _hud->SetStateBool( "tractorlocked", bTractorLocked ); + _hud->SetStateBool( "tractorallowed", bTractorAllowed ); + + // HUMANHEAD PCF pdm 05-18-06: Removed non-octal mass from shuttle hud +/* if( bHasTractor && bTractorLocked ) { + float mass = tractor.GetTarget()->GetPhysics()->GetMass(); + _hud->SetStateInt("tractormass", (int) mass); + const char *massFormatter = common->GetLanguageDict()->GetString("#str_41161"); + _hud->SetStateString("tractormasstext", va(massFormatter, (int)mass)); + }*/ + // HUMANHEAD END + + if (GetPilot() && GetPilot()->IsType(hhPlayer::Type)) { + + if (!g_tips.GetBool()) { + idealTipState = TIP_STATE_NONE; + nextTipChange = gameLocal.time; + } + + // Transition to desired tip state, always passing through TIP_STATE_NONE + if ( gameLocal.time >= nextTipChange && idealTipState != curTipState ) { + + if (curTipState != TIP_STATE_NONE) { // Turn off old tip + _hud->HandleNamedEvent( "shuttleTipWindowDown" ); + nextTipChange = gameLocal.time + 300; + curTipState = TIP_STATE_NONE; + } + else { // Turn on new tip + const char *tip = NULL; + switch(idealTipState) { + case TIP_STATE_NONE: + break; + case TIP_STATE_DOCKED: + gameLocal.SetTip(_hud, NULL, "", NULL, NULL, "tip1"); + tip = spawnArgs.GetString("text_exittip"); + gameLocal.SetTip(_hud, "_attackalt", tip, NULL, NULL, "tip2"); + _hud->HandleNamedEvent( "shuttleTipWindowUp" ); + curTipState = idealTipState; + break; + case TIP_STATE_UNDOCKED: + tip = spawnArgs.GetString("text_cannontip"); + gameLocal.SetTip(_hud, "_attack", tip, NULL, NULL, "tip1"); + tip = spawnArgs.GetString("text_tractortip"); + gameLocal.SetTip(_hud, "_attackalt", tip, NULL, NULL, "tip2"); + _hud->HandleNamedEvent( "shuttleTipWindowUp" ); + curTipState = idealTipState; + + // Go away in a little while + idealTipState = TIP_STATE_NONE; + nextTipChange = gameLocal.time + 3000; + break; + } + } + + } + } + + _hud->StateChanged(gameLocal.time); + hhVehicle::DrawHUD( _hud ); +} + +void hhShuttle::SetDock( const hhDock* dock ) { + hhVehicle::SetDock(dock); + + bool bDocked = (dock != NULL); + bool bDockedInTransporter = bDocked && dock->IsType(hhShuttleTransport::Type); + + if (bDocked && !bDockedInTransporter) { + // Just entered dock, display tip + idealTipState = TIP_STATE_DOCKED; + nextTipChange = gameLocal.time; + + // Just entered dock, set this one as my respawn point + idVec3 location = dock->GetOrigin() + dock->spawnArgs.GetVector("offset_console") * dock->GetAxis(); + teleportDest = location + idVec3(-64, 0, 0) * GetPhysics()->GetAxis(); + teleportAngles = idAngles( 0.0f, dock->GetAxis().ToAngles().yaw, 0.0f ); + } +} + +void hhShuttle::Undock() { + hhVehicle::Undock(); + + // Just left dock, display tip + idealTipState = TIP_STATE_UNDOCKED; + nextTipChange = gameLocal.time; +} + +void hhShuttle::Ticker() { + // Handle noclipping + physicsObj.SetContents(IsNoClipping() ? 0 : vehicleContents ); + physicsObj.SetClipMask(IsNoClipping() ? 0 : vehicleClipMask ); + + if( IsVehicle() && IsDocked() ) { + dock->UpdateAxis( GetAxis() ); + } + + tractor.Update(); +} + +void hhShuttle::RemoveVehicle() { + if (IsDocked()) { + dock->ShuttleExit(this); + dock = NULL; + } + GetPhysics()->SetGravity( vec3_origin ); // so shuttle doesn't bounce on ground and make noise + hhVehicle::RemoveVehicle(); +} + +void hhShuttle::SetConsoleModel() { + hhVehicle::SetConsoleModel(); + + // Fade console in + SetShaderParm(4, -MS2SEC(gameLocal.time)); // time + SetShaderParm(5, -1.0f); // Dir + // Need to add gui back(SetModel strips it) + if (!renderEntity.gui[0]) { //rww - don't double up guis + renderEntity.gui[0] = uiManager->FindGui(spawnArgs.GetString("gui"), true, true); + } +} + +void hhShuttle::SetVehicleModel() { + hhVehicle::SetVehicleModel(); + + // Fade shuttle in + SetShaderParm(4, -MS2SEC(gameLocal.time)); // time + SetShaderParm(5, 1.0f); // Dir + validThrustTime = gameLocal.time + spawnArgs.GetInt("delay_thrust"); +} + +void hhShuttle::RecoverFromDying() { + StopSound(SND_CHANNEL_DYING); + CancelEvents(&EV_VehicleExplode); + if (domelight.IsValid()) { + domelight->SetLightParm(7, 0.0f); + } + for(int ix=0; ixSetDying(false); + } + } +} + +void hhShuttle::PerformDeathAction(int deathAction, idActor *savedPilot, idEntity *attacker, idVec3 &dir) { + switch(deathAction) { + case 2: // Teleport pilot + if (!gameLocal.isMultiplayer) { //rww - fall through on purpose + if (savedPilot->IsType(hhPlayer::Type)) { + idVec3 origin; + idMat3 axis; + idVec3 viewDir; + idAngles angles; + idMat3 eyeAxis; + static_cast(savedPilot)->GetResurrectionPoint( origin, axis, viewDir, angles, eyeAxis, savedPilot->GetPhysics()->GetAbsBounds(), teleportDest, teleportAngles.ToMat3(), teleportAngles.ToForward(), teleportAngles ); + static_cast(savedPilot)->DeathWalk( origin, axis, viewDir.ToMat3(), angles, eyeAxis ); + } + else { + savedPilot->Teleport(teleportDest, teleportAngles, NULL); + } + break; + } + default: + hhVehicle::PerformDeathAction(deathAction, savedPilot, attacker, dir); + break; + } +} + +void hhShuttle::InitiateRecharging() { +} + +void hhShuttle::FinishRecharging() { + if (GetPilot() != NULL) { + fl.takedamage = true; + } +} + +void hhShuttle::Event_FireCannon() { + if (IsDocked() && GetPilot() && GetPilot()->IsType(idAI::Type) && !dock->AllowsFiring()) { + // Let monsters boost out of docks a bit + ApplyBoost( 127.0f * 0.25f ); + return; + } + + if( IsDocked() && spawnArgs.GetBool("dockBoost") && !dock->AllowsFiring() ) { + ApplyBoost( 127.0f * dockBoostFactor ); + return; + } + if( spawnArgs.GetBool("tractorlaunch") && GetTractorTarget() ) { + LaunchTractorTarget(spawnArgs.GetFloat("launch_speed")); + bDisallowAttackUntilRelease = true; + bDisallowAltAttackUntilRelease = true; + return; + } + if ( !IsDocked() || dock->AllowsFiring()) { + hhVehicle::Event_FireCannon(); + } +} + +void hhShuttle::Event_ActivateTractorBeam() { + tractor.RequestState( true ); +} + +void hhShuttle::Event_DeactivateTractorBeam() { + tractor.RequestState( false ); +} diff --git a/src/Prey/game_shuttle.h b/src/Prey/game_shuttle.h new file mode 100644 index 0000000..382f68f --- /dev/null +++ b/src/Prey/game_shuttle.h @@ -0,0 +1,152 @@ +#ifndef __GAME_SHUTTLE_H__ +#define __GAME_SHUTTLE_H__ + +class hhBindController; +class hhShuttle; + +#define TIP_STATE_NONE 0 +#define TIP_STATE_DOCKED 1 +#define TIP_STATE_UNDOCKED 2 + +//========================================================================== +// +// hhTractorBeam +// +//========================================================================== + +class hhTractorBeam : public idClass { + CLASS_PROTOTYPE( hhTractorBeam ); + + friend hhShuttle; + +public: + hhTractorBeam(); + void SpawnComponents( hhShuttle *shuttle ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + bool Exists(); + bool IsAllowed(); + bool IsActive(); + bool IsLocked(); + + idEntity * GetTarget(); + void SetAllow( bool allow ) { bAllow = allow; } + void Update(); + idEntity * GetTraceTarget() { return traceTarget.GetEntity(); } + void RequestState( bool wantsActive ); + idEntity * TraceForTarget( trace_t &results ); + void LaunchTarget(float speed); + +protected: + void SetOwner( hhShuttle *shuttle ) { owner = shuttle; } + void Activate(); + void Deactivate(); + void AttachTarget( idEntity *ent, trace_t &results ); + bool ValidTarget( idEntity *ent ); + void ReleaseTarget(); + +protected: + idEntityPtr owner; + idEntityPtr beam; + idEntityPtr traceTarget; // Results of target trace this frame + idEntityPtr bindController; // Bind controller for tractor beam + idEntityPtr traceController; // Point for traces + hhForce_Converge feedbackForce; // feedback force applied to shuttle + int tractorCost; // Power cost of tractor at 10 Hz + bool bAllow; // false if tractor beam is not allowed + bool bActive; // whether beam is active + int beamClientPredictTime; //rww - don't flicker the beam due to snapshot timings + idVec3 offsetTraceStart; // Offset to start of trace + idVec3 offsetTraceEnd; // Offset to end of trace + idVec3 offsetEquilibrium; // Offset to equilibrium point for targets + idVec3 targetLocalOffset; // Offset to attach point in entity local coordinates + int targetID; // Body ID of target entity +}; + +//========================================================================== +// +// hhShuttle +// +//========================================================================== + +class hhShuttle : public hhVehicle { + CLASS_PROTOTYPE( hhShuttle ); + + friend hhTractorBeam; + +public: + virtual ~hhShuttle(); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + idEntity * GetTractorTarget() { return tractor.GetTarget(); } + void ReleaseTractorTarget() { tractor.ReleaseTarget(); } + void AllowTractor( bool allow ) { tractor.SetAllow(allow); } + virtual bool TractorIsActive(void) { return tractor.IsActive(); } //rww + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void PerformDeathAction( int deathAction, idActor *savedPilot, idEntity *attacker, idVec3 &dir ); + void LaunchTractorTarget(float mag) { tractor.LaunchTarget(mag); } + + // Static pilot assessor, for queries when you don't have a vehicle instantiated + static bool ValidPilot( idActor *act ); + + virtual bool WillAcceptPilot( idActor *act ); + virtual void ProcessPilotInput( const usercmd_t* cmds, const idAngles* viewAngles ); + virtual void AcceptPilot( hhPilotVehicleInterface* pilotInterface ); + virtual void EjectPilot(); + + virtual void FireThrusters( const idVec3& impulse ); + void ApplyBoost( float magnitude ); + + virtual void DrawHUD( idUserInterface* _hud ); + + virtual void RemoveVehicle(); + + virtual void InitiateRecharging(); + virtual void FinishRecharging(); + virtual void SetDock( const hhDock* dock ); + virtual void Undock(); + void RecoverFromDying(); + + int noTractorTime; //HUMANHEAD rww +protected: + virtual void Ticker(); + hhVehicleThruster * SpawnThruster( idVec3 &offset, idVec3 &dir, const char *thrusterName, bool master ); + void UpdateEffects( const idVec3& impulse ); + + virtual void SetConsoleModel(); + virtual void SetVehicleModel(); + +protected: + void Event_PostSpawn(); + virtual void Event_FireCannon(); + void Event_ActivateTractorBeam(); + void Event_DeactivateTractorBeam(); + +protected: + hhTractorBeam tractor; + idEntityPtr thrusters[ THRUSTER_DIRECTIONS ]; + float terminalVelocitySquared; // Maximum speed of vehicle + idVec3 teleportDest; // teleport destination upon death + idAngles teleportAngles; // teleport destination upon death + float malfunctionThrustFactor; + bool bSputtering; + int idealTipState; // desired tip state + int curTipState; // current tip state + int nextTipChange; // next time a tip change can happen +}; + +#endif diff --git a/src/Prey/game_shuttledock.cpp b/src/Prey/game_shuttledock.cpp new file mode 100644 index 0000000..b18898f --- /dev/null +++ b/src/Prey/game_shuttledock.cpp @@ -0,0 +1,454 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//----------------------------------------------------------------------- +// +// hhShuttleDock +// +//----------------------------------------------------------------------- +const idEventDef EV_TrySpawnConsole("", NULL); + +CLASS_DECLARATION(hhDock, hhShuttleDock) + EVENT( EV_TrySpawnConsole, hhShuttleDock::Event_SpawnConsole) + EVENT( EV_Remove, hhShuttleDock::Event_Remove) + EVENT( EV_PostSpawn, hhShuttleDock::Event_PostSpawn ) +END_CLASS + +hhShuttleDock::hhShuttleDock() { + dockingBeam = NULL; + dockedShuttle = NULL; + shuttleCount = 0; + lastConsoleAttempt = 0; + bLocked = false; + bPlayingRechargeSound = false; +} + +void hhShuttleDock::Spawn() { + + amountHealth = spawnArgs.GetInt("amounthealth"); + amountPower = spawnArgs.GetInt("amountpower"); + + bCanExitLocked = spawnArgs.GetBool("canExitLocked"); + bLockOnEntry = spawnArgs.GetBool("lockOnEntry"); + offsetNozzle = spawnArgs.GetVector("offset_nozzle"); + offsetConsole = spawnArgs.GetVector("offset_console"); + offsetShuttlePoint = spawnArgs.GetVector("offset_shuttlepoint"); + maxDistance = spawnArgs.GetFloat("maxdistance"); + + dockingForce.SetRestoreFactor(spawnArgs.GetFloat("dockingforce")); + dockingForce.SetTarget(GetOrigin() + offsetShuttlePoint * GetAxis()); + + if (maxDistance) { + float shuttleMass = dockedShuttle->GetPhysics()->GetMass(); + float weaponKick = dockedShuttle->GetWeaponRecoil(); + float fireRate = dockedShuttle->GetWeaponFireDelay(); + float shuttleThrust = dockedShuttle->GetThrustFactor() + (weaponKick / 128.0f * fireRate); + float restorationFactor = 128.0f * shuttleMass * shuttleThrust / maxDistance; + restorationFactor *= 60.0f; // hack factor to allow for cannon recoil and inaccuracy + dockingForce.SetRestoreFactor(restorationFactor); + } + + GetPhysics()->SetContents(CONTENTS_SOLID); + + // Start with dock inactive (faded in) + SetShaderParm(4, -MS2SEC(gameLocal.time-10000)); + SetShaderParm(5, -1); + + fl.clientEvents = true; + + CancelEvents(&EV_PostSpawn); // hhDock may have already posted one + PostEventMS( &EV_PostSpawn, 0 ); + + fl.networkSync = true; //rww +} + +void hhShuttleDock::Event_PostSpawn() { + dockingBeam = SpawnDockingBeam(offsetNozzle); + if (!gameLocal.isClient) { + hhDock::Event_PostSpawn(); + } +} + +void hhShuttleDock::Save(idSaveGame *savefile) const { + savefile->WriteVec3( offsetNozzle ); + savefile->WriteVec3( offsetConsole ); + savefile->WriteVec3( offsetShuttlePoint ); + dockedShuttle.Save( savefile ); + dockingBeam.Save( savefile ); + savefile->WriteStaticObject( dockingForce ); + savefile->WriteInt( shuttleCount ); + savefile->WriteInt( lastConsoleAttempt ); + savefile->WriteInt( amountHealth ); + savefile->WriteInt( amountPower ); + savefile->WriteFloat( maxDistance ); + savefile->WriteBool( bLocked ); + savefile->WriteBool( bLockOnEntry ); + savefile->WriteBool( bCanExitLocked ); + savefile->WriteBool( bPlayingRechargeSound ); +} + +void hhShuttleDock::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3( offsetNozzle ); + savefile->ReadVec3( offsetConsole ); + savefile->ReadVec3( offsetShuttlePoint ); + dockedShuttle.Restore( savefile ); + dockingBeam.Restore( savefile ); + savefile->ReadStaticObject( dockingForce ); + savefile->ReadInt( shuttleCount ); + savefile->ReadInt( lastConsoleAttempt ); + savefile->ReadInt( amountHealth ); + savefile->ReadInt( amountPower ); + savefile->ReadFloat( maxDistance ); + savefile->ReadBool( bLocked ); + savefile->ReadBool( bLockOnEntry ); + savefile->ReadBool( bCanExitLocked ); + savefile->ReadBool( bPlayingRechargeSound ); +} + +void hhShuttleDock::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(dockingZone.GetSpawnId(), 32); + msg.WriteBits(dockedShuttle.GetSpawnId(), 32); + //msg.WriteBits(dockingBeam.GetSpawnId(), 32); + bool dockingBeamActive = false; + if (dockingBeam.IsValid()) { + dockingBeamActive = dockingBeam->IsActivated(); + } + msg.WriteBits(dockingBeamActive, 1); +} + +void hhShuttleDock::ReadFromSnapshot( const idBitMsgDelta &msg ) { + int spawnId; + + spawnId = msg.ReadBits(32); + if (!spawnId) { + dockingZone = NULL; + } + else { + dockingZone.SetSpawnId(spawnId); + } + + idEntityPtr newShuttle; + + spawnId = msg.ReadBits(32); + if (!spawnId) { + newShuttle = NULL; + } + else { + newShuttle.SetSpawnId(spawnId); + } + + if (newShuttle != dockedShuttle) { + if (dockedShuttle.IsValid()) { + DetachShuttle(dockedShuttle.GetEntity()); + dockedShuttle = NULL; + } + if (newShuttle.IsValid() && !newShuttle->IsHidden()) { + AttachShuttle(newShuttle.GetEntity()); + } + } + + bool dockingBeamActive = !!msg.ReadBits(1); + if (dockingBeam.IsValid()) { + if (dockingBeamActive != dockingBeam->IsActivated()) { + dockingBeam->Activate(dockingBeamActive); + } + } +} + +void hhShuttleDock::ClientPredictionThink( void ) { + //hhDock::ClientPredictionThink(); + if (dockedShuttle.IsValid() && dockedShuttle.GetEntity()) { //hax + BecomeActive(TH_THINK); + } + else { + BecomeInactive(TH_THINK); + } + Think(); +} + +void hhShuttleDock::SpawnConsole() { + idVec3 location = GetOrigin() + offsetConsole * GetAxis(); + + StartSound("snd_spawn", SND_CHANNEL_ANY); + + if ( !gameLocal.isClient ) { + idDict args; + args.SetVector("origin", location); + args.SetMatrix("rotation", GetAxis()); + args.Set("startDock", name.c_str()); + if (gameLocal.isMultiplayer) { //rww + const char *shuttleDef = spawnArgs.GetString("def_shuttle_mp"); + if (shuttleDef && shuttleDef[0]) { + gameLocal.SpawnObject(shuttleDef, &args); + return; + } + } + gameLocal.SpawnObject(spawnArgs.GetString("def_shuttle"), &args); + } +} + +hhBeamSystem *hhShuttleDock::SpawnDockingBeam(idVec3 &offset) { + idVec3 pos = GetPhysics()->GetOrigin() + offset * GetPhysics()->GetAxis(); + hhBeamSystem *beam = hhBeamSystem::SpawnBeam(pos, spawnArgs.GetString("beam_docking"), mat3_identity, true); + assert(beam); + beam->fl.networkSync = false; + beam->SetOrigin(pos); + beam->SetAxis( GetPhysics()->GetAxis()[2].ToMat3() ); + beam->Bind(this, false); + beam->Activate( false ); + return beam; +} + +bool hhShuttleDock::ValidEntity(idEntity *ent) { + if (ent->IsType(hhPlayer::Type)) { + int junk=0; + } + if (gameLocal.isMultiplayer) { //rww + return ( !idStr::Icmp(ent->GetEntityDefName(), spawnArgs.GetString("def_shuttle_mp")) && !ent->IsHidden() ); + } + return ( !idStr::Icmp(ent->GetEntityDefName(), spawnArgs.GetString("def_shuttle")) && !ent->IsHidden() ); +} + +void hhShuttleDock::EntityEntered(idEntity *ent) { + if (ent->IsType(hhShuttle::Type) && !dockedShuttle.IsValid()) { + shuttleCount++; + hhShuttle *shuttle = static_cast(ent); + if (shuttle->IsVehicle()) { + AttachShuttle(shuttle); + } + } +} + +void hhShuttleDock::EntityEncroaching(idEntity *ent) { + // If we contain a pilot, but no shuttle: spawn one + if (ent->IsType(idActor::Type) && !static_cast(ent)->InVehicle() && shuttleCount <= 0) { + if (gameLocal.time > lastConsoleAttempt) { + PostEventMS(&EV_TrySpawnConsole, 0); + lastConsoleAttempt = gameLocal.time + 100; + } + } + + else if (ent->IsType(hhShuttle::Type)) { + hhShuttle *shuttle = static_cast(ent); + + // Check for any shuttles that just became valid + if (!dockedShuttle.IsValid() && shuttle->IsVehicle()) { + AttachShuttle(shuttle); + } + + // Recharge docked shuttles + if (dockedShuttle == shuttle) { + if( shuttle->IsConsole() ) { + DetachShuttle( shuttle ); + } else { + shuttle->RecoverFromDying(); // Recover every time, just to be sure + if (shuttle->IsDamaged() || shuttle->NeedsPower()) { + + //HUMANHEAD bjk PCF (4-27-06) - shuttle recharge was slow + if(USERCMD_HZ == 30) { + shuttle->GiveHealth(2*amountHealth); + shuttle->GivePower(2*amountPower); + } else { + shuttle->GiveHealth(amountHealth); + shuttle->GivePower(amountPower); + } + + if (!bPlayingRechargeSound) { + StartSound("snd_recharge", SND_CHANNEL_RECHARGE); + bPlayingRechargeSound = true; + } + } + else { + if (bPlayingRechargeSound) { + StopSound(SND_CHANNEL_RECHARGE); + bPlayingRechargeSound = false; + } + } + } + } + } +} + +void hhShuttleDock::EntityLeaving(idEntity *ent) { + if (ent->IsType(hhShuttle::Type)) { + shuttleCount--; + shuttleCount = idMath::ClampInt(0, shuttleCount, shuttleCount); + if (dockedShuttle == ent && !IsLocked()) { + DetachShuttle(dockedShuttle.GetEntity()); + } + } +} + +void hhShuttleDock::AttachShuttle(hhShuttle *shuttle) { + if (!dockedShuttle.IsValid()) { + assert(!shuttle->IsHidden()); + + dockedShuttle = shuttle; + dockingForce.SetEntity(dockedShuttle.GetEntity()); + dockingBeam->SetTargetEntity(dockedShuttle.GetEntity(), 0, dockedShuttle->spawnArgs.GetVector("offset_dockingpoint")); + dockingBeam->Activate( true ); + dockedShuttle->SetDock( this ); + + if (amountHealth || amountPower) { + if ( shuttle->GetPilot() && !shuttle->GetPilot()->IsType( idAI::Type ) ) { + dockedShuttle->InitiateRecharging(); + } + } + + StartSound("snd_looper", SND_CHANNEL_DOCKED); + + if (bLockOnEntry) { + Lock(); + } + + if (spawnArgs.GetBool("nonsolidwhenactive")) { + GetPhysics()->SetContents(0); + } + + ActivateTargets( shuttle ); // Fire triggers + BecomeActive(TH_THINK); + + // Dock is active, make dock fade out + SetShaderParm(4, -MS2SEC(gameLocal.time)); + SetShaderParm(5, 1); + } +} + +void hhShuttleDock::DetachShuttle(hhShuttle *shuttle) { + assert(dockingBeam.IsValid()); + assert(shuttle); + + dockingForce.SetEntity(NULL); + dockingBeam->SetTargetEntity(NULL); + dockingBeam->Activate( false ); + shuttle->Undock(); + + shuttle->FinishRecharging(); + dockedShuttle = NULL; + StopSound(SND_CHANNEL_DOCKED); + StopSound(SND_CHANNEL_RECHARGE); + bPlayingRechargeSound = false; + Unlock(); // Shouldn't get in here when locked except when a shuttle dies within + BecomeInactive(TH_THINK); // Keep thinking so the restore position is updated + + if (spawnArgs.GetBool("nonsolidwhenactive")) { + GetPhysics()->SetContents(CONTENTS_SOLID); + } + + // Dock is inactive, make dock fade in + SetShaderParm(4, -MS2SEC(gameLocal.time)); + SetShaderParm(5, -1); +} + +void hhShuttleDock::Think() { + hhDock::Think(); + if (thinkFlags & TH_THINK) { + // Apply docking force to shuttle if docked, even if not in zone + assert(dockedShuttle == dockingForce.GetEntity()); + idVec3 target = GetOrigin() + offsetShuttlePoint * GetAxis(); + dockingForce.SetTarget(target); + dockingForce.Evaluate(gameLocal.time); + + if (maxDistance) { + // Drop the player if forced outside of our threshold + float threshold = maxDistance * 1.5f; + if ( (dockingForce.GetEntity()->GetOrigin() - target).LengthSqr() >= threshold*threshold) { + // exit + dockedShuttle->EjectPilot(); + } + } + } +} + +void hhShuttleDock::ShuttleExit(hhShuttle *shuttle) { + DetachShuttle(shuttle); + + // NOTE: Since zones don't get exit messages for entities that are destroyed, this is needed to + // decrease the shuttle count manually. + // However, since the shuttle physics shrinks when released, it's possible to get "nearly" outside + // the zone before releasing, in which case the exit DOES get called, screwing up the count. + // So... we bound the count to zero. + shuttleCount--; + shuttleCount = idMath::ClampInt(0, shuttleCount, shuttleCount); +} + +bool hhShuttleDock::AllowsExit() { + return !bLocked || CanExitLocked(); +} + +void hhShuttleDock::Lock() { + bLocked = true; + dockingForce.SetRestoreFactor(spawnArgs.GetFloat("dockingforce_locked")); +} + +void hhShuttleDock::Unlock() { + bLocked = false; + dockingForce.SetRestoreFactor(spawnArgs.GetFloat("dockingforce")); +} + +void hhShuttleDock::Event_SpawnConsole() { + if ( !spawnArgs.GetBool( "consolespawn" ) ) { + return; + } + + // Check to make sure there's room + idEntity *touch[ MAX_GENTITIES ]; + idBounds bounds, localBounds; + + idVec3 location = GetOrigin() + offsetConsole * GetAxis(); + localBounds[0] = spawnArgs.GetVector("consoleMins"); + localBounds[1] = spawnArgs.GetVector("consoleMaxs"); + + if ( GetAxis().IsRotated() ) { + bounds.FromTransformedBounds( localBounds, location, GetAxis() ); + } + else { + bounds[0] = localBounds[0] + location; + bounds[1] = localBounds[1] + location; + } + + int num = gameLocal.clip.EntitiesTouchingBounds( bounds, CLIPMASK_VEHICLE, touch, MAX_GENTITIES ); + bool blocked = false; + for (int i=0; iGetPhysics()->GetContents() & CLIPMASK_VEHICLE)) { + blocked = true; + break; + } + } + + if (blocked) { + // Now do more expensive check to see if there's really something blocking us. + // All the EntitiesTouchingBounds() check does is check the extents. + // Make bounds into clip model, and use to test contents at our spawn location + idTraceModel trm; + trm.SetupBox( localBounds ); + idClipModel *clipModel = new idClipModel( trm ); + int contents = gameLocal.clip.Contents(location, clipModel, GetAxis(), CLIPMASK_VEHICLE, this); + delete clipModel; + blocked = contents != 0; + } + + if (!blocked) { + SpawnConsole(); + } +} + +void hhShuttleDock::Event_Remove() { + if (IsLocked()) { + Unlock(); + } + if (dockedShuttle.IsValid()) { + DetachShuttle(dockedShuttle.GetEntity()); + } + hhDock::Event_Remove(); +} + +hhShuttle *hhShuttleDock::GetDockedShuttle(void) { + if (!dockedShuttle.IsValid()) { + return NULL; + } + return dockedShuttle.GetEntity(); +} diff --git a/src/Prey/game_shuttledock.h b/src/Prey/game_shuttledock.h new file mode 100644 index 0000000..acfecff --- /dev/null +++ b/src/Prey/game_shuttledock.h @@ -0,0 +1,67 @@ + +#ifndef __GAME_SHUTTLEDOCK_H__ +#define __GAME_SHUTTLEDOCK_H__ + +class hhShuttleDock : public hhDock { +public: + CLASS_PROTOTYPE( hhShuttleDock ); + + hhShuttleDock(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + virtual void Think(); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void ShuttleExit(hhShuttle *shuttle); + virtual bool IsLocked() { return bLocked; } + virtual bool CanExitLocked() { return bCanExitLocked; } + virtual bool AllowsBoost() { return !bLocked; } + virtual bool AllowsFiring() { return false; } + virtual bool AllowsExit(); + virtual bool IsTeleportDest(){ return true; } + virtual void UpdateAxis( const idMat3 &newAxis ) { } + + virtual bool Recharges() const { return true; } + + virtual hhShuttle *GetDockedShuttle(void); + +protected: + void AttachShuttle(hhShuttle *shuttle); + void DetachShuttle(hhShuttle *shuttle); + hhBeamSystem * SpawnDockingBeam(idVec3 &offset); + void SpawnConsole(); + virtual void Lock(); + virtual void Unlock(); + + void Event_SpawnConsole(); + virtual void Event_Remove(); + virtual void Event_PostSpawn(); + +protected: + idVec3 offsetNozzle; + idVec3 offsetConsole; + idVec3 offsetShuttlePoint; + idEntityPtr dockedShuttle; + idEntityPtr dockingBeam; + hhForce_Converge dockingForce; + int shuttleCount; + int lastConsoleAttempt; + int amountHealth; + int amountPower; + float maxDistance; + bool bLocked; + bool bLockOnEntry; + bool bCanExitLocked; + bool bPlayingRechargeSound; +}; + +#endif diff --git a/src/Prey/game_shuttletransport.cpp b/src/Prey/game_shuttletransport.cpp new file mode 100644 index 0000000..79757f6 --- /dev/null +++ b/src/Prey/game_shuttletransport.cpp @@ -0,0 +1,244 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//----------------------------------------------------------------------- +// +// hhShuttleTransport +// +//----------------------------------------------------------------------- + +const idEventDef EV_FadeOutTransporter("fadeOutTransporter"); + +CLASS_DECLARATION(hhDock, hhShuttleTransport) + EVENT( EV_DockLock, hhShuttleTransport::Event_Lock) + EVENT( EV_DockUnlock, hhShuttleTransport::Event_Unlock) + EVENT( EV_Activate, hhShuttleTransport::Event_Activate) + EVENT( EV_FadeOutTransporter, hhShuttleTransport::Event_FadeOut) + EVENT( EV_Remove, hhShuttleTransport::Event_Remove) +END_CLASS + +void hhShuttleTransport::Spawn() { + dockingBeam = NULL; + dockedShuttle = NULL; + shuttleCount = 0; + bLocked = false; + + amountHealth = spawnArgs.GetInt("amounthealth"); + amountPower = spawnArgs.GetInt("amountpower"); + + bCanExitLocked = spawnArgs.GetBool("canExitLocked"); + bLockOnEntry = spawnArgs.GetBool("lockOnEntry"); + bAllowFiring = spawnArgs.GetBool("allowFiring"); + offsetNozzle = spawnArgs.GetVector("offset_nozzle1"); +// offsetNozzle2 = spawnArgs.GetVector("offset_nozzle2"); + offsetShuttlePoint = spawnArgs.GetVector("offset_shuttlepoint"); + +// dockingBeam = SpawnDockingBeam(offsetNozzle); + + dockingForce.SetRestoreFactor(spawnArgs.GetFloat("dockingforce")); + dockingForce.SetTarget(GetOrigin() + offsetShuttlePoint * GetAxis()); + + // Fade in + SetShaderParm(SHADERPARM_TIMEOFFSET, -MS2SEC(gameLocal.time)); // Growth start time + SetShaderParm(5, 1.0f); // Growth direction (in) + SetShaderParm(6, 1.0f); // Make Beam opaque + StartSound("snd_fadein", SND_CHANNEL_ANY); +} + +void hhShuttleTransport::Save(idSaveGame *savefile) const { + savefile->WriteVec3( offsetNozzle ); + savefile->WriteVec3( offsetShuttlePoint ); + savefile->WriteObject( dockedShuttle ); + savefile->WriteObject( dockingBeam ); + savefile->WriteStaticObject( dockingForce ); + savefile->WriteInt( shuttleCount ); + savefile->WriteInt( amountHealth ); + savefile->WriteInt( amountPower ); + savefile->WriteBool( bLocked ); + savefile->WriteBool( bLockOnEntry ); + savefile->WriteBool( bCanExitLocked ); +} + +void hhShuttleTransport::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3( offsetNozzle ); + savefile->ReadVec3( offsetShuttlePoint ); + savefile->ReadObject( reinterpret_cast(dockedShuttle) ); + savefile->ReadObject( reinterpret_cast(dockingBeam) ); + savefile->ReadStaticObject( dockingForce ); + savefile->ReadInt( shuttleCount ); + savefile->ReadInt( amountHealth ); + savefile->ReadInt( amountPower ); + savefile->ReadBool( bLocked ); + savefile->ReadBool( bLockOnEntry ); + savefile->ReadBool( bCanExitLocked ); +} + +hhBeamSystem *hhShuttleTransport::SpawnDockingBeam(idVec3 &offset) { + idVec3 pos = GetPhysics()->GetOrigin() + offset * GetPhysics()->GetAxis(); + hhBeamSystem *beam = hhBeamSystem::SpawnBeam(pos, spawnArgs.GetString("beam_docking")); + assert(beam); + beam->SetOrigin(pos); + beam->SetAxis(GetPhysics()->GetAxis()); + beam->Bind(this, false); + beam->Hide(); + return beam; +} + +bool hhShuttleTransport::ValidEntity(idEntity *ent) { + return (ent->IsType(hhShuttle::Type) && !ent->IsHidden()); +} + +void hhShuttleTransport::EntityEntered(idEntity *ent) { + if (ent->IsType(hhShuttle::Type)) { + shuttleCount++; + hhShuttle *shuttle = static_cast(ent); + if (shuttle->IsVehicle()) { + AttachShuttle(shuttle); + } + } +} + +void hhShuttleTransport::EntityEncroaching(idEntity *ent) { + + if (ent->IsType(hhShuttle::Type)) { + hhShuttle *shuttle = static_cast(ent); + + // Check for any shuttles that just became valid + if (dockedShuttle == NULL && shuttle->IsVehicle()) { + AttachShuttle(shuttle); + } + + // Recharge docked shuttles + if (shuttle == dockedShuttle) { + + //HUMANHEAD bjk PCF (4-27-06) - shuttle recharge was slow + if(USERCMD_HZ == 30) { + shuttle->GiveHealth(2*amountHealth); + shuttle->GivePower(2*amountPower); + } else { + shuttle->GiveHealth(amountHealth); + shuttle->GivePower(amountPower); + } + } + } +} + +void hhShuttleTransport::EntityLeaving(idEntity *ent) { + if (ent->IsType(hhShuttle::Type)) { + shuttleCount--; + shuttleCount = idMath::ClampInt(0, shuttleCount, shuttleCount); + if (ent == dockedShuttle && !IsLocked()) { + DetachShuttle(dockedShuttle); + } + } +} + +void hhShuttleTransport::AttachShuttle(hhShuttle *shuttle) { + if (dockedShuttle == NULL) { + dockedShuttle = shuttle; + dockingForce.SetEntity(dockedShuttle); + if (dockingBeam) { + dockingBeam->SetTargetEntity(dockedShuttle, 0, dockedShuttle->spawnArgs.GetVector("offset_dockingpoint")); + dockingBeam->Activate( true ); + } + + dockedShuttle->SetDock(this); + dockedShuttle->InitiateRecharging(); + StartSound("snd_getin", SND_CHANNEL_ANY); + StartSound("snd_looper", SND_CHANNEL_DOCKED); + + if (bLockOnEntry) { + Lock(); + } + ActivateTargets( shuttle ); // Fire triggers + BecomeActive(TH_THINK); + SetShaderParm(6, 0.0f); // Make beam transparent + } +} + +void hhShuttleTransport::DetachShuttle(hhShuttle *shuttle) { + assert (shuttle==dockedShuttle); + if (dockedShuttle != NULL) { + dockingForce.SetEntity(NULL); + if (dockingBeam) { + dockingBeam->SetTargetEntity(NULL); + dockingBeam->Activate( false ); + } + + dockedShuttle->SetDock(NULL); + dockedShuttle->FinishRecharging(); + dockedShuttle = NULL; + StartSound("snd_getout", SND_CHANNEL_ANY); + StopSound(SND_CHANNEL_DOCKED); + Unlock(); // Shouldn't get in here when locked except when a shuttle dies within + BecomeInactive(TH_THINK); // Keep thinking so the restore position is updated + SetShaderParm(6, 1.0f); // Make beam opaque + } +} + +void hhShuttleTransport::Think() { + hhDock::Think(); + if (thinkFlags & TH_THINK) { + // Apply docking force to shuttle if docked, even if not in zone + assert(dockingForce.GetEntity() == dockedShuttle); + dockingForce.SetTarget(GetOrigin() + offsetShuttlePoint * GetAxis()); + dockingForce.Evaluate(gameLocal.time); + } +} + +void hhShuttleTransport::ShuttleExit(hhShuttle *shuttle) { + DetachShuttle(shuttle); + + // NOTE: Since zones don't get exit messages for entities that are destroyed, this is needed to + // decrease the shuttle count manually. + // However, since the shuttle physics shrinks when released, it's possible to get "nearly" outside + // the zone before releasing, in which case the exit DOES get called, screwing up the count. + // So... we bound the count to zero. + shuttleCount--; + shuttleCount = idMath::ClampInt(0, shuttleCount, shuttleCount); +} + +void hhShuttleTransport::UpdateAxis( const idMat3 &newAxis ) { + // Yaw transport to match yaw of shuttle + idAngles ang = newAxis.ToAngles(); + ang.pitch = ang.roll = 0.0f; + SetAxis(ang.ToMat3()); +} + +void hhShuttleTransport::Lock() { + bLocked = true; + dockedShuttle->SetOrigin(GetOrigin() + offsetShuttlePoint * GetAxis()); + dockingForce.SetRestoreFactor(spawnArgs.GetFloat("dockingforce_locked")); + dockedShuttle->Bind(this, false); +} + +void hhShuttleTransport::Unlock() { + bLocked = false; + dockingForce.SetRestoreFactor(spawnArgs.GetFloat("dockingforce")); + if (dockedShuttle) { + dockedShuttle->Unbind(); + } +} + +void hhShuttleTransport::Event_FadeOut() { + // Fade out + SetShaderParm(SHADERPARM_TIMEOFFSET, -MS2SEC(gameLocal.time)); // Growth start time + SetShaderParm(5, -1.0f); // Growth direction (out) + SetShaderParm(6, 0.0f); // Make Beam translucent + StartSound("snd_fadeout", SND_CHANNEL_ANY); + + PostEventMS(&EV_Remove, 1000); +} + +void hhShuttleTransport::Event_Remove() { + if (IsLocked()) { + Unlock(); + } + if (dockedShuttle) { + DetachShuttle(dockedShuttle); + } + hhDock::Event_Remove(); +} diff --git a/src/Prey/game_shuttletransport.h b/src/Prey/game_shuttletransport.h new file mode 100644 index 0000000..efe0e8f --- /dev/null +++ b/src/Prey/game_shuttletransport.h @@ -0,0 +1,51 @@ + +#ifndef __GAME_SHUTTLETRANSPORT_H__ +#define __GAME_SHUTTLETRANSPORT_H__ + +class hhShuttleTransport : public hhDock { +public: + CLASS_PROTOTYPE( hhShuttleTransport ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think(); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void ShuttleExit(hhShuttle *shuttle); + virtual bool IsLocked() { return bLocked; } + virtual bool CanExitLocked() { return bCanExitLocked; } + virtual bool AllowsBoost() { return false; } + virtual bool AllowsFiring() { return bAllowFiring; } + virtual bool AllowsExit() { return false; } + virtual bool IsTeleportDest(){ return false; } + virtual void UpdateAxis( const idMat3 &newAxis ); + +protected: + void AttachShuttle(hhShuttle *shuttle); + void DetachShuttle(hhShuttle *shuttle); + hhBeamSystem * SpawnDockingBeam(idVec3 &offset); + virtual void Lock(); + virtual void Unlock(); + void Event_FadeOut(); + virtual void Event_Remove(); + +protected: + idVec3 offsetNozzle; + idVec3 offsetShuttlePoint; + hhShuttle * dockedShuttle; + hhBeamSystem * dockingBeam; + hhForce_Converge dockingForce; + int shuttleCount; + int amountHealth; + int amountPower; + bool bLocked; + bool bLockOnEntry; + bool bCanExitLocked; + bool bAllowFiring; +}; + +#endif diff --git a/src/Prey/game_skybox.cpp b/src/Prey/game_skybox.cpp new file mode 100644 index 0000000..53b1c82 --- /dev/null +++ b/src/Prey/game_skybox.cpp @@ -0,0 +1,91 @@ +//************************************************************************** +//** +//** GAME_SKYBOX.CPP +//** +//** Game code for Prey-specific skyboxes +//** +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS ------------------------------------------------------------------ + +// TYPES ------------------------------------------------------------------- + +// CLASS DECLARATIONS ------------------------------------------------------ + +CLASS_DECLARATION(idEntity, hhSkybox) +END_CLASS + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// hhSkybox::Spawn +// +//========================================================================== + +void hhSkybox::Spawn(void) { + // Setup the initial state for the skybox + SetSkin( NULL ); + + BecomeActive( TH_UPDATEVISUALS ); + + UpdateVisuals(); +} + +/* +================ +hhSkybox::Present + +Present is called to allow entities to generate refEntities, lights, etc for the renderer. +================ +*/ +void hhSkybox::Present( void ) { + PROFILE_SCOPE("Present", PROFMASK_NORMAL); + + if ( !gameLocal.isNewFrame ) { + return; + } + + // don't present to the renderer if the entity hasn't changed + if ( !( thinkFlags & TH_UPDATEVISUALS ) ) { + return; + } + BecomeInactive( TH_UPDATEVISUALS ); + + // camera target for remote render views + // HUMANHEAD tmj: this is the only change from idEntity::Present. Don't care if the + // skybox is in the PVS since we only build the remoteRenderView on the first think + // before the skybox goes inactive. + if ( cameraTarget ) { + renderEntity.remoteRenderView = cameraTarget->GetRenderView(); + } + + // if set to invisible, skip + if ( !renderEntity.hModel || IsHidden() ) { + return; + } + + // add to refresh list + if ( modelDefHandle == -1 ) { + modelDefHandle = gameRenderWorld->AddEntityDef( &renderEntity ); + } else { + gameRenderWorld->UpdateEntityDef( modelDefHandle, &renderEntity ); + } +} diff --git a/src/Prey/game_skybox.h b/src/Prey/game_skybox.h new file mode 100644 index 0000000..b3e67dc --- /dev/null +++ b/src/Prey/game_skybox.h @@ -0,0 +1,14 @@ + +#ifndef __GAME_SKYBOX_H__ +#define __GAME_SKYBOX_H__ + +class hhSkybox : public idEntity { +public: + CLASS_PROTOTYPE( hhSkybox ); + + void Spawn(void); + + virtual void Present( void ); +}; + +#endif /* __GAME_SKYBOX_H__ */ diff --git a/src/Prey/game_slots.cpp b/src/Prey/game_slots.cpp new file mode 100644 index 0000000..2090721 --- /dev/null +++ b/src/Prey/game_slots.cpp @@ -0,0 +1,417 @@ +// hhSlots +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +enum { + SLOTRESULT_NONE=0, + SLOTRESULT_LOSE=1, + SLOTRESULT_WIN=2 +}; + +const idEventDef EV_Spin("spin", NULL); + +CLASS_DECLARATION(hhConsole, hhSlots) + EVENT( EV_Spin, hhSlots::Event_Spin) +END_CLASS + + +void hhSlots::Spawn() { + int ix; + + fruitTextures[FRUIT_CHERRY] = spawnArgs.GetString("mtr_cherry"); + fruitTextures[FRUIT_ORANGE] = spawnArgs.GetString("mtr_orange"); + fruitTextures[FRUIT_LEMON] = spawnArgs.GetString("mtr_lemon"); + fruitTextures[FRUIT_APPLE] = spawnArgs.GetString("mtr_apple"); + fruitTextures[FRUIT_GRAPE] = spawnArgs.GetString("mtr_grape"); + fruitTextures[FRUIT_MELON] = spawnArgs.GetString("mtr_melon"); + fruitTextures[FRUIT_BAR] = spawnArgs.GetString("mtr_bar"); + fruitTextures[FRUIT_BARBAR] = spawnArgs.GetString("mtr_barbar"); + fruitTextures[FRUIT_BARBARBAR] = spawnArgs.GetString("mtr_barbarbar"); + + for (ix=0; ixWriteInt( Bet ); + savefile->WriteInt( PlayerBet ); + savefile->WriteInt( PlayerCredits ); + savefile->WriteInt( result ); + savefile->WriteInt( creditsWon ); + savefile->WriteInt( victoryAmount ); + for (i=0; iWriteString( fruitTextures[i] ); + } + + for (i=0; iWriteInt( reel1[i] ); + savefile->WriteInt( reel2[i] ); + savefile->WriteInt( reel3[i] ); + } + + savefile->WriteFloat( reelPos1 ); + savefile->WriteFloat( reelPos2 ); + savefile->WriteFloat( reelPos3 ); + + savefile->WriteFloat( reelRate1 ); + savefile->WriteFloat( reelRate2 ); + savefile->WriteFloat( reelRate3 ); + savefile->WriteBool( bSpinning ); + savefile->WriteBool( bCanSpin ); + savefile->WriteBool( bCanIncBet ); + savefile->WriteBool( bCanDecBet ); +} + +void hhSlots::Restore( idRestoreGame *savefile ) { + int i; + savefile->ReadInt( Bet ); + savefile->ReadInt( PlayerBet ); + savefile->ReadInt( PlayerCredits ); + savefile->ReadInt( result ); + savefile->ReadInt( creditsWon ); + savefile->ReadInt( victoryAmount ); + for (i=0; iReadString( fruitTextures[i] ); + } + + for (i=0; iReadInt( (int&)reel1[i] ); + savefile->ReadInt( (int&)reel2[i] ); + savefile->ReadInt( (int&)reel3[i] ); + } + + savefile->ReadFloat( reelPos1 ); + savefile->ReadFloat( reelPos2 ); + savefile->ReadFloat( reelPos3 ); + + savefile->ReadFloat( reelRate1 ); + savefile->ReadFloat( reelRate2 ); + savefile->ReadFloat( reelRate3 ); + savefile->ReadBool( bSpinning ); + savefile->ReadBool( bCanSpin ); + savefile->ReadBool( bCanIncBet ); + savefile->ReadBool( bCanDecBet ); +} + +void hhSlots::Spin() { + bSpinning = true; + Bet = PlayerBet; + bCanSpin = bCanIncBet = bCanDecBet = false; + reelRate1 = 2000+gameLocal.random.RandomFloat()*100; + reelRate2 = 3000+gameLocal.random.RandomFloat()*100; + reelRate3 = 4000+gameLocal.random.RandomFloat()*100; + result = SLOTRESULT_NONE; + creditsWon = 0; +} + +void hhSlots::IncBet() { + int amount = 1; + idUserInterface *gui = renderEntity.gui[0]; + if (gui) { + amount = gui->GetStateInt("increment"); + } + + if (bCanIncBet) { + int oldBet = PlayerBet; + PlayerBet = idMath::ClampInt(PlayerBet, PlayerCredits, PlayerBet+amount); + PlayerBet = idMath::ClampInt(0, 999999, PlayerBet); + if (PlayerBet != oldBet) { + StartSound( "snd_betchange", SND_CHANNEL_ANY ); + } + } + UpdateView(); +} + +void hhSlots::DecBet() { + int amount = 1; + idUserInterface *gui = renderEntity.gui[0]; + if (gui) { + amount = gui->GetStateInt("increment"); + } + + if (bCanDecBet) { + int oldBet = PlayerBet; + if (PlayerBet > amount) { + PlayerBet -= amount; + } + else if (PlayerBet > 1) { + PlayerBet = 1; + } + if (PlayerBet != oldBet) { + StartSound( "snd_betchange", SND_CHANNEL_ANY ); + } + } + UpdateView(); +} + +int MaskForFruit(fruit_t fruit) { + int mask = -1; + switch(fruit) { + case FRUIT_CHERRY: mask = MASK_CHERRY; break; + case FRUIT_ORANGE: mask = MASK_ORANGE; break; + case FRUIT_LEMON: mask = MASK_LEMON; break; + case FRUIT_APPLE: mask = MASK_APPLE; break; + case FRUIT_GRAPE: mask = MASK_GRAPE; break; + case FRUIT_MELON: mask = MASK_MELON; break; + case FRUIT_BAR: mask = MASK_BAR; break; + case FRUIT_BARBAR: mask = MASK_BARBAR; break; + case FRUIT_BARBARBAR: mask = MASK_BARBARBAR; break; + } + assert(mask != -1); + return mask; +} + +int ReelPos2Slot(int rpos) { + return ((rpos + REEL_LENGTH) % REEL_LENGTH) / SLOT_HEIGHT; +} + +int ReelPos2SlotPos(int rpos) { + return ((rpos + REEL_LENGTH) % REEL_LENGTH) % SLOT_HEIGHT; +} + + +void hhSlots::UpdateView() { + bool bGameOver = false; + idUserInterface *gui = renderEntity.gui[0]; + + if (gui) { + if (PlayerCredits <= 0) { + bCanIncBet = bCanDecBet = bCanSpin = false; + bGameOver = true; + } + + gui->SetStateBool("bgameover", bGameOver); + gui->SetStateBool("bcanincbet", bCanIncBet); + gui->SetStateBool("bcandecbet", bCanDecBet); + gui->SetStateBool("bcanspin", bCanSpin); + gui->SetStateInt("currentbet", PlayerBet); + gui->SetStateInt("credits", PlayerCredits); + gui->SetStateInt("result", result); + gui->SetStateInt("creditswon", creditsWon); + + gui->SetStateInt("reel1pos", (int)reelPos1); + gui->SetStateInt("reel2pos", (int)reelPos2); + gui->SetStateInt("reel3pos", (int)reelPos3); + + gui->SetStateInt("reel1rate", (int)reelRate1); + gui->SetStateInt("reel2rate", (int)reelRate2); + gui->SetStateInt("reel3rate", (int)reelRate3); + + // Clear + int fruit1, fruit2, fruit3; + + // Reel 1 + fruit1 = reel1[ ReelPos2Slot( reelPos1-SLOT_HEIGHT )]; + fruit2 = reel1[ ReelPos2Slot( reelPos1 )]; + fruit3 = reel1[ ReelPos2Slot( reelPos1+SLOT_HEIGHT )]; + gui->SetStateString("r1s1_texture", fruitTextures[fruit1]); + gui->SetStateString("r1s2_texture", fruitTextures[fruit2]); + gui->SetStateString("r1s3_texture", fruitTextures[fruit3]); + + // Reel 2 + fruit1 = reel2[ ReelPos2Slot( reelPos2-SLOT_HEIGHT )]; + fruit2 = reel2[ ReelPos2Slot( reelPos2 )]; + fruit3 = reel2[ ReelPos2Slot( reelPos2+SLOT_HEIGHT )]; + gui->SetStateString("r2s1_texture", fruitTextures[fruit1]); + gui->SetStateString("r2s2_texture", fruitTextures[fruit2]); + gui->SetStateString("r2s3_texture", fruitTextures[fruit3]); + + // Reel 3 + fruit1 = reel3[ ReelPos2Slot( reelPos3-SLOT_HEIGHT )]; + fruit2 = reel3[ ReelPos2Slot( reelPos3 )]; + fruit3 = reel3[ ReelPos2Slot( reelPos3+SLOT_HEIGHT )]; + gui->SetStateString("r3s1_texture", fruitTextures[fruit1]); + gui->SetStateString("r3s2_texture", fruitTextures[fruit2]); + gui->SetStateString("r3s3_texture", fruitTextures[fruit3]); + + gui->StateChanged(gameLocal.time, true); + CallNamedEvent("Update"); + } +} + +void hhSlots::CheckVictory() { + + #define NUM_VICTORIES 16 + static victory_t victoryTable[NUM_VICTORIES] = { + { MASK_BARBARBAR, MASK_BARBARBAR, MASK_BARBARBAR, 10000}, + { MASK_BARBAR, MASK_BARBAR, MASK_BARBAR, 1000}, + { MASK_BAR, MASK_BAR, MASK_BAR, 500}, + { MASK_ANYBAR, MASK_ANYBAR, MASK_ANYBAR, 100}, + + { MASK_MELON, MASK_MELON, MASK_MELON, 60}, + { MASK_GRAPE, MASK_GRAPE, MASK_GRAPE, 50}, + { MASK_APPLE, MASK_APPLE, MASK_APPLE, 40}, + { MASK_LEMON, MASK_LEMON, MASK_LEMON, 30}, + { MASK_ORANGE, MASK_ORANGE, MASK_ORANGE, 20}, + { MASK_CHERRY, MASK_CHERRY, MASK_CHERRY, 10}, + + { MASK_CHERRY, MASK_CHERRY, MASK_ANY, 5}, + { MASK_ANY, MASK_CHERRY, MASK_CHERRY, 5}, + { MASK_CHERRY, MASK_ANY, MASK_CHERRY, 5}, + + { MASK_CHERRY, MASK_ANY, MASK_ANY, 2}, + { MASK_ANY, MASK_CHERRY, MASK_ANY, 2}, + { MASK_ANY, MASK_ANY, MASK_CHERRY, 2} + }; + + PlayerCredits -= Bet; + result = SLOTRESULT_LOSE; + creditsWon = 0; + for (int ix=0; ix= victoryAmount) { + StartSound( "snd_victory", SND_CHANNEL_ANY ); + ActivateTargets( gameLocal.GetLocalPlayer() ); + victoryAmount = 0; + } + else if (victoryTable[ix].payoff > 5) { + StartSound( "snd_winbig", SND_CHANNEL_ANY ); + } + else { + StartSound( "snd_win", SND_CHANNEL_ANY ); + } + break; + } + } + + PlayerBet = idMath::ClampInt(0, PlayerCredits, PlayerBet); +} + + +bool hhSlots::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + + idToken token; + + if (!src->ReadToken(&token)) { + return false; + } + + if (token == ";") { + return false; + } + + if (token.Icmp("spin") == 0) { + BecomeActive(TH_MISC3); + StartSound( "snd_spin", SND_CHANNEL_ANY ); + Spin(); + } + else if (token.Icmp("incbet") == 0) { + IncBet(); + } + else if (token.Icmp("decbet") == 0) { + DecBet(); + } + else if (token.Icmp("reset") == 0) { + Reset(); + } + else if (token.Icmp("restart") == 0) { + bCanSpin = 1; + bCanIncBet = 1; + bCanDecBet = 1; + PlayerCredits = spawnArgs.GetInt("credits"); + Bet = PlayerBet = 1; + UpdateView(); + } + else { + src->UnreadToken(&token); + return false; + } + + return true; +} + +void hhSlots::Think() { + hhConsole::Think(); + + if (thinkFlags & TH_MISC3) { + if (bSpinning) { + + float deltaTime = MS2SEC(gameLocal.msec); + if (reelRate1 > 0.0f) { + reelPos1 = ((int)(reelPos1 - reelRate1 * deltaTime) + REEL_LENGTH) % REEL_LENGTH; + reelRate1 *= 0.98f; + if (reelRate1 < MINIMUM_REEL_RATE) { + reelRate1 = 0.0f; + StartSound( "snd_stop", SND_CHANNEL_ANY ); + } + } + if (reelRate2 > 0.0f) { + reelPos2 = ((int)(reelPos2 - reelRate2 * deltaTime) + REEL_LENGTH) % REEL_LENGTH; + reelRate2 *= 0.98f; + if (reelRate2 < MINIMUM_REEL_RATE) { + reelRate2 = 0.0f; + StartSound( "snd_stop", SND_CHANNEL_ANY ); + } + } + if (reelRate3 > 0.0f) { + reelPos3 = ((int)(reelPos3 - reelRate3 * deltaTime) + REEL_LENGTH) % REEL_LENGTH; + reelRate3 *= 0.98f; + if (reelRate3 < MINIMUM_REEL_RATE) { + reelRate3 = 0.0f; + StartSound( "snd_stop", SND_CHANNEL_ANY ); + } + } + + if (reelRate1==0.0f && reelRate2==0.0f && reelRate3==0.0f) { + CheckVictory(); + bSpinning = false; + bCanSpin = bCanIncBet = bCanDecBet = true; + BecomeInactive(TH_MISC3); + } + + UpdateView(); + } + } +} + +void hhSlots::Event_Spin() { + Spin(); +} \ No newline at end of file diff --git a/src/Prey/game_slots.h b/src/Prey/game_slots.h new file mode 100644 index 0000000..59dcb11 --- /dev/null +++ b/src/Prey/game_slots.h @@ -0,0 +1,86 @@ + +#ifndef __GAME_SLOTS_H__ +#define __GAME_SLOTS_H__ + + #define SLOT_HEIGHT 100 + #define SLOTS_IN_REEL 10 + #define MINIMUM_REEL_RATE (SLOT_HEIGHT/2) + #define REEL_LENGTH (SLOTS_IN_REEL*SLOT_HEIGHT) + + #define MASK_CHERRY 0x00000001 + #define MASK_ORANGE 0x00000002 + #define MASK_LEMON 0x00000004 + #define MASK_APPLE 0x00000008 + #define MASK_GRAPE 0x00000010 + #define MASK_MELON 0x00000020 + #define MASK_BAR 0x00000100 + #define MASK_BARBAR 0x00000200 + #define MASK_BARBARBAR 0x00000400 + + #define MASK_ANYBAR 0x00000700 + #define MASK_ANY 0xffffffff + + typedef enum { + FRUIT_CHERRY=0, + FRUIT_ORANGE, + FRUIT_LEMON, + FRUIT_APPLE, + FRUIT_GRAPE, + FRUIT_MELON, + FRUIT_BAR, + FRUIT_BARBAR, + FRUIT_BARBARBAR, + NUM_FRUITS + }fruit_t; + + typedef struct victory_s { + int f1, f2, f3; + int payoff; + }victory_t; + + +class hhSlots : public hhConsole { +public: + CLASS_PROTOTYPE( hhSlots ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void Spin(); + void IncBet(); + void DecBet(); + void CheckVictory(); + + void Reset(); + void UpdateView(); + bool HandleSingleGuiCommand(idEntity *entityGui, idLexer *src); + +protected: + virtual void Think( void ); + void Event_Spin(); + +private: + int Bet; + int PlayerBet; + int PlayerCredits; + int result; + int creditsWon; + int victoryAmount; + + idStr fruitTextures[NUM_FRUITS]; + + fruit_t reel1[SLOTS_IN_REEL]; + fruit_t reel2[SLOTS_IN_REEL]; + fruit_t reel3[SLOTS_IN_REEL]; + float reelPos1, reelPos2, reelPos3; + float reelRate1, reelRate2, reelRate3; + bool bSpinning; + + bool bCanSpin; + bool bCanIncBet; + bool bCanDecBet; +}; + + +#endif /* __GAME_SLOTS_H__ */ diff --git a/src/Prey/game_sphere.cpp b/src/Prey/game_sphere.cpp new file mode 100644 index 0000000..c4fd166 --- /dev/null +++ b/src/Prey/game_sphere.cpp @@ -0,0 +1,151 @@ +// Game_Sphere.cpp +// +// pushable exploding sphere + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + + +CLASS_DECLARATION(hhMoveable, hhSphere) + EVENT( EV_Touch, hhSphere::Event_Touch ) +END_CLASS + + +hhSphere::hhSphere() { + additionalAxis.Identity(); +} + +void hhSphere::Spawn(void) { + + radius = (GetPhysics()->GetBounds()[1][0] - GetPhysics()->GetBounds()[0][0]) * 0.5f; + lastOrigin = GetPhysics()->GetOrigin(); + additionalAxis.Identity(); + CreateLight(); + + fl.takedamage = false; + BecomeActive(TH_THINK); +} + +void hhSphere::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( radius ); + savefile->WriteVec3( lastOrigin ); + savefile->WriteMat3( additionalAxis ); + light.Save(savefile); +} + +void hhSphere::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( radius ); + savefile->ReadVec3( lastOrigin ); + savefile->ReadMat3( additionalAxis ); + light.Restore(savefile); +} + +void hhSphere::CreateLight() { + if ( spawnArgs.GetBool("haslight") ) { + idStr light_shader = spawnArgs.GetString("mtr_light"); + idVec3 light_color = spawnArgs.GetVector("light_color"); + idVec3 light_frustum = spawnArgs.GetVector("light_frustum"); + idVec3 light_offset = spawnArgs.GetVector("offset_light"); + idVec3 light_target = spawnArgs.GetVector("offset_lighttarget"); + + idDict args; + idVec3 lightOrigin = GetPhysics()->GetOrigin() + light_offset * GetPhysics()->GetAxis(); + light_target.Normalize(); + idMat3 lightAxis = (light_target * GetPhysics()->GetAxis()).hhToMat3(); + + if ( light_shader.Length() ) { + args.Set( "texture", light_shader ); + } + args.SetVector( "origin", lightOrigin ); + args.Set ("angles", lightAxis.ToAngles().ToString()); + args.SetVector( "_color", light_color ); + args.SetVector( "light_target", lightAxis[0] * light_frustum.x ); + args.SetVector( "light_right", lightAxis[1] * light_frustum.y ); + args.SetVector( "light_up", lightAxis[2] * light_frustum.z ); + light = ( idLight * )gameLocal.SpawnEntityType( idLight::Type, &args ); + light->Bind(this, true); + light->SetLightParm( 6, 1.0f ); + light->SetLightParm( SHADERPARM_TIMEOFFSET, -MS2SEC(gameLocal.time) ); + } +} + +void hhSphere::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + // skip idMoveable::Damage which handles damage differently + idEntity::Damage(inflictor, attacker, dir, damageDefName, damageScale, location); +} + + +void hhSphere::RollThink( void ) { + float movedDistance, angle; + idVec3 curOrigin, gravityNormal, dir; + + bool wasAtRest = IsAtRest(); + + RunPhysics(); + + // only need to give the visual model an additional rotation if the physics were run + if ( !wasAtRest ) { + + // current physics state + curOrigin = GetPhysics()->GetOrigin(); + + dir = curOrigin - lastOrigin; + float movedDistanceSquared = dir.LengthSqr(); + + // if the sphere moved + if ( movedDistanceSquared > 0.0f && movedDistanceSquared < 20.0f) { + + gravityNormal = GetPhysics()->GetGravityNormal(); + + // movement since last frame + movedDistance = idMath::Sqrt( movedDistanceSquared ); + dir *= 1.0f / movedDistance; + + // Get local coordinate axes + idVec3 right = -dir.Cross(gravityNormal); + + // Rotate about it proportional to the distance moved using axis/angle + angle = 180.0f * movedDistance / (radius*idMath::PI); + additionalAxis *= (idRotation( vec3_origin, right, angle).ToMat3()); + } + + // save state for next think + lastOrigin = curOrigin; + } + + Present(); +} + +void hhSphere::Think() { + RollThink(); +} + +void hhSphere::ClientPredictionThink( void ) { + RollThink(); +} + +bool hhSphere::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + origin = vec3_origin; + axis = additionalAxis * GetPhysics()->GetAxis().Inverse(); + return true; +} + +void hhSphere::Event_Touch( idEntity *other, trace_t *trace ) { + if (spawnArgs.GetBool("walkthrough")) { + idVec3 otherVel = other->GetPhysics()->GetLinearVelocity(); + float otherSpeed = otherVel.NormalizeFast(); + if (otherSpeed > 50.0f) { // && GetPhysics()->IsAtRest()) { + idVec3 toSide = hhUtils::RandomSign() * other->GetAxis()[1]; + idVec3 toMoveable = GetOrigin() - other->GetOrigin(); + toMoveable.NormalizeFast(); + idVec3 newVel = ( 3*otherVel + toSide + hhUtils::RandomVector() ) * (1.0f/5.0f); + newVel.z = 0.0f; + newVel.NormalizeFast(); + newVel *= otherSpeed*1.5f; + GetPhysics()->SetLinearVelocity(newVel); + } + } +} diff --git a/src/Prey/game_sphere.h b/src/Prey/game_sphere.h new file mode 100644 index 0000000..ca48f4b --- /dev/null +++ b/src/Prey/game_sphere.h @@ -0,0 +1,31 @@ + +#ifndef __GAME_SPHERE_H__ +#define __GAME_SPHERE_H__ + +class hhSphere : public hhMoveable { +public: + CLASS_PROTOTYPE( hhSphere ); + + hhSphere(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); + virtual void Think( void ); + virtual void ClientPredictionThink( void ); + +protected: + void RollThink( void ); + void CreateLight(); + void Event_Touch( idEntity *other, trace_t *trace ); + +private: + float radius; // radius of sphere + idVec3 lastOrigin; // origin last frame + idMat3 additionalAxis; // transformation for visual model + idEntityPtr light; // light +}; + + +#endif /* __GAME_SPHERE_H__ */ diff --git a/src/Prey/game_spherepart.cpp b/src/Prey/game_spherepart.cpp new file mode 100644 index 0000000..c2b7db2 --- /dev/null +++ b/src/Prey/game_spherepart.cpp @@ -0,0 +1,319 @@ +//************************************************************************** +//** +//** GAME_SPHEREPART.CPP +//** +//** SphereParts are generic environment objects that animate and can be +//** interacted with in simple general ways: Touched, shot / killed. +//** +//** Pulse Tubes should be able to: +//** - Animate +//** - Randomly play other anims +//** - Take Pain +//** - Die (?) +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS ------------------------------------------------------------------ + +// TYPES ------------------------------------------------------------------- + +// CLASS DECLARATIONS ------------------------------------------------------ + +const idEventDef EV_Pulse("pulse", NULL); +const idEventDef EV_PlayIdle("", NULL); + +CLASS_DECLARATION( hhAnimatedEntity, hhSpherePart ) + EVENT( EV_Pulse, hhSpherePart::Event_Pulse ) + EVENT( EV_Activate, hhSpherePart::Event_Trigger ) + EVENT( EV_PlayIdle, hhSpherePart::Event_PlayIdle ) +END_CLASS + +// STATE DECLARATIONS ------------------------------------------------------- + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// hhSpherePart::Spawn +// +//========================================================================== + +void hhSpherePart::Spawn(void) { + GetPhysics()->SetContents( CONTENTS_BODY ); + + fl.takedamage = true; // Allow the spherepart to be damaged + + spawnArgs.GetFloat("pulsetime", "10", pulseTime); // A pulsetime of zero will ensure that the object never pulses + spawnArgs.GetFloat("pulserandom", "5", pulseRandom); + + idleAnim = GetAnimator()->GetAnim("idle"); + painAnim = GetAnimator()->GetAnim("pain"); + pulseAnim = GetAnimator()->GetAnim("pulse"); + triggerAnim = GetAnimator()->GetAnim("trigger"); + + BecomeActive(TH_THINK); + + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleAnim, gameLocal.time, 100); + StartSound( "snd_idle", SND_CHANNEL_ANY ); + + if ( pulseAnim && pulseTime ) { + PostEventSec(&EV_Pulse, pulseTime + gameLocal.random.RandomFloat() * pulseRandom); + } +} + +void hhSpherePart::Save(idSaveGame *savefile) const { + savefile->WriteFloat( pulseTime ); + savefile->WriteFloat( pulseRandom ); + savefile->WriteInt( idleAnim ); + savefile->WriteInt( painAnim ); + savefile->WriteInt( pulseAnim ); + savefile->WriteInt( triggerAnim ); +} + +void hhSpherePart::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( pulseTime ); + savefile->ReadFloat( pulseRandom ); + savefile->ReadInt( idleAnim ); + savefile->ReadInt( painAnim ); + savefile->ReadInt( pulseAnim ); + savefile->ReadInt( triggerAnim ); +} + +//========================================================================== +// +// hhSpherePart::~hhSpherePart +// +//========================================================================== +hhSpherePart::~hhSpherePart() { +} + +//========================================================================== +// +// hhSpherePart::Damage +// +//========================================================================== + +void hhSpherePart::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + int startTime; + + + if ( painAnim && !GetAnimator()->IsAnimPlaying( GetAnimator()->GetAnim( painAnim ) ) ) { + StartSound( "snd_pain", SND_CHANNEL_ANY ); + GetAnimator()->ClearAllAnims( gameLocal.time, 100 ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, painAnim, gameLocal.time, 100); + + startTime = GetAnimator()->CurrentAnim( ANIMCHANNEL_ALL )->Length(); + PostEventMS( &EV_PlayIdle, startTime ); + } +} + +//========================================================================== +// +// hhSpherePart::Event_Trigger +// +//========================================================================== + +void hhSpherePart::Event_Trigger( idEntity *activator ) { + int startTime; + + if(triggerAnim) { + GetAnimator()->ClearAllAnims( gameLocal.time, 500 ); + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, triggerAnim, gameLocal.time, 500); + + startTime = GetAnimator()->GetAnim( triggerAnim )->Length(); + PostEventMS( &EV_PlayIdle, startTime ); + } +} + +//========================================================================== +// +// hhSpherePart::Event_Pulse +// +//========================================================================== + +void hhSpherePart::Event_Pulse(void) { + int startTime; + + if ( pulseAnim && pulseTime ) { + StartSound( "snd_pulse", SND_CHANNEL_ANY ); + + GetAnimator()->ClearAllAnims( gameLocal.time, 500 ); + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, pulseAnim, gameLocal.time, 500); + + startTime = GetAnimator()->GetAnim( pulseAnim )->Length(); + PostEventMS( &EV_PlayIdle, startTime ); + + PostEventSec(&EV_Pulse, pulseTime + gameLocal.random.RandomFloat() * pulseRandom); + } +} + +//========================================================================== +// +// hhSpherePart::Event_PlayIdle +// +//========================================================================== + +void hhSpherePart::Event_PlayIdle() { + // We are playing the idle, cancel any pending idles + CancelEvents( &EV_PlayIdle ); + + GetAnimator()->ClearAllAnims( gameLocal.time, 100 ); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleAnim, gameLocal.time, 100); +} + +/********************************************************************** + +hhGenericAnimatedPart + +**********************************************************************/ +CLASS_DECLARATION( hhAnimatedEntity, hhGenericAnimatedPart ) + EVENT( EV_PostSpawn, hhGenericAnimatedPart::Event_PostSpawn ) +END_CLASS + +/* +============ +hhGenericAnimatedPart::Spawn +============ +*/ +void hhGenericAnimatedPart::Spawn() { + if( spawnArgs.GetBool("solid", "1") ) { + fl.takedamage = true; + GetPhysics()->SetContents( CONTENTS_SOLID ); + } else { + fl.takedamage = false; + GetPhysics()->SetContents( 0 ); + GetPhysics()->UnlinkClip(); + } + + //rww - make sure it's sent + fl.networkSync = true; + + PostEventMS( &EV_PostSpawn, 0 ); +} + +void hhGenericAnimatedPart::Save(idSaveGame *savefile) const { + owner.Save(savefile); +} + +void hhGenericAnimatedPart::Restore( idRestoreGame *savefile ) { + owner.Restore(savefile); +} + +void hhGenericAnimatedPart::WriteToSnapshot( idBitMsgDelta &msg ) const +{ + GetPhysics()->WriteToSnapshot(msg); + msg.WriteBits(owner.GetSpawnId(), 32); +} + +void hhGenericAnimatedPart::ReadFromSnapshot( const idBitMsgDelta &msg ) +{ + GetPhysics()->ReadFromSnapshot(msg); + owner.SetSpawnId(msg.ReadBits(32)); +} + +void hhGenericAnimatedPart::ClientPredictionThink( void ) +{ + Think(); +} + + +/* +============ +hhGenericAnimatedPart::SetOwner +============ +*/ +void hhGenericAnimatedPart::SetOwner( idEntity* pOwner ) { + owner = pOwner; +} + +/* +============ +hhGenericAnimatedPart::LinkCombatModel +============ +*/ +void hhGenericAnimatedPart::LinkCombatModel( idEntity* self, const int renderModelHandle ) { + hhAnimatedEntity::LinkCombatModel( (owner.IsValid()) ? owner.GetEntity() : this, renderModelHandle ); +} + +/* +============ +hhGenericAnimatedPart::Damage +============ +*/ +void hhGenericAnimatedPart::Damage( idEntity* inflictor, idEntity* attacker, const idVec3& dir, const char* damageDefName, const float damageScale, const int location ) { + if( owner.IsValid() && spawnArgs.GetBool("transferDamage") ) { + owner->Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + } else { + hhAnimatedEntity::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + } +} + +/* +================ +hhGenericAnimatedPart::PlayAnim +================ +*/ +int hhGenericAnimatedPart::PlayAnim( const char* animName, int channel ) { + ClearAllAnims(); + + int anim = GetAnimator()->GetAnim( animName ); + if( !anim ) { + return 0; + } + + GetAnimator()->PlayAnim( channel, anim, gameLocal.GetTime(), 0 ); + return GetAnimator()->CurrentAnim( channel )->GetEndTime() - gameLocal.GetTime(); +} + +/* +================ +hhGenericAnimatedPart::CycleAnim +================ +*/ +void hhGenericAnimatedPart::CycleAnim( const char* animName, int channel ) { + ClearAllAnims(); + + int anim = GetAnimator()->GetAnim( animName ); + if( !anim ) { + return; + } + + GetAnimator()->CycleAnim( channel, anim, gameLocal.GetTime(), 0 ); +} + +/* +================ +hhGenericAnimatedPart::ClearAllAnims +================ +*/ +void hhGenericAnimatedPart::ClearAllAnims() { + GetAnimator()->ClearAllAnims( gameLocal.GetTime(), 0 ); +} + +/* +============ +hhGenericAnimatedPart::Event_PostSpawn +============ +*/ +void hhGenericAnimatedPart::Event_PostSpawn() { + idStr ownerName = spawnArgs.GetString( "owner" ); + if( ownerName.Length() ) { + SetOwner( gameLocal.FindEntity(ownerName.c_str()) ); + } +} diff --git a/src/Prey/game_spherepart.h b/src/Prey/game_spherepart.h new file mode 100644 index 0000000..b005344 --- /dev/null +++ b/src/Prey/game_spherepart.h @@ -0,0 +1,71 @@ + +#ifndef __GAME_SPHEREPART_H__ +#define __GAME_SPHEREPART_H__ + +extern const idEventDef EV_Pulse; +extern const idEventDef EV_PlayIdle; + +class hhSpherePart : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE(hhSpherePart); + + virtual ~hhSpherePart(); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + +protected: + void Event_Pulse(void); + void Event_Trigger( idEntity *activator ); + void Event_PlayIdle(); + +protected: + float pulseTime; // Time between pulses. Slight randomness is added + float pulseRandom; + + int idleAnim; + int painAnim; + int pulseAnim; + int triggerAnim; +}; + +/********************************************************************** + +hhGenericAnimatedPart + +**********************************************************************/ +class hhGenericAnimatedPart: public hhAnimatedEntity { + CLASS_PROTOTYPE( hhGenericAnimatedPart ); + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - networking + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + virtual void Damage( idEntity* inflictor, idEntity* attacker, const idVec3& dir, const char* damageDefName, const float damageScale, const int location ); + + virtual int PlayAnim( const char* animName, int channel ); + virtual void CycleAnim( const char* animName, int channel ); + virtual void ClearAllAnims(); + + void SetOwner( idEntity* pOwner ); + +protected: + virtual void LinkCombatModel( idEntity* self, const int renderModelHandle ); + +protected: + void Event_PostSpawn(); + +protected: + idEntityPtr owner; +}; + +#endif /* __GAME_SPHEREPART_H__ */ diff --git a/src/Prey/game_spring.cpp b/src/Prey/game_spring.cpp new file mode 100644 index 0000000..c3ad56e --- /dev/null +++ b/src/Prey/game_spring.cpp @@ -0,0 +1,191 @@ +/* +game_spring.cpp +*/ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +/*********************************************************************** + + hhSpring + +***********************************************************************/ + +const idEventDef EV_HHLinkSpring( "linkspring", "" ); +const idEventDef EV_HHUnlinkSpring( "unlinkspring", "" ); + +CLASS_DECLARATION( idEntity, hhSpring ) + EVENT( EV_HHLinkSpring, hhSpring::Event_LinkSpring ) + EVENT( EV_HHUnlinkSpring, hhSpring::Event_UnlinkSpring ) +END_CLASS + + +/* +================ +hhSpring::Spawn +================ +*/ +void hhSpring::Spawn( void ) { + float Kstretch, damping, restLength, Kcompress; + + spawnArgs.GetString( "ent1", "", name1 ); + spawnArgs.GetString( "ent2", "", name2 ); + spawnArgs.GetInt( "id1", "0", id1 ); + spawnArgs.GetInt( "id2", "0", id2 ); + spawnArgs.GetVector( "point1", "0 0 0", p1 ); + spawnArgs.GetVector( "point2", "0 0 0", p2 ); + spawnArgs.GetFloat( "constant", "100.0f", Kstretch ); + spawnArgs.GetFloat( "damping", "10.0f", damping ); + spawnArgs.GetFloat( "restlength", "0.0f", restLength ); + spawnArgs.GetFloat( "compress", "0.0f", Kcompress ); + + // HUMANHEAD: Added compression constant + spring.InitSpring( Kstretch, Kcompress, damping, restLength ); + + PostEventMS( &EV_HHLinkSpring, 0 ); +} + +void hhSpring::Save(idSaveGame *savefile) const { + savefile->WriteString( name1 ); + savefile->WriteString( name2 ); + physics1.Save( savefile ); + physics2.Save( savefile ); + savefile->WriteInt( id1 ); + savefile->WriteInt( id2 ); + savefile->WriteVec3( p1 ); + savefile->WriteVec3( p2 ); + savefile->WriteStaticObject( spring ); +} + +void hhSpring::Restore( idRestoreGame *savefile ) { + savefile->ReadString( name1 ); + savefile->ReadString( name2 ); + physics1.Restore( savefile ); + physics2.Restore( savefile ); + savefile->ReadInt( id1 ); + savefile->ReadInt( id2 ); + savefile->ReadVec3( p1 ); + savefile->ReadVec3( p2 ); + savefile->ReadStaticObject( spring ); +} + +/* +================ +hhSpring::Think + HUMANHEAD +================ +*/ +#define TEST_SPRINGS +void hhSpring::Think() { + if (thinkFlags & TH_THINK) { + spring.Evaluate( gameLocal.time ); + +#ifdef TEST_SPRINGS + idVec3 start, end, origin; + idMat3 axis; + + start = p1; + if ( physics1.IsValid() ) { + axis = physics1->GetPhysics()->GetAxis(id1); + origin = physics1->GetPhysics()->GetOrigin(id1); + start = origin + p1 * axis; + } + + end = p2; + if ( physics2.IsValid() ) { + axis = physics2->GetPhysics()->GetAxis(id2); + origin = physics2->GetPhysics()->GetOrigin(id2); + end = origin + p2 * axis; + } + + gameRenderWorld->DebugLine( colorYellow, start, end, 0, true ); +#endif + } +} + +/* +================ +hhSpring::LinkSpring + HUMANHEAD +================ +*/ +void hhSpring::LinkSpring(idEntity *ent1, idEntity *ent2 ) { + LinkSpringAll(ent1, id1, p1, ent2, id2, p2); +} + +/* +================ +hhSpring::LinkSpringIDs + HUMANHEAD +================ +*/ +void hhSpring::LinkSpringIDs(idEntity *ent1, int bodyID1, idEntity *ent2, int bodyID2) { + id1 = bodyID1; + id2 = bodyID2; + LinkSpringAll(ent1, id1, p1, ent2, id2, p2); +} + +/* +================ +hhSpring::LinkSpringAll + HUMANHEAD +================ +*/ +void hhSpring::LinkSpringAll(idEntity *ent1, int bodyID1, idVec3 &offset1, + idEntity *ent2, int bodyID2, idVec3 &offset2) { + + physics1 = ent1; + physics2 = ent2; + spring.SetPosition( physics1.GetEntity(), bodyID1, offset1, physics2.GetEntity(), bodyID2, offset2 ); + BecomeActive(TH_THINK); +} + +/* +================ +hhSpring::UnLinkSpring + HUMANHEAD +================ +*/ +void hhSpring::UnLinkSpring() { + BecomeInactive(TH_THINK); +} + +/* +================ +hhSpring::SpringSettings + HUMANHEAD +================ +*/ +void hhSpring::SpringSettings(float kStretch, float kCompress, float damping, float restLength) { + spring.InitSpring(kStretch, kCompress, damping, restLength); +} + +/* +================ +hhSpring::Event_LinkSpring +================ +*/ +void hhSpring::Event_LinkSpring( void ) { + idEntity *ent1, *ent2; + + ent1 = gameLocal.FindEntity( name1 ); + if ( !ent1 ) { + ent1 = gameLocal.entities[ENTITYNUM_WORLD]; + } + ent2 = gameLocal.FindEntity( name2 ); + if ( !ent2 ) { + ent2 = gameLocal.entities[ENTITYNUM_WORLD]; + } + LinkSpring(ent1, ent2); +} + +/* +================ +hhSpring::Event_UnlinkSpring +================ +*/ +void hhSpring::Event_UnlinkSpring( void ) { + UnLinkSpring(); +} diff --git a/src/Prey/game_spring.h b/src/Prey/game_spring.h new file mode 100644 index 0000000..3d34e85 --- /dev/null +++ b/src/Prey/game_spring.h @@ -0,0 +1,45 @@ + +#ifndef __GAME_SPRING_H__ +#define __GAME_SPRING_H__ + +/*********************************************************************** + +hhSpring + +***********************************************************************/ +extern const idEventDef EV_HHLinkSpring; +extern const idEventDef EV_HHUnlinkSpring; + +class hhSpring : public idEntity { + CLASS_PROTOTYPE( hhSpring ); + +public: + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think( void ); + + // HUMANHEAD pdm + void LinkSpring(idEntity *ent1, idEntity *ent2 ); + void LinkSpringIDs(idEntity *ent1, int bodyID1, idEntity *ent2, int bodyID2); + void LinkSpringAll(idEntity *ent1, int bodyID1, idVec3 &offset1, idEntity *ent2, int bodyID2, idVec3 &offset2); + void UnLinkSpring(); + void SpringSettings(float kStretch, float kCompress, float damping, float restLength); + // HUMANHEAD END + +protected: + void Event_LinkSpring( void ); + void Event_UnlinkSpring( void ); + +protected: + idStr name1, name2; + idEntityPtr physics1, physics2; + int id1, id2; + idVec3 p1, p2; + idForce_Spring spring; +}; + + +#endif /* !__GAME_SPRING_H__ */ diff --git a/src/Prey/game_sunCorona.cpp b/src/Prey/game_sunCorona.cpp new file mode 100644 index 0000000..ae64993 --- /dev/null +++ b/src/Prey/game_sunCorona.cpp @@ -0,0 +1,123 @@ +//***************************************************************************** +//** +//** GAME_SUNCORONA.CPP +//** +//** Game code for Sun Coronas +//** +//** TODO: +//** - Implement corona scale (or just leave this for the shader?) +//** - Check if 4096 is good enough for distance -- issues in very large rooms? +//** - Verify if the noFragment check is valid for checking for skyboxes +//** - Make it function corrected with the rifle zoom view +//***************************************************************************** + +// HEADER FILES --------------------------------------------------------------- + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS --------------------------------------------------------------------- + +// TYPES ---------------------------------------------------------------------- + +// CLASS DECLARATIONS --------------------------------------------------------- + +CLASS_DECLARATION( idEntity, hhSunCorona ) +END_CLASS + +// STATE DECLARATIONS --------------------------------------------------------- + +// EXTERNAL FUNCTION PROTOTYPES ----------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES ------------------------------------------------ + +// EXTERNAL DATA DECLARATIONS ------------------------------------------------- + +// PUBLIC DATA DEFINITIONS ---------------------------------------------------- + +// PRIVATE DATA DEFINITIONS --------------------------------------------------- + +// CODE ----------------------------------------------------------------------- + +//============================================================================= +// +// hhSunCorona::Spawn +// +//============================================================================= + +void hhSunCorona::Spawn(void) { + corona = declManager->FindMaterial( spawnArgs.GetString( "mtr_corona" ) ); + scale = spawnArgs.GetFloat( "scale", "1" ); + sunVector = spawnArgs.GetVector( "sunVector", "0 0 -1" ); + sunVector.Normalize(); + sunDistance = spawnArgs.GetFloat( "sunDistance", "4096" ); + + GetPhysics()->SetContents(0); + Hide(); + BecomeInactive(TH_THINK); + + gameLocal.SetSunCorona( this ); +} + +void hhSunCorona::Save(idSaveGame *savefile) const { + savefile->WriteMaterial( corona ); + savefile->WriteFloat( scale ); + savefile->WriteVec3( sunVector ); + savefile->WriteFloat( sunDistance ); +} + +void hhSunCorona::Restore( idRestoreGame *savefile ) { + savefile->ReadMaterial( corona ); + savefile->ReadFloat( scale ); + savefile->ReadVec3( sunVector ); + savefile->ReadFloat( sunDistance ); +} + +//============================================================================= +// +// hhSunCorona::~hhSunCorona +// +//============================================================================= + +hhSunCorona::~hhSunCorona() { + corona = NULL; +} + +//============================================================================= +// +// hhhSunCorona::Draw +// +//============================================================================= + +void hhSunCorona::Draw( hhPlayer *player ) { +/* Commented out for E3 demo. Need a better method for visibility, instead of a trace. -cjr + idVec3 v[3]; + float dot[3]; + player->viewAngles.ToVectors( &v[0], &v[1], &v[2] ); + + for( int i = 0; i < 3; i++ ) { + dot[i] = v[i] * sunVector; + } + + if ( dot[0] < 0 ) { + trace_t trace; + idVec3 origin; + idMat3 axis; + player->GetViewPos( origin, axis ); + idVec3 end = origin - sunVector * sunDistance; + gameLocal.clip.TracePoint( trace, origin, end, MASK_SOLID, player ); + + if ( trace.c.material->NoFragment() ) { // Trace succeeded, or it hit a skybox + // Draw a corona on the screen + renderSystem->SetColor4( -dot[0], -dot[0], -dot[0], -dot[0] ); + float hFov = idMath::Sin( g_fov.GetFloat() * 0.5 ); + float vFov = idMath::Sin( g_fov.GetFloat() * 0.5 * 0.75 ); + float sx = ( ( dot[1] - hFov ) / ( -2 * hFov ) ) * 640 - 640; + float sy = ( dot[2] + vFov ) / ( 2 * vFov ) * 480 - 480; + renderSystem->DrawStretchPic( sx, sy, 1280, 960, 0, 0, 1, 1, corona ); + } + } +*/ +} diff --git a/src/Prey/game_sunCorona.h b/src/Prey/game_sunCorona.h new file mode 100644 index 0000000..4e99cd4 --- /dev/null +++ b/src/Prey/game_sunCorona.h @@ -0,0 +1,24 @@ + +#ifndef __GAME_SUNCORONA_H__ +#define __GAME_SUNCORONA_H__ + +//----------------------------------------------------------------------------- + +class hhSunCorona : public idEntity { +public: + CLASS_PROTOTYPE(hhSunCorona); + + ~hhSunCorona(); //fixme: does this need to be virtual ? + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void Draw( hhPlayer *player ); + +protected: + const idMaterial *corona; + float scale; + idVec3 sunVector; + float sunDistance; +}; + +#endif /* __GAME_SUNCORONA_H__ */ diff --git a/src/Prey/game_talon.cpp b/src/Prey/game_talon.cpp new file mode 100644 index 0000000..8a88de6 --- /dev/null +++ b/src/Prey/game_talon.cpp @@ -0,0 +1,1777 @@ +//***************************************************************************** +//** +//** GAME_TALON.CPP +//** +//** Game code for Tommy's sidekick, Talon the Hawk +//** +//***************************************************************************** + +// HEADER FILES --------------------------------------------------------------- + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS --------------------------------------------------------------------- + +#define PERCH_ROTATION 3.5 +#define ROTATION_SPEED 3.5 +#define ROTATION_SPEED_FAST 5 +#define TALON_BLEND 100 + +// TYPES ---------------------------------------------------------------------- + +// CLASS DECLARATIONS --------------------------------------------------------- + +const idEventDef EV_PerchTicker("", NULL); +const idEventDef EV_PerchChatter("perchChatter", NULL); +const idEventDef EV_PerchSquawk("", NULL); + +const idEventDef EV_CheckForTarget( "", NULL ); +const idEventDef EV_CheckForEnemy( "", NULL ); + +const idEventDef EV_TalonAction("talonaction", "ed"); + +// Anim Events +const idEventDef EV_LandAnim("", NULL); +const idEventDef EV_PreLandAnim("", NULL); +const idEventDef EV_IdleAnim("", NULL); +const idEventDef EV_TommyIdleAnim("", NULL); +const idEventDef EV_FlyAnim("", NULL); +const idEventDef EV_GlideAnim("", NULL); +const idEventDef EV_TakeOffAnim("", NULL); +const idEventDef EV_TakeOffAnimB("", NULL); + +CLASS_DECLARATION( hhMonsterAI, hhTalon ) + EVENT(EV_PerchChatter, hhTalon::Event_PerchChatter) + EVENT(EV_PerchSquawk, hhTalon::Event_PerchSquawk) + EVENT(EV_CheckForTarget, hhTalon::Event_CheckForTarget) + EVENT(EV_CheckForEnemy, hhTalon::Event_CheckForEnemy) + + // Anims + EVENT(EV_LandAnim, hhTalon::Event_LandAnim) + EVENT(EV_PreLandAnim, hhTalon::Event_PreLandAnim) + EVENT(EV_IdleAnim, hhTalon::Event_IdleAnim) + EVENT(EV_TommyIdleAnim, hhTalon::Event_TommyIdleAnim) + EVENT(EV_FlyAnim, hhTalon::Event_FlyAnim) + EVENT(EV_GlideAnim, hhTalon::Event_GlideAnim) + EVENT(EV_TakeOffAnim, hhTalon::Event_TakeOffAnim) + EVENT(EV_TakeOffAnimB, hhTalon::Event_TakeOffAnimB) +END_CLASS + +const idEventDef EV_CallTalon("callTalon", "fff" ); +const idEventDef EV_ReleaseTalon("releaseTalon", NULL ); +const idEventDef EV_SetPerchState("setPerchState", "f" ); + +CLASS_DECLARATION( idEntity, hhTalonTarget ) + EVENT( EV_CallTalon, hhTalonTarget::Event_CallTalon ) + EVENT( EV_ReleaseTalon, hhTalonTarget::Event_ReleaseTalon ) + EVENT( EV_SetPerchState, hhTalonTarget::Event_SetPerchState ) +END_CLASS + +// STATE DECLARATIONS --------------------------------------------------------- + +// EXTERNAL FUNCTION PROTOTYPES ----------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES ------------------------------------------------ + +// EXTERNAL DATA DECLARATIONS ------------------------------------------------- + +// PUBLIC DATA DEFINITIONS ---------------------------------------------------- + +// PRIVATE DATA DEFINITIONS --------------------------------------------------- + +// CODE ----------------------------------------------------------------------- + +//============================================================================= +// +// hhTalon::Spawn +// +//============================================================================= + +void hhTalon::Spawn(void) { + flyAnim = GetAnimator()->GetAnim("fly"); + glideAnim = GetAnimator()->GetAnim("glide"); + prelandAnim = GetAnimator()->GetAnim("preland"); + landAnim = GetAnimator()->GetAnim("land"); + idleAnim = GetAnimator()->GetAnim("idle"); + tommyIdleAnim = GetAnimator()->GetAnim("tommy_idle"); + squawkAnim = GetAnimator()->GetAnim("squawk"); + stepAnim = GetAnimator()->GetAnim("step"); + takeOffAnim = GetAnimator()->GetAnim("takeoffA"); + takeOffAnimB = GetAnimator()->GetAnim("takeoffB"); + attackAnim = GetAnimator()->GetAnim("attack1"); + preAttackAnim = GetAnimator()->GetAnim("preattack"); + + fl.neverDormant = true; + fl.takedamage = false; + fl.notarget = true; + + allowHiddenMovement = true; // Allow Talon to move while hidden + + health = 1; // Talon need some health for enemies to consider it alive + + //AOB + GetAnimator()->RemoveOriginOffset( true ); + + // Register the wings skins + openWingsSkin = declManager->FindSkin( spawnArgs.GetString("skin_openWings") ); + closedWingsSkin = declManager->FindSkin( spawnArgs.GetString("skin_closeWings") ); + + Event_SetMoveType(MOVETYPE_FLY); + + GetPhysics()->SetContents( 0 ); // No collisions + GetPhysics()->SetClipMask( 0 ); // No collisions + Hide(); + BecomeInactive(TH_THINK); +} + +void hhTalon::Save(idSaveGame *savefile) const { + owner.Save(savefile); + savefile->WriteVec3(velocity); + savefile->WriteVec3(acceleration); + + savefile->WriteObject(talonTarget); + savefile->WriteVec3(talonTargetLoc); + savefile->WriteMat3(talonTargetAxis); + savefile->WriteVec3(lastCheckOrigin); + + savefile->WriteFloat(checkTraceTime); + savefile->WriteFloat(checkFlyTime); + savefile->WriteFloat(flyStraightTime); + + savefile->WriteBool(bLanding); + savefile->WriteBool(bReturnToTommy); + savefile->WriteBool( bForcedTarget ); + savefile->WriteBool( bClawingAtEnemy ); + + savefile->WriteFloat( velocityFactor ); + savefile->WriteFloat( rotationFactor ); + savefile->WriteFloat( perchRotationFactor ); + + savefile->WriteInt(flyAnim); + savefile->WriteInt(glideAnim); + savefile->WriteInt(prelandAnim); + savefile->WriteInt(landAnim); + savefile->WriteInt(idleAnim); + savefile->WriteInt(tommyIdleAnim); + savefile->WriteInt(squawkAnim); + savefile->WriteInt(stepAnim); + savefile->WriteInt(takeOffAnim); + savefile->WriteInt(takeOffAnimB); + savefile->WriteInt(attackAnim); + savefile->WriteInt(preAttackAnim); + + savefile->WriteSkin(openWingsSkin); + savefile->WriteSkin(closedWingsSkin); + + enemy.Save( savefile ); + trailFx.Save( savefile ); + + savefile->WriteInt( state ); +} + +void hhTalon::Restore( idRestoreGame *savefile ) { + owner.Restore(savefile); + savefile->ReadVec3(velocity); + savefile->ReadVec3(acceleration); + + savefile->ReadObject( reinterpret_cast(talonTarget) ); + savefile->ReadVec3(talonTargetLoc); + savefile->ReadMat3(talonTargetAxis); + savefile->ReadVec3(lastCheckOrigin); + + savefile->ReadFloat(checkTraceTime); + savefile->ReadFloat(checkFlyTime); + savefile->ReadFloat(flyStraightTime); + + savefile->ReadBool(bLanding); + savefile->ReadBool(bReturnToTommy); + savefile->ReadBool( bForcedTarget ); + savefile->ReadBool( bClawingAtEnemy ); + + savefile->ReadFloat( velocityFactor ); + savefile->ReadFloat( rotationFactor ); + savefile->ReadFloat( perchRotationFactor ); + + savefile->ReadInt(flyAnim); + savefile->ReadInt(glideAnim); + savefile->ReadInt(prelandAnim); + savefile->ReadInt(landAnim); + savefile->ReadInt(idleAnim); + savefile->ReadInt(tommyIdleAnim); + savefile->ReadInt(squawkAnim); + savefile->ReadInt(stepAnim); + savefile->ReadInt(takeOffAnim); + savefile->ReadInt(takeOffAnimB); + savefile->ReadInt(attackAnim); + savefile->ReadInt(preAttackAnim); + + savefile->ReadSkin(openWingsSkin); + savefile->ReadSkin(closedWingsSkin); + + enemy.Restore( savefile ); + trailFx.Restore( savefile ); + + savefile->ReadInt( reinterpret_cast (state) ); +} + +//============================================================================= +// +// hhTalon::hhTalon +// +//============================================================================= + +hhTalon::hhTalon() { + owner = NULL; + talonTarget = NULL; + enemy.Clear(); + trailFx.Clear(); +} + +//============================================================================= +// +// hhTalon::~hhTalon +// +//============================================================================= + +hhTalon::~hhTalon() { + if( owner.IsValid() ) { + owner->talon = NULL; + } + + if ( trailFx.IsValid() ) { + trailFx->Nozzle( false ); + SAFE_REMOVE( trailFx ); + } +} + +//============================================================================= +// +// hhTalon::SummonTalon +// +//============================================================================= + +void hhTalon::SummonTalon(void) { + hhFxInfo fxInfo; + + state = StateNone; + + BecomeActive(TH_THINK); + Show(); + + checkTraceTime = spawnArgs.GetFloat( "traceCheckPulse", "0.1" ); + lastCheckOrigin = GetOrigin(); + + FindTalonTarget(NULL, NULL, true); + + CalculateTalonTargetLocation(); // Must recalculate the target location, as the target may have moved + SetOrigin(talonTargetLoc); + viewAxis = talonTargetAxis; + + bLanding = false; + + SetSkin( closedWingsSkin ); // The only time this is set in code, otherwise it's set by the land anim + + fl.neverDormant = true; + + SetForcedTarget( false ); + + velocityFactor = 1.0f; + rotationFactor = 1.0f; + perchRotationFactor = 1.0f; + + SetShaderParm( SHADERPARM_DIVERSITY, 0 ); + + EnterTommyState(); + TommyTicker(); // Force Talon to tick once to adjust his orientation +} + +//============================================================================= +// +// hhTalon::SetOwner +// +//============================================================================= + +void hhTalon::SetOwner( hhPlayer *newOwner ) { + owner = newOwner; + Event_SetOwner( newOwner ); +} + +void hhTalon::OwnerEnteredVehicle( void ) { + if ( talonTarget ) { + talonTarget->Left( this ); + } + + SummonTalon(); + EnterVehicleState(); +} + +void hhTalon::OwnerExitedVehicle( void ) { + ExitVehicleState(); + SummonTalon(); +} + +//============================================================================= +// +// hhTalon::FindTalonTarget +// +//============================================================================= + +void hhTalon::FindTalonTarget( idEntity *skipEntity, hhTalonTarget *forceTarget, bool bForcePlayer ) { + int i; + int count; + float dist; + float bestDist; + hhTalonTarget *ent; + idVec3 dir; + + talonTarget = NULL; + bestDist = spawnArgs.GetFloat( "perchDistance", "250" ); + + if ( owner->InVehicle() ) { + bestDist *= 10; + } + + if( bReturnToTommy) { + bForcePlayer = true; + } + + if ( forceTarget ) { + talonTarget = forceTarget; + } + + // Iterate through valid perch spots and find the best choice + if( !bForcePlayer && !forceTarget ) { + count = gameLocal.talonTargets.Num(); + for(i = 0; i < count; i++) { + ent = (hhTalonTarget *)gameLocal.talonTargets[i]; + + if ( !ent->bValidForTalon ) { // Not a valid target + continue; + } + + // ignore if there is no way we can see this perch spot + if( ent == skipEntity || !gameLocal.InPlayerPVS( ent ) || ent->priority == -1) { + continue; + } + + if ( owner.IsValid() && owner->IsSpiritOrDeathwalking() && ent->bNotInSpiritWalk ) { // If the owner is spiritwalking, then don't try to fly to this target (useful for lifeforce pickups) + continue; + } + + // Calculate the distance from the player to the perch spot + dist = ( owner->GetEyePosition() - ent->GetPhysics()->GetOrigin() ).Length(); + + // Adjust the distance based upon the priority (ranges from 0 - 2, -1 to completely skip). + dist *= ent->priority; + + if(dist > bestDist) { // Farther than the nearest perch spot + continue; + } + + talonTarget = ent; + bestDist = dist; + } + } + + if( !talonTarget && owner.IsValid() ) { // No perch spot, so use a spot close to the player + owner->GetJointWorldTransform( "FX_bird", talonTargetLoc, talonTargetAxis ); + talonTargetAxis = owner->viewAxis; + } else { + CalculateTalonTargetLocation(); + } +} + +//============================================================================= +// +// hhTalon::CalculateTalonTargetLocation +// +//============================================================================= + +void hhTalon::CalculateTalonTargetLocation() { + trace_t tr; + + if ( !talonTarget ) { + return; + } + + // Trace down a bit to get the exact perch location + if( gameLocal.clip.TracePoint( tr, talonTarget->GetPhysics()->GetOrigin() + idVec3(0, 0, 4), talonTarget->GetPhysics()->GetOrigin() - idVec3(0, 0, 16), MASK_SOLID, this )) { + talonTargetLoc = tr.endpos - idVec3( 0, 0, 1 ); // End position, minus a tiny bit so Talon perches on railings + } else { // No collision, just use the current floating spot + talonTargetLoc = talonTarget->GetPhysics()->GetOrigin(); + } + + talonTargetAxis = talonTarget->GetPhysics()->GetAxis(); +} + +//============================================================================= +// +// CheckReachedTarget +// +//============================================================================= + +bool hhTalon::CheckReachedTarget( float distance ) { + idVec3 vec; + + if(distance < spawnArgs.GetFloat( "distanceSlow", "80" ) ) { + if(talonTarget) { + velocity = (talonTargetLoc - GetOrigin()) * 0.05f; // Move more slowly when about to perch + + if( !GetAnimator()->IsAnimPlaying( GetAnimator()->GetAnim( prelandAnim ) ) ) { + Event_PreLandAnim(); + StartSound( "snd_preland", SND_CHANNEL_BODY ); + } + bLanding = true; + } + + if(talonTarget) { + if(distance < spawnArgs.GetFloat( "distancePerchEpsilon", "3" ) ) { // Perch on this spot + Event_LandAnim(); + StartSound( "snd_land", SND_CHANNEL_BODY ); + + talonTarget->Reached(this); + EnterPerchState(); + + return true; // No need to continue the fly ticker + } + } else if(distance < spawnArgs.GetFloat( "distanceTommyEpsilon", "15.0" ) ) { // No perch spot, so perch on Tommy around this location + Event_LandAnim(); + EnterTommyState(); + return true; // No need to continue the fly ticker + } + } else if(distance >= spawnArgs.GetFloat( "distanceTurbo", "2500" ) ) { // Distance is so far, we should kick Talon into turbo mode + velocity *= 10; + } + + return false; +} + +//============================================================================= +// +// hhTalon::CheckCollisions +// +//============================================================================= + +void hhTalon::CheckCollisions(float deltaTime) { + idVec3 oldOrigin; + trace_t tr; + + checkTraceTime -= deltaTime; + + if(checkTraceTime > 0) { + return; + } + + checkTraceTime = spawnArgs.GetFloat( "traceCheckPulse", "0.1" ); + + idVec3 nextLocation = GetOrigin() + this->velocity * 0.5f; + + // Trace to determine if the hawk is just about to fly into a wall + if ( gameLocal.clip.TracePoint( tr, GetOrigin(), nextLocation, MASK_SOLID, owner.GetEntity() ) ) { // Struck something solid + // Glow + SetShaderParm( SHADERPARM_TIMEOFFSET, MS2SEC( gameLocal.time ) ); + } else if ( gameLocal.clip.TracePoint( tr, GetOrigin(), lastCheckOrigin, MASK_SOLID, owner.GetEntity() ) ) { // Exited something solid + // Glow + SetShaderParm( SHADERPARM_TIMEOFFSET, MS2SEC( gameLocal.time ) ); + } + + lastCheckOrigin = GetOrigin(); +} + +//============================================================================= +// +// hhTalon::Think +// +//============================================================================= + +void hhTalon::Think(void) { + + if ( owner->InGravityZone() ) { // don't perch in gravity zones, unless gravity is "normal" + idVec3 gravity = owner->GetGravity(); + gravity.Normalize(); + + float dot = gravity * idVec3( 0.0f, 0.0f, -1.0f ); + if ( dot < 1.0f ) { + if ( talonTarget ) { + talonTarget->Left( this ); + } + + ReturnToTommy(); + } + } + + // Shadow suppression based upon the player + if ( g_showPlayerShadow.GetBool() ) { + renderEntity.suppressShadowInViewID = 0; + } + else { + renderEntity.suppressShadowInViewID = owner->entityNumber + 1; + } + + hhMonsterAI::Think(); +} + +//============================================================================= +// +// hhTalon::FlyMove +// +//============================================================================= + +void hhTalon::FlyMove( void ) { + // Run the specific task Ticker + switch( state ) { + case StateTommy: + TommyTicker(); + break; + case StateFly: + FlyTicker(); + break; + case StateVehicle: + VehicleTicker(); + break; + case StatePerch: + PerchTicker(); + break; + case StateAttack: + AttackTicker(); + break; + } + + // run the physics for this frame + physicsObj.UseFlyMove( true ); + physicsObj.UseVelocityMove( false ); + physicsObj.SetDelta( vec3_zero ); + physicsObj.ForceDeltaMove( disableGravity ); + RunPhysics(); +} + +//============================================================================= +// +// hhTalon::AdjustFlyingAngles +// +// No need to adjust flying angles on Talon +//============================================================================= + +void hhTalon::AdjustFlyingAngles() { + return; +} + +//============================================================================= +// +// hhTalon::GetPhysicsToVisualTransform +// +//============================================================================= + +bool hhTalon::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + origin = modelOffset; + if ( GetBindMaster() && bindJoint != INVALID_JOINT ) { + idMat3 masterAxis; + idVec3 masterOrigin; + GetMasterPosition( masterOrigin, masterAxis ); + + origin = masterOrigin + physicsObj.GetLocalOrigin() * masterAxis; + axis = GetBindMaster()->GetAxis(); + } else { + axis = viewAxis; + } + return true; +} + +//============================================================================= +// +// hhTalon::CrashLand +// +// No need to check crash landing on Talon +//============================================================================= + +void hhTalon::CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ) { + return; +} + +//============================================================================= +// +// hhTalon::Damage +// +// Talon cannot be damaged +//============================================================================= + +void hhTalon::Damage( idEntity *inflictor, idEntity *attack, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + return; +} + +//============================================================================= +// +// hhTalon::Killed +// +// Talon cannot be killed +//============================================================================= + +void hhTalon::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + return; +} + +//============================================================================= +// +// hhTalon::Portalled +// +//============================================================================= + +void hhTalon::Portalled( idEntity *portal ) { + if ( state == StateTommy ) { // No need to portal if Talon is already on Tommy's shoulder + return; + } + + CancelEvents( &EV_GlideAnim ); // Could possibly have a glide anim queue + CancelEvents( &EV_TakeOffAnimB ); + + // If Talon is currently on a gui, inform the gui he is leaving + if ( talonTarget && ( state == StatePerch || state == StateVehicle ) ) { // CJR PCF 050306: Only call Left() if Talon is perching on a console + talonTarget->Left( this ); + } + + StopAttackFX(); + + // Force Talon to Tommy's shoulder after he portals + talonTarget = NULL; + bLanding = false; + SetSkin( openWingsSkin ); // The only time this is set in code, otherwise it's set by the land anim + Event_LandAnim(); + SetForcedTarget( false ); + EnterTommyState(); +} + +//============================================================================= +// +// hhTalon::EnterFlyState +// +//============================================================================= + +void hhTalon::EnterFlyState(void) { + EndState(); + state = StateFly; + checkFlyTime = spawnArgs.GetFloat( "checkFlyTime", "4.0" ); + flyStraightTime = spawnArgs.GetFloat( "flyStraightTime", "0.5" ); + bLanding = false; + + SetShaderParm( SHADERPARM_DIVERSITY, 0 ); // Talon's model should phase out if the player is too close + + +} + +//============================================================================= +// +// hhTalon::FlyTicker +// +//============================================================================= + +void hhTalon::FlyTicker(void) { + idAngles ang; + float distance; + float deltaTime = MS2SEC(gameLocal.msec); + idVec3 oldVel; + float distanceXY; + float deltaZ; + float rotSpeed; + + // If the bird is currently in fly mode, but hasn't finished the takeoff anim, then don't let the bird fly around + if( GetAnimator()->IsAnimPlaying( GetAnimator()->GetAnim( takeOffAnim ) ) ) { + return; + } + + acceleration = talonTargetLoc - GetOrigin(); + + idVec2 vecXY = acceleration.ToVec2(); + distance = acceleration.Normalize(); + + distanceXY = vecXY.Length(); + deltaZ = talonTargetLoc.z - GetOrigin().z; + + float dp = acceleration * GetAxis()[0]; + float side = acceleration * GetAxis()[1]; + ang = GetAxis().ToAngles(); + + // Talon can either rotate towards the target, or fly straight. + if( flyStraightTime > 0 ) { // Continue to fly straight + flyStraightTime -= deltaTime; + + if ( flyStraightTime <= 0 && !bLanding ) { // No longer flying straight + Event_FlyAnim(); + } + + rotSpeed = 0; + } else if( bLanding ) { // About to land, so turn slightly faster than normal + rotSpeed = ROTATION_SPEED_FAST * rotationFactor; + } else { + rotSpeed = ROTATION_SPEED * rotationFactor; + } + + + rotSpeed *= (60.0f * USERCMD_ONE_OVER_HZ); + + // Determine whether Talon should rotate left or right to reach the target + if(dp > 0) { + if(dp >= 0.98f || ( side <= 0.1f && side >= 0.1f )) { // CJR PCF 04/28/06: don't turn if directly facing the target + rotSpeed = 0; + } else if (side < -0.1f) { + rotSpeed *= -1.0f; + } + } else { + if(side < 0) { + rotSpeed *= -1.0f; + } + } + + // Apply rotation + deltaViewAngles.yaw = rotSpeed; + oldVel = velocity; + + float defaultVelocityXY = spawnArgs.GetFloat( "velocityXY", "7.0" ); + float defaultVelocityZ = spawnArgs.GetFloat( "velocityZ", "4" ); + + velocity = viewAxis[0] * defaultVelocityXY * velocityFactor; // xy-velocity + + // Z-change based upon distance + if(abs(deltaZ) > 0 && distanceXY > 0) { + velocity.z = (deltaZ * defaultVelocityXY) / distanceXY; + + // Clamp z velocity + if(velocity.z > defaultVelocityZ) { + velocity.z = defaultVelocityZ; + } + } + + if ( CheckReachedTarget( distance ) ) { + return; + } + + // Apply velocity + SetOrigin( GetOrigin() + velocity * (60.0f * USERCMD_ONE_OVER_HZ) ); + + // Check for colliding with world surfaces (only if not currently trying to land) + if( !bLanding ) { + CheckCollisions(deltaTime); + } + + // Timeout for flying to perch spot... if too much time has passed, then fly straight for a bit + checkFlyTime -= deltaTime; + if( checkFlyTime <= 0 && !bLanding ) { + flyStraightTime = spawnArgs.GetFloat( "flyStraightTime", "0.5" ); + checkFlyTime = spawnArgs.GetFloat( "checkFlyTime", "4.0" ); + + PostEventMS( &EV_GlideAnim, 0 ); + } + + // If no talonTarget (probably flying towards Tommy), then constantly look for a new target + if( !bForcedTarget && !GetAnimator()->IsAnimPlaying( GetAnimator()->GetAnim( prelandAnim ) ) ) { + FindTalonTarget( NULL, NULL, false ); + } +} + +//============================================================================= +// +// hhTalon::EnterTommyState +// +//============================================================================= + +void hhTalon::EnterTommyState(void) { + idVec3 bindOrigin; + idMat3 bindAxis; + + bReturnToTommy = false; + + EndState(); + state = StateTommy; + + Hide(); + + UpdateVisuals(); + + // Bind the bird to Tommy's shoulder + owner->GetJointWorldTransform( "fx_bird", bindOrigin, bindAxis ); + SetOrigin( bindOrigin ); + SetAxis( owner->viewAxis ); + BindToJoint( owner.GetEntity(), "fx_bird", true ); + + // Start checking for enemies + PostEventSec( &EV_CheckForEnemy, spawnArgs.GetFloat("checkEnemyTime", "4.0" ) ); + + // Start checking for nearby targets + PostEventSec( &EV_CheckForTarget, spawnArgs.GetFloat( "checkTargetTime", "1.0" ) ); +} + +//============================================================================= +// +// hhTalon::EndState +// +//============================================================================= + +void hhTalon::EndState(void) { + Unbind(); + + int oldState = state; + state = StateNone; + + if ( fl.hidden ) { // If the bird is hidden, show it when ending the state + Show(); + } + + // Zero out Talon's velocity when initially taking off, so it doesn't inherit any from the bind master + velocity = vec3_zero; + GetPhysics()->SetLinearVelocity( vec3_zero ); + + // Cancel any checks for enemies + CancelEvents( &EV_CheckForEnemy ); + CancelEvents( &EV_CheckForTarget ); + + StopAttackFX(); + + if( oldState == StateTommy ) { + GetPhysics()->SetAxis( mat3_identity ); + + // Show the hawk in the player's view after it takes off + Show(); + } else if( oldState == StatePerch ) { + CancelEvents( &EV_PerchSquawk ); + } else if ( oldState == StateAttack ) { + // Clear the enemy + if ( enemy.IsValid() ) { + (static_cast(enemy.GetEntity()))->Distracted( GetOwner() ); // Set the enemy's enemy back to Tommy + enemy.Clear(); + } + } +} + +//============================================================================= +// +// hhTalon::TommyTicker +// +//============================================================================= + +void hhTalon::TommyTicker(void) { + idVec3 viewOrigin; + idMat3 viewAxis; + idMat3 ownerAxis; + float deltaTime = MS2SEC(gameLocal.msec); + + bLanding = false; + + // Turn towards direction of Tommy + idVec3 toFace = owner->GetAxis()[0]; + toFace.z = 0; + toFace.Normalize(); + float dp = toFace * GetAxis()[0]; + float side = toFace * GetAxis()[1]; + + if(side > 0.05 || dp < 0 ) { + deltaViewAngles.yaw = PERCH_ROTATION * perchRotationFactor; + UpdateVisuals(); + } else if (side < -0.05) { + deltaViewAngles.yaw = -PERCH_ROTATION * perchRotationFactor; + UpdateVisuals(); + } +} + +//============================================================================= +// +// hhTalon::FindEnemy +// +//============================================================================= + +bool hhTalon::FindEnemy( void ) { + idEntity *ent; + hhMonsterAI *actor; + hhMonsterAI *bestEnemy; + float bestDist; + float maxEnemyDist; + float dist; + idVec3 delta; + int enemyCount; + + // Check if Talon already has a valid enemy + if ( enemy.IsValid() && enemy->health > 0 ) { + return true; + } + + bestDist = 0; + maxEnemyDist = 1024.0f * 1024.0f; // sqr + bestEnemy = NULL; + enemyCount = 0; + for ( ent = gameLocal.activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { + if ( ent->fl.hidden || ent->fl.isDormant || !ent->IsType( hhMonsterAI::Type ) ) { + continue; + } + + actor = static_cast( ent ); + + // Only attack living enemies, and only attack enemies that are targetting Tommy or Talon + if ( (actor->health <= 0) || ( actor->GetEnemy() != GetOwner() && actor->GetEnemy() != this ) ) { + continue; + } + + if ( !gameLocal.InPlayerPVS( actor ) ) { + continue; + } + + // Check if Talon should avoid this enemy -- these enemies are also not included in the enemy count + if ( !ent->spawnArgs.GetBool( "bTalonAttack", "0" ) ) { + continue; + } + + delta = GetOwner()->GetOrigin() - actor->GetPhysics()->GetOrigin(); // Find the farthest enemy (in the PVS) to Tommy + dist = delta.LengthSqr(); + if ( dist > bestDist && dist < maxEnemyDist && GetOwner()->CanSee( actor, false ) ) { + bestDist = dist; + bestEnemy = actor; + } + + enemyCount++; + } + if ( bestEnemy && enemyCount >= 2 ) { // Only attack if more than one enemy is attacking the player + enemy = bestEnemy; + return true; + } + + enemy.Clear(); + return false; +} + +//============================================================================= +// +// hhTalon::EnterPerchState +// +//============================================================================= + +void hhTalon::EnterPerchState(void) { + EndState(); + state = StatePerch; + + velocity = vec3_zero; + + if ( talonTarget ) { + CalculateTalonTargetLocation(); // Must recalculate the target location, as the target may have moved + SetOrigin( talonTargetLoc ); + + if ( talonTarget->IsBound() ) { // Only bind talon to this target, if it's bound to something + Bind( talonTarget, true ); + } + + GetPhysics()->SetLinearVelocity( vec3_zero ); + } + + if ( talonTarget && talonTarget->bShouldSquawk ) { + float frequency = talonTarget->spawnArgs.GetFloat( "squawkFrequency", "5.0" ); + PostEventSec( &EV_PerchSquawk, frequency ); + } + + SetShaderParm( SHADERPARM_DIVERSITY, 1 ); // Talon's model shouldn't phase out if the player is too close + + // Start checking for nearby enemies + PostEventSec( &EV_CheckForEnemy, spawnArgs.GetFloat("checkEnemyTime", "4.0" ) ); + + // Start checking for nearby targets + PostEventSec( &EV_CheckForTarget, spawnArgs.GetFloat( "checkTargetTime", "1.0" ) ); +} + +//============================================================================= +// +// hhTalon::PerchTicker +// +//============================================================================= + +void hhTalon::PerchTicker(void) { + float deltaTime = MS2SEC(gameLocal.msec); + + bLanding = false; + + // Turn towards direction of perch spot + idVec3 toFace = talonTarget->GetPhysics()->GetAxis()[0]; + toFace.z = 0; + toFace.Normalize(); + float dp = toFace * GetAxis()[0]; + float side = toFace * GetAxis()[1]; + + if ( dp < 0 ) { + if ( side >= 0 ) { + deltaViewAngles.yaw = PERCH_ROTATION * perchRotationFactor; + } else { + deltaViewAngles.yaw = -PERCH_ROTATION * perchRotationFactor; + } + } else if (side > 0.05 ) { + deltaViewAngles.yaw = PERCH_ROTATION * perchRotationFactor; + } else if (side < -0.05) { + deltaViewAngles.yaw = -PERCH_ROTATION * perchRotationFactor; + } + + UpdateVisuals(); +} + +//============================================================================= +// +// hhTalon::Event_PerchChatter +// +// Small chirping noise which is part of Talon's idle animation +//============================================================================= + +void hhTalon::Event_PerchChatter(void) { + if ( state != StateTommy ) { // Don't chatter when on Tommy's shoulder + StartSound( "snd_chatter", SND_CHANNEL_VOICE ); + } +} + +//============================================================================= +// +// hhTalon::Event_PerchSquawk +// +// Louder, squawking noise, alerting the player to this location +//============================================================================= + +void hhTalon::Event_PerchSquawk(void) { + if ( !talonTarget ) { + return; + } + + if ( talonTarget->bShouldSquawk ) { // Always check, because it could have been turned off after Talon perched + + // Check if the player is outside of the squawk distance + float squawkDistSqr = talonTarget->spawnArgs.GetFloat( "squawkDistance", "0" ); + squawkDistSqr *= squawkDistSqr; + + if ( (GetOrigin() - owner->GetOrigin() ).LengthSqr() > squawkDistSqr ) { // Far enough away, so squawk + GetAnimator()->ClearAllAnims( gameLocal.time, TALON_BLEND ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, squawkAnim, gameLocal.time, TALON_BLEND); + PostEventMS( &EV_IdleAnim, GetAnimator()->GetAnim( squawkAnim )->Length() + TALON_BLEND ); + StartSound( "snd_squawk", SND_CHANNEL_VOICE ); + + // Glow + SetShaderParm( SHADERPARM_TIMEOFFSET, MS2SEC( gameLocal.time ) ); + } + + // Post the next squawk attempt + float frequency = talonTarget->spawnArgs.GetFloat( "squawkFrequency", "5.0" ); + PostEventSec( &EV_PerchSquawk, frequency + frequency * gameLocal.random.RandomFloat() ); + } +} + +//============================================================================= +// +// hhTalon::Event_CheckForTarget +// +//============================================================================= + +void hhTalon::Event_CheckForTarget() { + hhTalonTarget *oldTarget = talonTarget; + + // Determine if Talon should take off from his perch + if ( !bForcedTarget ) { // do not check if Talon is forced this this point + FindTalonTarget(NULL, NULL, false); + + if( oldTarget != talonTarget ) { + Event_TakeOffAnim(); + + if( oldTarget ) { // Inform the perch spot that talon is leaving + oldTarget->Left(this); + } + + EnterFlyState(); + return; + } + } + + PostEventSec( &EV_CheckForTarget, spawnArgs.GetFloat( "checkTargetTime", "1.0" ) ); +} + +//============================================================================= +// +// hhTalon::Event_CheckForEnemy +// +//============================================================================= + +void hhTalon::Event_CheckForEnemy() { + if ( !ai_talonAttack.GetBool() ) { + return; + } + if ( !bForcedTarget ) { + if ( FindEnemy() ) { + Event_TakeOffAnim(); + + if( talonTarget ) { // Inform the perch spot that talon is leaving + talonTarget->Left( this ); + } + + EnterAttackState(); + return; + } + } + + PostEventSec( &EV_CheckForEnemy, spawnArgs.GetFloat("checkEnemyTime", "4.0" ) ); +} + +//============================================================================= +// +// hhTalon::EnterAttackState +// +//============================================================================= + +void hhTalon::EnterAttackState(void) { + EndState(); + state = StateAttack; + checkFlyTime = spawnArgs.GetFloat( "attackTime", "8.0f" ); + + StartSound( "snd_attack", SND_CHANNEL_BODY ); + + bClawingAtEnemy = false; + + StartAttackFX(); +} + +//============================================================================= +// +// hhTalon::AttackTicker +// +//============================================================================= + +void hhTalon::AttackTicker(void) { + float deltaTime = MS2SEC(gameLocal.msec); + + if ( !FindEnemy() ) { + ReturnToTommy(); + StopAttackFX(); + return; + } + + idAngles ang; + float distance; + idVec3 oldVel; + bool landing = false; + float distanceXY; + float deltaZ; + float rotSpeed; + float distToEnemy; + + idMat3 junkAxis; + enemy->GetViewPos(talonTargetLoc, junkAxis); + talonTargetLoc -= junkAxis[2] * 10.0f; + + distToEnemy = (talonTargetLoc - GetOrigin()).Length(); + + acceleration = talonTargetLoc - GetOrigin(); + + idVec2 vecXY = acceleration.ToVec2(); + distance = acceleration.Normalize(); + + distanceXY = vecXY.Length(); + deltaZ = talonTargetLoc.z - GetOrigin().z; + + float dp = acceleration * GetAxis()[0]; + float side = acceleration * GetAxis()[1]; + ang = GetAxis().ToAngles(); + + // Talon can either rotate towards the target, or fly straight. + if( flyStraightTime > 0 ) { // Continue to fly straight + flyStraightTime -= deltaTime; + + if ( flyStraightTime <= 0 ) { // Done gliding, play a flap animation + Event_FlyAnim(); + + if ( bClawingAtEnemy ) { // Done attacking + bClawingAtEnemy = false; + + flyStraightTime = 1.0f; + } + } + + rotSpeed = 0; + } else { + rotSpeed = ROTATION_SPEED * rotationFactor; + } + + rotSpeed *= (60.0f * USERCMD_ONE_OVER_HZ); // CJR PCF 04/28/06: Adjust for 30Hz + + // Determine whether Talon should rotate left or right to reach the target + if(dp > 0) { + if(dp >= 0.98f || ( side <= 0.1f && side >= 0.1f )) { // CJR PCF 04/28/06: don't turn if directly facing the target + rotSpeed = 0; + } else if (side < -0.1f) { + rotSpeed *= -1; + } + } else { + if(side < 0) { + rotSpeed *= -1; + } + } + + // Apply rotation + deltaViewAngles.yaw = rotSpeed; + float desiredRoll = -side * 20 * rotSpeed; + + oldVel = velocity; + + float defaultVelocityXY = spawnArgs.GetFloat( "velocityXY_attack", "8.5" ); + float defaultVelocityZ = spawnArgs.GetFloat( "velocityZ", "4" ); + + // Adjust velocity based upon the attack + if ( bClawingAtEnemy ) { + defaultVelocityXY = 0.0f; + + idVec3 tempVec( vecXY.x, vecXY.y, 0.0f ); + viewAxis = tempVec.ToMat3(); + + } else if ( flyStraightTime > 0 ) { // Flying away from the enemy + float adjust = hhMath::ClampFloat( 0.0f, 1.0f, 2.0f - flyStraightTime * 2.0f ); + defaultVelocityXY *= adjust; // Move more slowly when about to attack + } else { // Flying around, check if the bird should slow down when nearing the enemy + if ( distToEnemy < 150.0f && dp > 0 ) { // Close to the enemy and facing it + if( !GetAnimator()->IsAnimPlaying( GetAnimator()->GetAnim( preAttackAnim ) ) ) { + GetAnimator()->ClearAllAnims( gameLocal.time, TALON_BLEND ); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, preAttackAnim, gameLocal.time, TALON_BLEND); + CancelEvents( &EV_GlideAnim ); // Could possibly have a glide anim queue + CancelEvents( &EV_TakeOffAnimB ); + } + float adjust = 1.0f - hhMath::ClampFloat( 0.0f, 0.9f, (100 - (distToEnemy-50) ) / 100.0f); // Decelerate Talon + defaultVelocityXY *= adjust; + } + } + + velocity = viewAxis[0] * defaultVelocityXY * velocityFactor; // xy-velocity + + // Z-change based upon distance + if(abs(deltaZ) > 0 && distanceXY > 0) { + velocity.z = (deltaZ * defaultVelocityXY) / distanceXY; + + // Clamp z velocity + if(velocity.z > defaultVelocityZ) { + velocity.z = defaultVelocityZ; + } + } + + // Apply velocity + SetOrigin( GetOrigin() + velocity ); + + UpdateVisuals(); + + // Timeout for flying to perch spot... if too much time has passed, then fly straight for a bit + checkFlyTime -= deltaTime; + if( checkFlyTime <= 0 && !bClawingAtEnemy && flyStraightTime <= 0 ) { + ReturnToTommy(); + StopAttackFX(); + } + + // Check if Talon reached the target and attack! + if ( flyStraightTime <= 0 && ( distToEnemy < spawnArgs.GetFloat( "distanceAttackEpsilon", "60" ) ) ) { + // Damage the enemy + StartSound( "snd_attack", SND_CHANNEL_BODY ); + + // Distract the enemy by setting Talon as its enemy -- random chance that this will succeed + if ( enemy.IsValid() && gameLocal.random.RandomFloat() < spawnArgs.GetFloat( "attackChance", "0.5" ) ) { + (static_cast(enemy.GetEntity()))->Distracted( this ); + owner->TalonAttackComment(); + } + + // Play an attack anim here + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, attackAnim, gameLocal.time, 0 ); + + flyStraightTime = MS2SEC( GetAnimator()->GetAnim( attackAnim )->Length() ); + PostEventSec( &EV_FlyAnim, flyStraightTime ); // Play a fly anim once this attack animation is done + + bClawingAtEnemy = true; + } +} + +//============================================================================= +// +// hhTalon::StartAttackFX +// +//============================================================================= + +void hhTalon::StartAttackFX() { + SetShaderParm( SHADERPARM_MODE, 1 ); // Fire glow attack + + if ( trailFx.IsValid() ) { // An effect already exists, turn it on + trailFx->Nozzle( true ); + } else { // Spawn a new effect + const char *defName = spawnArgs.GetString( "fx_attackTrail" ); + if (defName && defName[0]) { + hhFxInfo fxInfo; + + fxInfo.SetNormal( -GetAxis()[0] ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( false ); + trailFx = SpawnFxLocal( defName, GetOrigin(), GetAxis(), &fxInfo, gameLocal.isClient ); + if (trailFx.IsValid()) { + trailFx->fl.neverDormant = true; + trailFx->fl.networkSync = false; + trailFx->fl.clientEvents = true; + } + } + } +} + +//============================================================================= +// +// hhTalon::StopAttackFX +// +//============================================================================= + +void hhTalon::StopAttackFX() { + SetShaderParm( SHADERPARM_MODE, 0 ); // Fire glow attack done + + if ( trailFx.IsValid() ) { + trailFx->Nozzle( false ); + } +} + +//============================================================================= +// +// hhTalon::EnterVehicleState +// +//============================================================================= + +void hhTalon::EnterVehicleState(void) { + EndState(); + state = StateVehicle; + Hide(); + + CancelEvents( &EV_GlideAnim ); // Could possibly have a glide anim queue + CancelEvents( &EV_TakeOffAnimB ); +} + +//============================================================================= +// +// hhTalon::ExitVehicleState +// +//============================================================================= + +void hhTalon::ExitVehicleState(void) { + if ( talonTarget ) { + talonTarget->Left( this ); + } +} + +//============================================================================= +// +// hhTalon::VehicleTicker +// +// How Talon acts when the player is in a vehicle: +// - In the interest of speed, Talon warps from target to Target +// - This allows for the giant translation GUIs to instantly translate as the player gets near +//============================================================================= + +void hhTalon::VehicleTicker(void) { + float deltaTime = MS2SEC(gameLocal.msec); + hhTalonTarget *oldEnt = talonTarget; + + if( GetAnimator()->IsAnimPlaying( GetAnimator()->GetAnim( prelandAnim ) ) ) { + return; + } + + FindTalonTarget( NULL, NULL, bReturnToTommy ); + + if ( talonTarget == oldEnt ) { // No change to the target, so do nothing + return; + } + + if ( !talonTarget ) { // No Talon Target, so hide Talon + Hide(); + } else { // have a target, to show the bird + Show(); + } + + // Have a target, so spawn the bird at the target, if it isn't already there + CalculateTalonTargetLocation(); // Must recalculate the target location, as the target may have moved + SetOrigin( talonTargetLoc ); + SetAxis( talonTargetAxis ); + UpdateVisuals(); + + SetSkin( openWingsSkin ); + Event_LandAnim(); + + // Trigger the target + if ( talonTarget ) { + talonTarget->Reached( this ); // Reached the spot, but fly through it + } + + // Inform the last talonTarget that Talon has left + if ( oldEnt ) { + oldEnt->Left( this ); + } +} + +//============================================================================= +// +// hhTalon::UpdateAnimationControllers +// Talon doesn't use any anim controllers +//============================================================================= + +bool hhTalon::UpdateAnimationControllers() { + idVec3 vel; + float speed; + float roll; + + speed = velocity.Length(); + if ( speed < 3.0f || flyStraightTime > 0 ) { + roll = 0.0f; + } else { + roll = acceleration * viewAxis[1] * -fly_roll_scale / fly_speed; + if ( roll > fly_roll_max ) { + roll = fly_roll_max; + } else if ( roll < -fly_roll_max ) { + roll = -fly_roll_max; + } + } + + fly_roll = fly_roll * 0.95f + roll * 0.05f; + + viewAxis = idAngles( 0, current_yaw, fly_roll ).ToMat3(); + + return false; +} + +//============================================================================= +// +// hhTalon::ReturnToTommy +// +//============================================================================= + +void hhTalon::ReturnToTommy( void ) { + if ( bReturnToTommy ) { // already returning + return; + } + + bReturnToTommy = true; + bLanding = false; + + FindTalonTarget(NULL, NULL, true); + + SetSkin( openWingsSkin ); + GetAnimator()->ClearAllAnims( gameLocal.time, TALON_BLEND ); + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, flyAnim, gameLocal.time, TALON_BLEND); + EnterFlyState(); +} + +// ANIM EVENTS ================================================================ + +//============================================================================= +// +// hhTalon::Event_LandAnim +// +//============================================================================= + +void hhTalon::Event_LandAnim(void) { + GetAnimator()->ClearAllAnims( gameLocal.time, TALON_BLEND ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, landAnim, gameLocal.time, TALON_BLEND); + + if ( talonTarget ) { // Different animation if landing on Tommy or not + PostEventMS( &EV_IdleAnim, GetAnimator()->GetAnim( landAnim )->Length() + TALON_BLEND ); + } else { // No target, so the bird must be landing on Tommy + PostEventMS( &EV_TommyIdleAnim, GetAnimator()->GetAnim( landAnim )->Length() + TALON_BLEND ); + } + + CancelEvents( &EV_GlideAnim ); // Could possibly have a glide anim queue + CancelEvents( &EV_TakeOffAnimB ); +} + +//============================================================================= +// +// hhTalon::Event_PreLandAnim +// +//============================================================================= + +void hhTalon::Event_PreLandAnim(void) { + GetAnimator()->ClearAllAnims( gameLocal.time, TALON_BLEND ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, prelandAnim, gameLocal.time, TALON_BLEND ); + + CancelEvents( &EV_GlideAnim ); // Could possibly have a glide anim queue + CancelEvents( &EV_TakeOffAnimB ); +} + +//============================================================================= +// +// hhTalon::Event_IdleAnim +// +//============================================================================= + +void hhTalon::Event_IdleAnim(void) { + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, idleAnim, gameLocal.time, TALON_BLEND); + + StartSound("snd_idle", SND_CHANNEL_BODY); +} + +//============================================================================= +// +// hhTalon::Event_TommyIdleAnim +// +//============================================================================= + +void hhTalon::Event_TommyIdleAnim(void) { + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, tommyIdleAnim, gameLocal.time, TALON_BLEND); +} + +//============================================================================= +// +// hhTalon::Event_FlyAnim +// +//============================================================================= + +void hhTalon::Event_FlyAnim(void) { + GetAnimator()->ClearAllAnims( gameLocal.time, TALON_BLEND ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, flyAnim, gameLocal.time, TALON_BLEND); + + StartSound("snd_fly", SND_CHANNEL_BODY); +} + +//============================================================================= +// +// hhTalon::Event_GlideAnim +// +//============================================================================= + +void hhTalon::Event_GlideAnim(void) { + GetAnimator()->ClearAllAnims( gameLocal.time, TALON_BLEND ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, glideAnim, gameLocal.time, TALON_BLEND); + + StartSound("snd_glide", SND_CHANNEL_BODY); +} + +//============================================================================= +// +// hhTalon::Event_TakeOffAnim +// +//============================================================================= + +void hhTalon::Event_TakeOffAnim(void) { + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); // No blend when clearing old anims while taking off + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, takeOffAnim, gameLocal.time, TALON_BLEND); + + PostEventMS( &EV_TakeOffAnimB, GetAnimator()->GetAnim( takeOffAnim )->Length() + TALON_BLEND ); + + StartSound("snd_takeoff", SND_CHANNEL_ANY); +} + +//============================================================================= +// +// hhTalon::Event_TakeOffAnimB +// +//============================================================================= + +void hhTalon::Event_TakeOffAnimB(void) { + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, takeOffAnimB, gameLocal.time, 0); + + PostEventMS( &EV_GlideAnim, GetAnimator()->GetAnim( takeOffAnimB )->Length() + TALON_BLEND ); +} + +//============================================================================= +// +// hhTalon::Show +// +// Special show needed to remove any collision information on Talon +//============================================================================= + +void hhTalon::Show() { + if ( state == StateTommy ) { + return; + } + + hhMonsterAI::Show(); + + // Needed after the show + GetPhysics()->SetContents( 0 ); // No collisions + GetPhysics()->SetClipMask( 0 ); // No collisions +} + +// TALON PERCH SPOT =========================================================== + +//============================================================================= +// +// hhTalonTarget::Spawn +// +//============================================================================= + +void hhTalonTarget::Spawn(void) { + priority = 2.0f - spawnArgs.GetInt("priority"); + if ( priority < 0 ) { + priority = 0; + } else if ( priority > 2) { + priority = 2; + } + + bNotInSpiritWalk = spawnArgs.GetBool( "notInSpiritWalk" ); + bShouldSquawk = spawnArgs.GetBool( "shouldSquawk" ); + bValidForTalon = spawnArgs.GetBool( "valid", "1" ); + + gameLocal.RegisterTalonTarget(this); + + GetPhysics()->SetContents( 0 ); + + Hide(); +} + +void hhTalonTarget::Save(idSaveGame *savefile) const { + savefile->WriteInt( priority ); + savefile->WriteBool( bNotInSpiritWalk ); + savefile->WriteBool( bShouldSquawk ); + savefile->WriteBool( bValidForTalon ); +} + +void hhTalonTarget::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( priority ); + savefile->ReadBool( bNotInSpiritWalk ); + savefile->ReadBool( bShouldSquawk ); + savefile->ReadBool( bValidForTalon ); +} + +//============================================================================= +// +// hhTalonTarget::ShowTargets +// +//============================================================================= + +void hhTalonTarget::ShowTargets( void ) { + int i; + int count; + idEntity *ent; + + count = gameLocal.talonTargets.Num(); + for(i = 0; i < count; i++) { + ent = (hhTalonTarget *)gameLocal.talonTargets[i]; + ent->Show(); + } +} + +//============================================================================= +// +// hhTalonTarget::HideTargets +// +//============================================================================= + +void hhTalonTarget::HideTargets( void ) { + int i; + int count; + idEntity *ent; + + count = gameLocal.talonTargets.Num(); + for(i = 0; i < count; i++) { + ent = (hhTalonTarget *)gameLocal.talonTargets[i]; + ent->Hide(); + } +} + +//============================================================================= +// +// hhTalonTarget::~hhTalonTarget +// +//============================================================================= + +hhTalonTarget::~hhTalonTarget() { + gameLocal.talonTargets.Remove( this ); +} + +//============================================================================= +// +// hhTalonTarget::Reached +// +// Called when Talon reaches the target point +//============================================================================= + +void hhTalonTarget::Reached( hhTalon *talon ) { + if( !talon ) { + return; + } + + // Landing on a perch spot sends an action message to the spot's targets + int num = targets.Num(); + for (int ix=0; ixPostEventMS(&EV_TalonAction, 0, talon, true); + } + } +} + +//============================================================================= +// +// hhTalonTarget::Left +// +// Called when Talon leaves the target point +//============================================================================= + +void hhTalonTarget::Left( hhTalon *talon ) { + if( !talon ) { + return; + } + + // Leaving on a perch spot sends an action message to the spot's targets + int num = targets.Num(); + for (int ix=0; ixPostEventMS(&EV_TalonAction, 0, talon, false); + } + } +} + +//============================================================================= +// +// hhTalonTarget::Event_CallTalon +// +// Calls Talon to this point and forces him to stay on this point unless released +// or called to another point +//============================================================================= + +void hhTalonTarget::Event_CallTalon( float vel, float rot, float perch ) { + hhPlayer *player; + hhTalon *talon; + + // Find Talon + player = static_cast( gameLocal.GetLocalPlayer() ); + if ( !player->talon.IsValid() ) { + return; + } + + talon = player->talon.GetEntity(); + + talon->SetForcedTarget( true ); + + talon->SetForcedPhysicsFactors( vel, rot, perch ); + + talon->FindTalonTarget( NULL, this, false ); // Force this entity as the target + talon->Event_TakeOffAnim(); + + talon->EnterFlyState(); +} + +//============================================================================= +// +// hhTalonTarget::Event_ReleaseTalon +// +// Releases Talon from this perch spot and allows him to continue his normal scripting +//============================================================================= + +void hhTalonTarget::Event_ReleaseTalon() { + hhPlayer *player; + hhTalon *talon; + + // Find Talon + player = static_cast( gameLocal.GetLocalPlayer() ); + if ( !player->talon.IsValid() ) { + return; + } + + talon = player->talon.GetEntity(); + + talon->SetForcedTarget( false ); + talon->SetForcedPhysicsFactors( 1.0f, 1.0f, 1.0f ); +} + +//============================================================================= +// +// hhTalonTarget::Event_SetPerchState +// +// Set the state if Talon can or cannot fly towards this target +//============================================================================= + +void hhTalonTarget::Event_SetPerchState( float newState ) { + bValidForTalon = ( newState > 0 ) ? true : false; +} diff --git a/src/Prey/game_talon.h b/src/Prey/game_talon.h new file mode 100644 index 0000000..2ab5fd8 --- /dev/null +++ b/src/Prey/game_talon.h @@ -0,0 +1,194 @@ + +#ifndef __GAME_TALON_H__ +#define __GAME_TALON_H__ + +#define MAX_TALON_PTS 16 + +typedef enum talontargetType_s { + TTT_NORMAL, + TTT_IMPORTANT, + TTT_BENEFICIAL +} talonTargetType_t; + +extern const idEventDef EV_TalonAction; + +class hhTalonTarget; + +//----------------------------------------------------------------------------- + +class hhTalon : public hhMonsterAI { +public: + CLASS_PROTOTYPE(hhTalon); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + hhTalon(); + ~hhTalon(); + void SummonTalon(void); + void OwnerEnteredVehicle( void ); + void OwnerExitedVehicle( void ); + + void SetOwner( hhPlayer *newOwner ); + hhPlayer *GetOwner( void ) { return owner.GetEntity(); } + void FindTalonTarget( idEntity *skipEntity, hhTalonTarget *forceTarget, bool bForcePlayer ); + void CalculateTalonTargetLocation(); + + bool CheckReachedTarget(float distance); + void CheckCollisions(float deltaTime); + + void Portalled( idEntity *portal ); + void Show(); + + void SetForcedTarget( bool force ) { bForcedTarget = force; } + void SetForcedPhysicsFactors( float newVel, float newRot, float newPerch ) { velocityFactor = newVel; rotationFactor = newRot; perchRotationFactor = newPerch; } + bool UpdateAnimationControllers(); + + bool FindEnemy( void ); + + void EnterTommyState(); + void EnterFlyState(); + void EnterVehicleState(); + void ExitVehicleState(void); + + void EnterPerchState(); + void EnterAttackState(); + void EndState(); + + void Event_LandAnim(void); + void Event_PreLandAnim(void); + void Event_IdleAnim(void); + void Event_TommyIdleAnim(void); + void Event_FlyAnim(void); + void Event_GlideAnim(void); + void Event_TakeOffAnim(void); + void Event_TakeOffAnimB(void); + + void ReturnToTommy( void ); + + virtual int HasAmmo( ammo_t type, int amount ) { return 0; } + virtual bool UseAmmo( ammo_t type, int amount ) { return false ; } + + bool IsAttacking( void ) { return state == StateAttack; } + +protected: + void Think(); + void FlyMove( void ); + void AdjustFlyingAngles(); + bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); + void CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ); + + void Damage( idEntity *inflictor, idEntity *attack, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + + void Event_PerchChatter(void); + void Event_PerchSquawk(void); // Perched on a spot and squawking + + void Event_CheckForTarget(); // Checks for nearby targets to land upon + void Event_CheckForEnemy(); // Checks for nearby enemies + + void PerchTicker(void); // Perched on a spot + void FlyTicker(void); // Flying to a perch spot or to Tommy + void TommyTicker(void); // Perched on Tommy's shoulder + void AttackTicker(void); // Attacking an enemy + void VehicleTicker(void); // Owner is in a vehicle, so warp from talon point to talon point + + void StartAttackFX(); + void StopAttackFX(); + +protected: + idEntityPtr owner; + idVec3 velocity; + idVec3 acceleration; + + hhTalonTarget *talonTarget; + idVec3 talonTargetLoc; + idMat3 talonTargetAxis; + + idVec3 lastCheckOrigin; // Used to check if the bird flew through a wall + + float checkTraceTime; // Used to determine when to trace for world collisions + float checkFlyTime; // Used to timeout and find a new flying spot + float flyStraightTime; // Used to force Talon to fly straight for a short time + + bool bLanding; + bool bReturnToTommy; // Forces the bird to return to Tommy, no matter what + + bool bForcedTarget; // True if talon is forced to stay at particular talon target + + bool bClawingAtEnemy; // True if talon is currently playing his attack anim and clawing at creature's heads + + float velocityFactor; + float rotationFactor; + float perchRotationFactor; + + // Anims + int flyAnim; + int glideAnim; + int prelandAnim; + int landAnim; + int idleAnim; + int tommyIdleAnim; + int squawkAnim; + int stepAnim; + int takeOffAnim; + int takeOffAnimB; + int attackAnim; + int preAttackAnim; + + const idDeclSkin *openWingsSkin; + const idDeclSkin *closedWingsSkin; + + // Task variables + idEntityPtr enemy; // Used for attacking + + idEntityPtr trailFx; + + enum States { + StateNone = 0, + StateTommy, + StateFly, + StateVehicle, + StatePerch, + StateAttack + } state; + const idEventDef *stateEvent; // Ticker event for current state +}; + +//----------------------------------------------------------------------------- + +class hhTalonTarget : public idEntity { + friend class hhTalon; + +public: + CLASS_PROTOTYPE(hhTalonTarget); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + ~hhTalonTarget(); + + virtual void Reached( hhTalon *talon ); // Called when Talon reached this point + virtual void Left( hhTalon *talon ); // Called when Talon leaves this point + + static void ShowTargets( void ); + static void HideTargets( void ); + + void SetSquawk( bool newSquawk ) { bShouldSquawk = newSquawk; } + +protected: + void Event_CallTalon( float vel, float rot, float perch ); + void Event_SetPerchState( float newState ); + + void Event_ReleaseTalon(); + + int priority; // priority value. Zero is default, higher numbers is a higher priority + // -1 skips the target spot completely + bool bNotInSpiritWalk; // Talon will skip this target if the player is spiritwalking + + bool bShouldSquawk; // if Talon should squawk on this spot or not + bool bValidForTalon; // if Talon is attacted to this spot or not +}; + +#endif /* __GAME_TALON_H__ */ diff --git a/src/Prey/game_targetproxy.cpp b/src/Prey/game_targetproxy.cpp new file mode 100644 index 0000000..f78b06f --- /dev/null +++ b/src/Prey/game_targetproxy.cpp @@ -0,0 +1,282 @@ +// Game_targetproxy.cpp +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// +// hhModelProxy +// +// Keeps model/anims in sync with another entity +//========================================================================== +CLASS_DECLARATION(hhAnimatedEntity, hhModelProxy) +END_CLASS + +void hhModelProxy::Spawn() { + renderEntity.noSelfShadow = true; + renderEntity.noShadow = true; + + GetPhysics()->SetContents(0); + + BecomeActive(TH_THINK|TH_TICKER); + + // Required so that models move in place. + GetAnimator()->RemoveOriginOffset( true ); + + original = NULL; +} + +void hhModelProxy::Save(idSaveGame *savefile) const { + original.Save(savefile); + owner.Save(savefile); +} + +void hhModelProxy::Restore( idRestoreGame *savefile ) { + original.Restore(savefile); + owner.Restore(savefile); +} + +void hhModelProxy::WriteToSnapshot( idBitMsgDelta &msg ) const { + GetPhysics()->WriteToSnapshot(msg); + WriteBindToSnapshot(msg); + + msg.WriteBits(original.GetSpawnId(), 32); + msg.WriteBits(owner.GetSpawnId(), 32); + + msg.WriteBits(renderEntity.allowSurfaceInViewID, GENTITYNUM_BITS); +} + +void hhModelProxy::ReadFromSnapshot( const idBitMsgDelta &msg ) { + GetPhysics()->ReadFromSnapshot(msg); + ReadBindFromSnapshot(msg); + + original.SetSpawnId(msg.ReadBits(32)); + owner.SetSpawnId(msg.ReadBits(32)); + + renderEntity.allowSurfaceInViewID = msg.ReadBits(GENTITYNUM_BITS); +} + +void hhModelProxy::ClientPredictionThink( void ) { + Think(); +} + + +/* +=============== +hhModelProxy::SetOriginAndAxis +Note: This was ripped from idEntity::UpdateModelTransform (Was ripped from idEntity) +=============== +*/ +void hhModelProxy::SetOriginAndAxis( idEntity *entity ) { + idVec3 origin; + idMat3 axis; + + + if ( original->GetPhysicsToVisualTransform( origin, axis ) ) { + axis = axis * original->GetPhysics()->GetAxis(); + origin = original->GetPhysics()->GetOrigin() + origin * axis; + } + else { + axis = original->GetPhysics()->GetAxis(); + origin = original->GetPhysics()->GetOrigin(); + } + + SetOrigin( origin ); + SetAxis( axis ); +} + + +void hhModelProxy::SetOriginal(idEntity *other) { + original = other; + if (original.IsValid() && original.GetEntity()) { + SetOriginAndAxis( original.GetEntity() ); + Bind(original.GetEntity(), true); + } +} + +idEntity *hhModelProxy::GetOriginal() { + if (!original.IsValid()) { + return NULL; + } + return original.GetEntity(); +} + +void hhModelProxy::SetOwner(hhPlayer *other) { + owner = other; + renderEntity.allowSurfaceInViewID = owner->entityNumber+1; +} + +hhPlayer *hhModelProxy::GetOwner() { + if (!owner.IsValid()) { + return NULL; + } + return owner.GetEntity(); +} + +void hhModelProxy::Ticker() { + if (!owner.IsValid() || !original.IsValid()) { //rww - added validation for "original" + PostEventMS(&EV_Remove, 0); + return; + } + UpdateVisualState(); +} + +void hhModelProxy::UpdateVisualState() { + + // Update visual state to state of original + if (original.IsValid() && original.GetEntity() && !fl.hidden) { + if (GetRenderEntity()->hModel != original->GetRenderEntity()->hModel) { + const char *modelname; + + if (gameLocal.isMultiplayer && (original->IsType(hhPlayer::Type) || original->IsType(hhSpiritProxy::Type))) { + //rww - players and spirit proxies in mp have variable model names, but keep the original "model" field + //as a fallback. + modelname = original->spawnArgs.GetString( "playerModel" ); + if (!modelname || !modelname[0]) { //empty string, fallback to "model" + modelname = original->spawnArgs.GetString( "model" ); + } + } + else { + modelname = original->spawnArgs.GetString( "model" ); + } + + SetModel(modelname); + } + + // Run our physics to keep our binding to the original up to date. + GetPhysics()->Evaluate( gameLocal.time - gameLocal.previousTime, gameLocal.time ); + + if ( original->GetAnimator() ) { + GetAnimator()->CopyAnimations( *( original->GetAnimator() ) ); + GetAnimator()->CopyPoses( *( original->GetAnimator() ) ); + } + + UpdateVisuals(); + } +} + +void hhModelProxy::ProxyFinished() { + PostEventMS(&EV_Remove, 0); +} + + +//========================================================================== +// +// hhTargetProxy +// +// Special proxy for spirit bow vision that allows sight through walls +//========================================================================== +const idEventDef EV_FadeOutProxy(""); + +CLASS_DECLARATION(hhModelProxy, hhTargetProxy) + EVENT( EV_FadeOutProxy, hhTargetProxy::Event_FadeOut ) +END_CLASS + +void hhTargetProxy::Spawn() { + renderEntity.weaponDepthHack = true; + SetShaderParm(5, 0.5f + ((float)(entityNumber % 8)) / 8.0f); // Unique number [0.5 .. 1.5] for jitter + + owner = NULL; + + //TODO: Need a way to be visible only if owner is local client + + fadeAlpha.Init(gameLocal.time, BOWVISION_FADEIN_DURATION, 0.0f, 1.0f); + StayAlive(); + + fl.networkSync = true; +} + +void hhTargetProxy::Save(idSaveGame *savefile) const { + savefile->WriteFloat( fadeAlpha.GetStartTime() ); // idInterpolate + savefile->WriteFloat( fadeAlpha.GetDuration() ); + savefile->WriteFloat( fadeAlpha.GetStartValue() ); + savefile->WriteFloat( fadeAlpha.GetEndValue() ); +} + +void hhTargetProxy::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadFloat( set ); // idInterpolate + fadeAlpha.SetStartTime( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetDuration( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetStartValue(set); + savefile->ReadFloat( set ); + fadeAlpha.SetEndValue( set ); +} + +void hhTargetProxy::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhModelProxy::WriteToSnapshot(msg); + + msg.WriteFloat(fadeAlpha.GetStartTime()); + msg.WriteFloat(fadeAlpha.GetDuration()); + msg.WriteFloat(fadeAlpha.GetStartValue()); + msg.WriteFloat(fadeAlpha.GetEndValue()); +} + +void hhTargetProxy::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhModelProxy::ReadFromSnapshot(msg); + + fadeAlpha.SetStartTime(msg.ReadFloat()); + fadeAlpha.SetDuration(msg.ReadFloat()); + fadeAlpha.SetStartValue(msg.ReadFloat()); + fadeAlpha.SetEndValue(msg.ReadFloat()); +} + +void hhTargetProxy::ProxyFinished() { + CancelEvents(&EV_FadeOutProxy); + fadeAlpha.Init(gameLocal.time, BOWVISION_FADEOUT_DURATION, 1.0f, 0.0f); + PostEventMS(&EV_Remove, BOWVISION_FADEOUT_DURATION); +} + +void hhTargetProxy::UpdateVisualState() { + //TODO: If owner not in spirit walk, immediately hide and post a remove + // This should handle the case of being in altmode and toggling spiritwalk off + // so that we don't see the targets fade out in non-spiritwalk mode + + //TODO: net play support: snapshot owner->entityNumber. Hide if owner isn't the localclient +// if (owner->entityNumber == gameLocal.localClientNum) { +// } + + + if( !original.IsValid() || !original.GetEntity() || original->IsHidden() ) { // mdl: Don't show hidden entities (keeps actors from showing up in bowvision after gibbing) + //rww - added validity checks for "original" + Hide(); + return; + } + + // Check to see if within players view + idVec3 toProxy = GetPhysics()->GetOrigin() - owner->GetPhysics()->GetOrigin(); + float score = toProxy * (owner->viewAngles.ToMat3()[0]); + if (score < 0.2f) { + Hide(); + } + else { + Show(); + SetShaderParm(3, fadeAlpha.GetCurrentValue(gameLocal.time)); + hhModelProxy::UpdateVisualState(); + } +} + +void hhTargetProxy::StayAlive() { + // Stay alive for BOWVISION_UPDATE_FREQUENCY longer (MS) + a little slop + CancelEvents(&EV_Remove); + CancelEvents(&EV_FadeOutProxy); + PostEventMS(&EV_FadeOutProxy, BOWVISION_UPDATE_FREQUENCY * 2); + + // Fade back in if fading out, otherwise stay faded in + float curvalue = fadeAlpha.GetCurrentValue(gameLocal.time); + fadeAlpha.Init(gameLocal.time, BOWVISION_FADEIN_DURATION, curvalue, 1.0f); + + if ( owner.IsValid() && owner.GetEntity() ) { + UpdateVisualState(); + } +} + +void hhTargetProxy::Event_FadeOut() { + ProxyFinished(); +} diff --git a/src/Prey/game_targetproxy.h b/src/Prey/game_targetproxy.h new file mode 100644 index 0000000..0c8ceb3 --- /dev/null +++ b/src/Prey/game_targetproxy.h @@ -0,0 +1,63 @@ + +#ifndef __GAME_TARGETPROXY_H__ +#define __GAME_TARGETPROXY_H__ + +class hhModelProxy : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhModelProxy ); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - net code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + void SetOriginal(idEntity *other); + idEntity * GetOriginal(); + virtual void ProxyFinished(); + virtual void Ticker(); + virtual void UpdateVisualState(); + virtual void SetOriginAndAxis( idEntity * ); + void SetOwner(hhPlayer *other); + hhPlayer * GetOwner(); + +protected: + //rww - these need to be entityptr's, not raw pointers + idEntityPtr original; + idEntityPtr owner; +}; + + +#define BOWVISION_FADEIN_DURATION 1500 +#define BOWVISION_FADEOUT_DURATION 1500 +#define BOWVISION_UPDATE_FREQUENCY 12*MAX_GENTITIES/BOWVISION_ROVER_STRIDE // Rate at which to search for proxies +#define BOWVISION_ROVER_STRIDE 25 // Number of entities to check each tick + +class hhTargetProxy : public hhModelProxy { +public: + CLASS_PROTOTYPE( hhTargetProxy ); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - net code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + void StayAlive(); + virtual void ProxyFinished(); + virtual void UpdateVisualState(); + +protected: + void Event_FadeOut(); + +protected: + idInterpolate fadeAlpha; +}; + + +#endif diff --git a/src/Prey/game_targets.cpp b/src/Prey/game_targets.cpp new file mode 100644 index 0000000..861af6a --- /dev/null +++ b/src/Prey/game_targets.cpp @@ -0,0 +1,849 @@ +/* + game_targets.cpp + + These entities, when targeted, perform an action + +*/ + + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_Autosave( "" ); +const idEventDef EV_FinishedSave( "" ); + +/*********************************************************************** + + hhTarget_SetSkin + + Set all targets to given skin +***********************************************************************/ + +CLASS_DECLARATION( idTarget, hhTarget_SetSkin ) + EVENT( EV_Activate, hhTarget_SetSkin::Event_Trigger ) +END_CLASS + +/* +================ +hhTarget_SetSkin::Spawn +================ +*/ +void hhTarget_SetSkin::Spawn( void ) { + spawnArgs.GetString( "skinName", "", skinName ); +} + + +/* +================ + hhTarget_SetSkin::Event_Trigger +================ +*/ +void hhTarget_SetSkin::Event_Trigger( idEntity *activator ) { + idEntity *ent = NULL; + + // Set all targets to the specified skin + for( int t=0; tSetSkinByName( (ent->GetSkin()) ? NULL : skinName.c_str() ); + } +} + + + +/*********************************************************************** + + hhTarget_Enable + + Enable all targets +***********************************************************************/ + +CLASS_DECLARATION( idTarget, hhTarget_Enable ) + EVENT( EV_Activate, hhTarget_Enable::Event_Trigger ) +END_CLASS + +/* +================ +hhTarget_Enable::Spawn +================ +*/ +void hhTarget_Enable::Spawn( void ) { +} + + +/* +================ + hhTarget_Enable::Event_Trigger +================ +*/ +void hhTarget_Enable::Event_Trigger( idEntity *activator ) { + + // Enable all targets + for (int t=0; tPostEventMS( &EV_Enable, 0 ); + } + } +} + + +/*********************************************************************** + + hhTarget_Disable + + Disable all targets +***********************************************************************/ + +CLASS_DECLARATION( idTarget, hhTarget_Disable ) + EVENT( EV_Activate, hhTarget_Disable::Event_Trigger ) +END_CLASS + +/* +================ +hhTarget_Disable::Spawn +================ +*/ +void hhTarget_Disable::Spawn( void ) { +} + + +/* +================ + hhTarget_Disable::Event_Trigger +================ +*/ +void hhTarget_Disable::Event_Trigger( idEntity *activator ) { + + // Disable all targets + for (int t=0; tPostEventMS( &EV_Disable, 0 ); + } + } +} + + +/*********************************************************************** + + hhTarget_Earthquake + +***********************************************************************/ +const idEventDef EV_TurnOff( "", NULL ); + +CLASS_DECLARATION( idTarget, hhTarget_Earthquake ) + EVENT( EV_Activate, hhTarget_Earthquake::Event_Trigger ) + EVENT( EV_TurnOff, hhTarget_Earthquake::Event_TurnOff ) +END_CLASS + +/* +================ +hhTarget_Earthquake::Spawn +================ +*/ +void hhTarget_Earthquake::Spawn( void ) { + //PDMMERGE: Modify to use the sound for the visual shake exclusively + // and forcefield for the world shake exclusively. + //This duplicates functionality of func_earthquake. Check their effect + //out and see which we want to keep. If keeping, this could then become func_earthquake. + //Could alternatively use a sound player and a func_idforcefield + + shakeTime = SEC2MS(spawnArgs.GetFloat("shake_time")); + shakeAmplitude = spawnArgs.GetFloat("shake_severity"); + + forceField.RandomTorque( shakeAmplitude * 40); + + // Grab clip model to use for checking who's currently encroaching me + cm = new idClipModel( GetPhysics()->GetClipModel() ); + if( !cm ) { + gameLocal.Error( "hhTarget_Earthquake::Spawn: Unable to spawn idClipModel\n" ); + return; + } + forceField.SetClipModel( cm ); + // remove the collision model from the physics object + GetPhysics()->SetClipModel( NULL, 1.0f ); +} + +void hhTarget_Earthquake::Save(idSaveGame *savefile) const { + savefile->WriteFloat( shakeTime ); + savefile->WriteFloat( shakeAmplitude ); + savefile->WriteStaticObject( forceField ); + savefile->WriteClipModel( cm ); +} + +void hhTarget_Earthquake::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( shakeTime ); + savefile->ReadFloat( shakeAmplitude ); + savefile->ReadStaticObject( forceField ); + savefile->ReadClipModel( cm ); +} + +/* +================ + hhTarget_Earthquake::Think +================ +*/ +void hhTarget_Earthquake::Think() { + if (thinkFlags & TH_THINK) { + forceField.Evaluate( gameLocal.time ); + } +} + + +/* +================ +hhTarget_Earthquake::Event_TurnOff +================ +*/ +void hhTarget_Earthquake::Event_TurnOff( void ) { + hhPlayer *player; + BecomeInactive(TH_THINK); + StopSound(SND_CHANNEL_BODY); + + // Turn camera interpolator back on + for (int i = 0; i < gameLocal.numClients; i++ ) { + player = static_cast(gameLocal.entities[ i ]); + if ( player ) { + player->cameraInterpolator.SetInterpolationType( IT_VariableMidPointSinusoidal ); + } + } +} + +/* +================ + hhTarget_Earthquake::Event_Trigger +================ +*/ +void hhTarget_Earthquake::Event_Trigger( idEntity *activator ) { + hhPlayer *player; + + if( !cm ) { // No collision model + return; + } + + StartSound("snd_quake", SND_CHANNEL_BODY); + + idBounds bounds; + bounds.FromTransformedBounds( cm->GetBounds(), cm->GetOrigin(), cm->GetAxis() ); + //gameRenderWorld->DebugBounds(colorRed, bounds, vec3_origin, 5000); + + for (int i = 0; i < gameLocal.numClients; i++ ) { + player = static_cast(gameLocal.entities[ i ]); + if ( !player || ( player->fl.notarget ) ) { + continue; + } + + if( bounds.IntersectsBounds(player->GetPhysics()->GetAbsBounds()) ) { + player->cameraInterpolator.SetInterpolationType(IT_None); + } + } + + // Turn on physics + BecomeActive( TH_THINK ); + PostEventSec( &EV_TurnOff, MS2SEC(shakeTime) ); +} + + +/*********************************************************************** + + hhTarget_SetLightParm + +***********************************************************************/ + +CLASS_DECLARATION( idTarget, hhTarget_SetLightParm ) + EVENT( EV_Activate, hhTarget_SetLightParm::Event_Activate ) +END_CLASS + +/* +================ +hhTarget_SetLightParm::Event_Activate +================ +*/ +void hhTarget_SetLightParm::Event_Activate( idEntity *activator ) { + int i; + idEntity * ent; + float value; + idVec3 color; + int parmnum; + + // set the color on the targets + if ( spawnArgs.GetVector( "_color", "1 1 1", color ) ) { + for( i = 0; i < targets.Num(); i++ ) { + ent = targets[ i ].GetEntity(); + if ( ent ) { + ent->SetColor( color[ 0 ], color[ 1 ], color[ 2 ] ); + } + } + } + + // set any shader parms on the targets + for( parmnum = 0; parmnum < MAX_ENTITY_SHADER_PARMS; parmnum++ ) { + if ( spawnArgs.GetFloat( va( "shaderParm%d", parmnum ), "0", value ) ) { + for( i = 0; i < targets.Num(); i++ ) { + ent = targets[ i ].GetEntity(); + if ( ent && ent->IsType(idLight::Type) ) { + static_cast(ent)->SetLightParm( parmnum, value ); + } + } + if (spawnArgs.GetBool("toggle") && (value == 0 || value == 1)) { + int val = value; + val ^= 1; + value = val; + spawnArgs.SetFloat(va("shaderParm%d", parmnum), value); + } + } + } +} + +/*********************************************************************** + + hhTarget_PlayWeaponAnim + +***********************************************************************/ + +CLASS_DECLARATION( idTarget, hhTarget_PlayWeaponAnim ) + EVENT( EV_Activate, hhTarget_PlayWeaponAnim::Event_Trigger ) +END_CLASS + +/* +================ + hhTarget_PlayWeaponAnim::Event_Trigger +================ +*/ +void hhTarget_PlayWeaponAnim::Event_Trigger( idEntity *activator ) { + hhPlayer* pPlayer = NULL; + + if( activator && activator->IsType(hhPlayer::Type) ) { + pPlayer = static_cast( activator ); + if( pPlayer ) { + pPlayer->ProcessEvent( &EV_PlayWeaponAnim, spawnArgs.GetString("anim", "initialPickup"), spawnArgs.GetInt("numTries") ); + } + } +} + + +//========================================================================== +// +// hhTarget_ControlVehicle +// +//========================================================================== + +CLASS_DECLARATION(idTarget, hhTarget_ControlVehicle) + EVENT( EV_Activate, hhTarget_ControlVehicle::Event_Activate ) +END_CLASS + + +void hhTarget_ControlVehicle::Spawn() { +} + +void hhTarget_ControlVehicle::Event_Activate( idEntity *activator ) { + hhVehicle *vehicle = NULL; + + if (activator->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast(activator); + + // Search target list to find vehicle + int numTargets = targets.Num(); + for (int ix=0; ixIsType(hhVehicle::Type)) { + vehicle = static_cast(targets[ix].GetEntity()); + } + } + } + + if( vehicle ) { + player->EnterVehicle( vehicle ); + } + } +} + + +//========================================================================== +// +// hhTarget_AttachToRail +// +// OBSOLETE, just target the hhRailRide directly +//========================================================================== + +CLASS_DECLARATION(idTarget, hhTarget_AttachToRail) + EVENT( EV_Activate, hhTarget_AttachToRail::Event_Activate ) +END_CLASS + + +void hhTarget_AttachToRail::Spawn() { +} + +// Bind player to given bone of a targeted hhAnimator +void hhTarget_AttachToRail::Event_Activate( idEntity *activator ) { + hhRailRide *rail = NULL; + + if (activator && activator->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast(activator); + + // Search target list to find hhRailController + int numTargets = targets.Num(); + for (int ix=0; ixIsType(hhRailRide::Type)) { + rail = static_cast(targets[ix].GetEntity()); + } + } + } + + if (rail) { + rail->Attach(player, false); + } + } +} + +//========================================================================== +// +// hhTarget_EnableReactions +// +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_EnableReactions ) + EVENT( EV_Activate, hhTarget_EnableReactions::Event_Activate ) +END_CLASS + +void hhTarget_EnableReactions::Spawn() { +} + +void hhTarget_EnableReactions::Event_Activate(idEntity *activator) { + // Enable reactions on all targets + for (int t=0; tfl.refreshReactions = true; + } + } +} + +//========================================================================== +// +// hhTarget_DisableReactions +// +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_DisableReactions ) + EVENT( EV_Activate, hhTarget_DisableReactions::Event_Activate ) +END_CLASS + +void hhTarget_DisableReactions::Spawn() { +} + +void hhTarget_DisableReactions::Event_Activate(idEntity *activator) { + // Disable reactions on all targets + for (int t=0; tfl.refreshReactions = false; + } + } +} + +//========================================================================== +// +// hhTarget_EnablePassageway +// +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_EnablePassageway ) + EVENT( EV_Activate, hhTarget_EnablePassageway::Event_Activate ) +END_CLASS + +void hhTarget_EnablePassageway::Spawn() { +} + +void hhTarget_EnablePassageway::Event_Activate(idEntity *activator) { + // Enable reactions on all targets + for (int t=0; tIsType(hhAIPassageway::Type) ) { + static_cast(ent)->SetEnablePassageway(TRUE); + } + } +} + +//========================================================================== +// +// hhTarget_DisablePassageway +// +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_DisablePassageway ) + EVENT( EV_Activate, hhTarget_DisablePassageway::Event_Activate ) +END_CLASS + +void hhTarget_DisablePassageway::Spawn() { +} + +void hhTarget_DisablePassageway::Event_Activate(idEntity *activator) { + // Enable reactions on all targets + for (int t=0; tIsType(hhAIPassageway::Type)) { + static_cast(ent)->SetEnablePassageway(FALSE); + } + } +} + + +const idEventDef EV_TriggerTargets( "" ); + +//========================================================================== +// +// hhTarget_PatternRelay +// +// When triggered, relays trigger messages in a pattern +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_PatternRelay ) + EVENT( EV_Activate, hhTarget_PatternRelay::Event_Activate ) + EVENT( EV_TriggerTargets, hhTarget_PatternRelay::Event_TriggerTargets ) +END_CLASS + +void hhTarget_PatternRelay::Spawn() { + timeGranularity = spawnArgs.GetFloat("timestep", "1"); +} + +void hhTarget_PatternRelay::Event_Activate(idEntity *activator) { + idStr pattern = spawnArgs.GetString("pattern", "xoxoxo"); + + //TODO: Clear out any from last time around? + + float issueTime = 0.0f; + int length = pattern.Length(); + for (int ix=0; ixhud->SetStateInt("subtitlex", bCentered ? 0 : x); + player->hud->SetStateInt("subtitley", y); + player->hud->SetStateInt("subtitlecentered", bCentered); + player->hud->SetStateString("subtitletext", text); + player->hud->StateChanged(gameLocal.time); + player->hud->HandleNamedEvent("DisplaySubtitle"); + + CancelEvents(&EV_TurnOff); + PostEventSec(&EV_TurnOff, duration); +} + +void hhTarget_Subtitle::Event_FadeOutText() { + idPlayer *player = gameLocal.GetLocalPlayer(); + if (player && player->hud) { + player->hud->HandleNamedEvent("RemoveSubtitle"); + } +} + + +//========================================================================== +// +// hhTarget_EndLevel +// +// When triggered, does a level transition +//========================================================================== +CLASS_DECLARATION( idTarget_EndLevel, hhTarget_EndLevel ) + EVENT( EV_Activate, hhTarget_EndLevel::Event_Activate ) +END_CLASS + +void hhTarget_EndLevel::Event_Activate(idEntity *activator) { + idUserInterface *guiLoading = NULL; + idStr nextMap; + if ( spawnArgs.GetString( "nextMap", "", nextMap ) ) { + + // Session code hasn't yet loaded the gui for next level here, so we preload it based on the same + // logic as it uses in idSessionLocal::LoadLoadingGui(). + idStr stripped = nextMap; + stripped.StripFileExtension(); + stripped.StripPath(); + idStr guiMap = va( "guis/map/%s.gui", stripped.c_str() ); + if (uiManager->CheckGui( guiMap ) ) { + guiLoading = uiManager->FindGui( guiMap, true, false, true ); + } else { + guiLoading = uiManager->FindGui( "guis/map/loading.gui", true, false, true ); + } + guiLoading->SetStateFloat("map_loading", 0.0f); + guiLoading->SetStateBool("showddainfo", true); + + const idDecl *mapDecl = declManager->FindType(DECL_MAPDEF, stripped.c_str(), false ); + if ( mapDecl ) { + const idDeclEntityDef *mapInfo = static_cast(mapDecl); + const char *friendlyName = mapInfo->dict.GetString("name"); + guiLoading->SetStateString("friendlyname", friendlyName); + } + + guiLoading->StateChanged(gameLocal.time); + + // HUMANHEAD CJR: If the player hits this and they are spiritwalking, stop the spiritwalk before loading the next level + if ( activator->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast( activator ); + if ( player ) { + + // Player managed kill themselves at the end of level? Clear deathwalk flags + if (player->IsDeathWalking()) { + player->bDeathWalk = false; + player->SetHealth( 50.0f ); // Don't say I never got you anything + player->SetSpiritPower( 0.0f ); // Clear spirit power + } + + if ( player->IsSpiritWalking() ) { + player->PutawayEtherealWeapon(); // Don't do a full spiritstop, as that teleports the player, too. Just fix the player's weapon + } + } + } + // END CJR + } + + idTarget_EndLevel::Event_Activate(activator); +} + + +//========================================================================== +// +// hhTarget_ConsolidatePlayers +// +// When triggered, consolidates all players into a single view (coop) +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_ConsolidatePlayers ) + EVENT( EV_Activate, hhTarget_ConsolidatePlayers::Event_Activate ) +END_CLASS + +void hhTarget_ConsolidatePlayers::Event_Activate(idEntity *activator) { + if (gameLocal.IsCooperative()) { + //TODO: Implement + } +} + + +//========================================================================== +// +// hhTarget_WarpPlayers +// +// When triggered, teleports all players to targeted start spots (coop) +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_WarpPlayers ) + EVENT( EV_Activate, hhTarget_WarpPlayers::Event_Activate ) +END_CLASS + +void hhTarget_WarpPlayers::Event_Activate(idEntity *activator) { + if (gameLocal.IsCooperative()) { + //TODO: Implement + } +} + +//========================================================================== +// +// hhTarget_FollowPath +// +// When triggered, make an ai follow a path +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_FollowPath ) + EVENT( EV_Activate, hhTarget_FollowPath::Event_Activate ) +END_CLASS + +void hhTarget_FollowPath::Event_Activate(idEntity *activator) { + int i; + idEntity *ent; + const function_t *func; + const char *funcName; + idThread *thread; + hhMonsterAI *entAI; + + funcName = spawnArgs.GetString( "call" ); + for( i = 0; i < targets.Num(); i++ ) { + ent = targets[ i ].GetEntity(); + if ( ent && ent->scriptObject.HasObject() ) { + func = ent->scriptObject.GetFunction( funcName ); + if ( !func ) { + gameLocal.Error( "Function '%s' not found on entity '%s' for function call from '%s'", funcName, ent->name.c_str(), name.c_str() ); + } + if ( func->type->NumParameters() != 1 ) { + gameLocal.Error( "Function '%s' on entity '%s' has the wrong number of parameters for function call from '%s'", funcName, ent->name.c_str(), name.c_str() ); + } + if ( !ent->scriptObject.GetTypeDef()->Inherits( func->type->GetParmType( 0 ) ) ) { + gameLocal.Error( "Function '%s' on entity '%s' is the wrong type for function call from '%s'", funcName, ent->name.c_str(), name.c_str() ); + } + entAI = static_cast(ent); + if ( entAI && entAI->AI_FOLLOWING_PATH ) { + gameLocal.Warning( "hhTarget_FollowPath %s already following path", ent->name.c_str() ); + continue; + } + ent->spawnArgs.Set( "alt_path", spawnArgs.GetString( "alt_path" ) ); + // create a thread and call the function + thread = new idThread(); + thread->CallFunction( ent, func, true ); + thread->Start(); + } + } +} + +//=============================================================================== +// +// hhTarget_LockDoor +// +//=============================================================================== + +CLASS_DECLARATION( idTarget, hhTarget_LockDoor ) + EVENT( EV_Activate, hhTarget_LockDoor::Event_Activate ) +END_CLASS + +void hhTarget_LockDoor::Event_Activate( idEntity *activator ) { + int i; + idEntity *ent; + int lock; + + lock = spawnArgs.GetInt( "locked", "1" ); + for( i = 0; i < targets.Num(); i++ ) { + ent = targets[ i ].GetEntity(); + if ( ent && ent->IsType( idDoor::Type ) ) { + if ( static_cast( ent )->IsLocked() ) { + static_cast( ent )->Lock( 0 ); + } else { + static_cast( ent )->Lock( lock ); + } + } + if ( ent && ent->IsType( hhModelDoor::Type ) ) { + if ( static_cast( ent )->IsLocked() ) { + static_cast( ent )->Lock( 0 ); + } else { + static_cast( ent )->Lock( lock ); + } + } + if ( ent && ent->IsType( hhProxDoor::Type ) ) { + if ( static_cast( ent )->IsLocked() ) { + static_cast( ent )->Lock( 0 ); + } else { + static_cast( ent )->Lock( lock ); + } + } + } +} + +//========================================================================== +// +// hhTarget_DisplayGui +// +// When triggered, displays full screen gui +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_DisplayGui ) + EVENT( EV_Activate, hhTarget_DisplayGui::Event_Activate ) +END_CLASS + +void hhTarget_DisplayGui::Event_Activate(idEntity *activator) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if (player && player->IsType(hhPlayer::Type)) { + static_cast(player)->SetOverlayGui( spawnArgs.GetString("gui_overlay") ); + } +} + + +//========================================================================== +// +// hhTarget_Autosave +// +// When triggered, autosaves +//========================================================================== +CLASS_DECLARATION( idTarget, hhTarget_Autosave ) + EVENT( EV_Activate, hhTarget_Autosave::Event_Activate ) +END_CLASS + +void hhTarget_Autosave::Event_Activate(idEntity *activator) { + const char *desc = spawnArgs.GetString( "text_savename" ); + if ( !desc || desc[0] == 0 ) { + gameLocal.Warning( "hhTarget_Autosave has no text_savename key" ); + desc = ""; + } + + idStr saveName = va( common->GetLanguageDict()->GetString( "#str_00838" ), desc ); + //HUMANHEAD PCF mdl 05/05/06 - Changed ' to " to avoid problems when saves contained ' + cmdSystem->BufferCommandText( CMD_EXEC_APPEND, va( "savegame \"%s\"", saveName.c_str() ) ); +} + + +//========================================================================== +// +// hhTarget_Show +// +//========================================================================== + +CLASS_DECLARATION( idTarget, hhTarget_Show ) + EVENT( EV_Activate, hhTarget_Show::Event_Activate ) +END_CLASS + +void hhTarget_Show::Event_Activate( idEntity *activator ) { + int i; + idEntity *ent; + + for( i = 0; i < targets.Num(); i++ ) { + ent = targets[ i ].GetEntity(); + if ( ent ) { + ent->Show(); + } + } +} + +//========================================================================== +// +// hhTarget_Hide +// +//========================================================================== + +CLASS_DECLARATION( idTarget, hhTarget_Hide ) + EVENT( EV_Activate, hhTarget_Hide::Event_Activate ) +END_CLASS + +void hhTarget_Hide::Event_Activate( idEntity *activator ) { + int i; + idEntity *ent; + + for( i = 0; i < targets.Num(); i++ ) { + ent = targets[ i ].GetEntity(); + if ( ent ) { + ent->Hide(); + } + } +} + diff --git a/src/Prey/game_targets.h b/src/Prey/game_targets.h new file mode 100644 index 0000000..036f5c7 --- /dev/null +++ b/src/Prey/game_targets.h @@ -0,0 +1,235 @@ + +#ifndef __GAME_TARGETS_H__ +#define __GAME_TARGETS_H__ + +class hhTarget_SetSkin : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_SetSkin); + + void Spawn(void); + void Save( idSaveGame *savefile ) const { savefile->WriteString(skinName); } + void Restore( idRestoreGame *savefile ) { savefile->ReadString(skinName); } + +protected: + void Event_Trigger( idEntity *activator ); + + idStr skinName; +}; + +class hhTarget_Enable : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_Enable); + + void Spawn(void); + +protected: + void Event_Trigger( idEntity *activator ); +}; + +class hhTarget_Disable : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_Disable); + + void Spawn(void); + +protected: + void Event_Trigger( idEntity *activator ); +}; + +class hhTarget_Earthquake : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_Earthquake); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Present(void) { } + virtual void Think(void); + +protected: + void Event_Trigger( idEntity *activator ); + void Event_TurnOff( void ); + +private: + float shakeTime; + float shakeAmplitude; + idForce_Field forceField; + idClipModel *cm; +}; + +class hhTarget_SetLightParm : public idTarget { +public: + CLASS_PROTOTYPE( hhTarget_SetLightParm ); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_PlayWeaponAnim : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_PlayWeaponAnim); + +protected: + void Event_Trigger( idEntity *activator ); +}; + +class hhTarget_ControlVehicle : public idTarget { + CLASS_PROTOTYPE( hhTarget_ControlVehicle ); + +public: + void Spawn(); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_AttachToRail : public idTarget { + CLASS_PROTOTYPE( hhTarget_AttachToRail ); + +public: + void Spawn(); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_EnableReactions : public idTarget { + CLASS_PROTOTYPE( hhTarget_EnableReactions ); + +public: + void Spawn(); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_DisableReactions : public idTarget { + CLASS_PROTOTYPE( hhTarget_DisableReactions ); + +public: + void Spawn(); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_EnablePassageway : public idTarget { + CLASS_PROTOTYPE( hhTarget_EnablePassageway ); + +public: + void Spawn(); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_DisablePassageway : public idTarget { + CLASS_PROTOTYPE( hhTarget_DisablePassageway ); + +public: + void Spawn(); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_PatternRelay : public idTarget { + CLASS_PROTOTYPE( hhTarget_PatternRelay ); + +public: + void Spawn(); + void Save( idSaveGame *savefile ) const { savefile->WriteFloat(timeGranularity); } + void Restore( idRestoreGame *savefile ) { savefile->ReadFloat(timeGranularity); } + +protected: + void Event_Activate( idEntity *activator ); + void Event_TriggerTargets(); + + float timeGranularity; +}; + +class hhTarget_Subtitle : public idTarget { + CLASS_PROTOTYPE( hhTarget_Subtitle ); + +public: + void Spawn(); + +protected: + void Event_Activate( idEntity *activator ); + void Event_FadeOutText(); +}; + + +class hhTarget_EndLevel : public idTarget_EndLevel { + CLASS_PROTOTYPE( hhTarget_EndLevel ); + +protected: + virtual void Event_Activate(idEntity *activator); +}; + + +class hhTarget_ConsolidatePlayers : public idTarget { + CLASS_PROTOTYPE( hhTarget_ConsolidatePlayers ); + +protected: + void Event_Activate( idEntity *activator ); +}; + + +class hhTarget_WarpPlayers : public idTarget { + CLASS_PROTOTYPE( hhTarget_WarpPlayers ); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_FollowPath : public idTarget { + CLASS_PROTOTYPE( hhTarget_FollowPath ); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_LockDoor: public idTarget { +public: + CLASS_PROTOTYPE( hhTarget_LockDoor ); + +private: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_DisplayGui : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_DisplayGui); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_Autosave : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_Autosave); + +protected: + void Event_Activate( idEntity *activator ); + void Event_Autosave( void ); + void Event_FinishedSave( void ); +}; + +class hhTarget_Show : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_Show); + +protected: + void Event_Activate( idEntity *activator ); +}; + +class hhTarget_Hide : public idTarget { +public: + CLASS_PROTOTYPE(hhTarget_Hide); + +protected: + void Event_Activate( idEntity *activator ); +}; + +#endif /* __GAME_TARGETS_H__ */ diff --git a/src/Prey/game_trackmover.cpp b/src/Prey/game_trackmover.cpp new file mode 100644 index 0000000..510f212 --- /dev/null +++ b/src/Prey/game_trackmover.cpp @@ -0,0 +1,172 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_StartNextMove( "", NULL ); +const idEventDef EV_Kill( "" ); + +CLASS_DECLARATION(idMover, hhTrackMover) + EVENT( EV_PostSpawn, hhTrackMover::Event_PostSpawn ) + EVENT( EV_StartNextMove, hhTrackMover::Event_StartNextMove ) + EVENT( EV_Activate, hhTrackMover::Event_Activate ) + EVENT( EV_Deactivate, hhTrackMover::Event_Deactivate ) + EVENT( EV_Kill, hhTrackMover::Event_Kill ) +END_CLASS + +hhTrackMover::hhTrackMover() { + currentNode = NULL; +} + +void hhTrackMover::Spawn( void ) { + timeBetweenMoves = spawnArgs.GetInt("moveDelayMS"); + Hide(); + GetPhysics()->SetContents( 0 ); + fl.takedamage = false; + PostEventMS(&EV_PostSpawn, 0); +} + +// Return the next hhTrackNode to move to based on current player position +idEntity *hhTrackMover::GetNextDestination( void ) { + + idEntity *node, *minNode; + float distToPlayerSquared, minDistToPlayerSquared; + + if ( !currentNode ) { + return NULL; + } + + // Get player closest to currentNode + hhPlayer *player = NULL; + float bestDistSqr = idMath::INFINITY; + idVec3 origin = currentNode->GetPhysics()->GetOrigin(); + for ( int i = 0; i < MAX_CLIENTS ; i++ ) { + idEntity *ent = gameLocal.entities[ i ]; + if (ent && ent->IsType(hhPlayer::Type)) { + idVec3 toEnt = ent->GetPhysics()->GetOrigin() - origin; + float distSqr = toEnt.LengthSqr(); + if (distSqr < bestDistSqr) { + bestDistSqr = distSqr; + player = static_cast(ent); + } + } + } + if ( !player ) { + return NULL; + } + + // Find closest adjacent node to player + minNode = currentNode; + minDistToPlayerSquared = (player->GetPhysics()->GetOrigin() - currentNode->GetPhysics()->GetOrigin()).LengthSqr(); + for (int t=0; ttargets.Num(); t++) { + node = currentNode->targets[t].GetEntity(); + if (node) { + idVec3 toPlayer = player->GetPhysics()->GetOrigin() - node->GetPhysics()->GetOrigin(); + distToPlayerSquared = toPlayer.LengthSqr(); + if (distToPlayerSquared < minDistToPlayerSquared) { + minDistToPlayerSquared = distToPlayerSquared; + minNode = node; + + if(p_mountedGunDebug.GetInteger()) { + gameRenderWorld->DebugLine(colorGreen, currentNode->GetPhysics()->GetOrigin(), node->GetPhysics()->GetOrigin(), 2000, false); + } + } + else { + if(p_mountedGunDebug.GetInteger()) { + gameRenderWorld->DebugLine(colorYellow, currentNode->GetPhysics()->GetOrigin(), node->GetPhysics()->GetOrigin(), 2000, false); + } + } + } + } + + return minNode; +} + +void hhTrackMover::DoneMoving( void ) { + if( state == StateGlobal ) { + idMover::DoneMoving(); + PostEventMS( &EV_StartNextMove, 0); + } +} + +void hhTrackMover::Event_PostSpawn() { + bool activate; + spawnArgs.GetBool("autoactivate", "0", activate); + + if( !targets.Num() || !targets[0].IsValid() ) { + gameLocal.Error("hhTrackMover %s doesn't have a target tracknode\n", name.c_str()); + } + + currentNode = targets[0].GetEntity(); + dest_position = GetLocalCoordinates(currentNode->GetPhysics()->GetOrigin()); + SetOrigin( dest_position ); + + if (activate) { + state = StateGlobal; + PostEventMS( &EV_StartNextMove, 1000 ); + } else { + state = StateIdle; + } +} + +void hhTrackMover::Event_Activate( idEntity *activator ) { + if( state == StateIdle ) { + PostEventMS( &EV_StartNextMove, 0 ); + state = StateGlobal; + } +} + +void hhTrackMover::Event_Deactivate() { + if( state == StateGlobal ) { + state = StateIdle; + + DoneMoving(); + DoneRotating(); + CancelEvents(&EV_StartNextMove); + + } +} + +void hhTrackMover::Event_StartNextMove() { + idAngles Angles; + idVec3 Origin; + idEntity *ent = GetNextDestination(); + + if (ent && ent != currentNode) { + + Origin = GetLocalCoordinates( ent->GetPhysics()->GetOrigin() ); + if( !GetPhysics()->GetOrigin().Compare(Origin) ) { + dest_position = Origin; + BeginMove( NULL ); + } + + Angles = ent->GetPhysics()->GetAxis().ToAngles(); + if( !GetPhysics()->GetAxis().ToAngles().Compare(Angles) ) { + dest_angles = Angles; + BeginRotation( NULL, true ); + } + + currentNode = ent; + } else { + PostEventMS( &EV_StartNextMove, timeBetweenMoves ); + } +} + +void hhTrackMover::Event_Kill() { + idMover::DoneMoving(); + idMover::DoneRotating(); + CancelEvents(&EV_StartNextMove); + state = StateDead; +} + +void hhTrackMover::Save( idSaveGame *savefile ) const { + savefile->WriteInt( state ); + savefile->WriteInt( timeBetweenMoves ); + savefile->WriteObject( currentNode ); +} + +void hhTrackMover::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( reinterpret_cast ( state ) ); + savefile->ReadInt( timeBetweenMoves ); + savefile->ReadObject( reinterpret_cast ( currentNode ) ); +} diff --git a/src/Prey/game_trackmover.h b/src/Prey/game_trackmover.h new file mode 100644 index 0000000..a5eff64 --- /dev/null +++ b/src/Prey/game_trackmover.h @@ -0,0 +1,33 @@ +#ifndef __GAME_TRACKMOVER_H__ +#define __GAME_TRACKMOVER_H__ + + +class hhTrackMover : public idMover { + CLASS_PROTOTYPE( hhTrackMover ); +public: + hhTrackMover(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + idEntity * GetNextDestination( void ); + void DoneMoving( void ); + +protected: + enum States { + StateGlobal = 0, + StateIdle, + StateDead + } state; + + int timeBetweenMoves; + idEntity * currentNode; // Current node of track + + void Event_Activate( idEntity *activator ); + void Event_Deactivate(); + void Event_PostSpawn( void ); + void Event_StartNextMove( void ); + void Event_Kill(); +}; + + +#endif /* __GAME_TRACKMOVER_H__ */ diff --git a/src/Prey/game_trigger.cpp b/src/Prey/game_trigger.cpp new file mode 100644 index 0000000..f9a2215 --- /dev/null +++ b/src/Prey/game_trigger.cpp @@ -0,0 +1,1482 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/********************************************************************** + +hhFuncParmAccessor + +**********************************************************************/ + +CLASS_DECLARATION( idClass, hhFuncParmAccessor ) +END_CLASS + +/* +================ +hhFuncParmAccessor::hhFuncParmAccessor +================ +*/ +hhFuncParmAccessor::hhFuncParmAccessor() { + function = NULL; +} + +/* +================ +hhFuncParmAccessor::hhFuncParmAccessor +================ +*/ +hhFuncParmAccessor::hhFuncParmAccessor( const hhFuncParmAccessor* accessor ) { + assert( accessor ); + + SetInfo( accessor->GetReturnKey(), accessor->GetFunction(), accessor->GetParms() ); +} + +/* +================ +hhFuncParmAccessor::hhFuncParmAccessor +================ +*/ +hhFuncParmAccessor::hhFuncParmAccessor( const hhFuncParmAccessor& accessor ) { + SetInfo( accessor.GetReturnKey(), accessor.GetFunction(), accessor.GetParms() ); +} + +/* +================ +hhFuncParmAccessor::SetInfo +================ +*/ +void hhFuncParmAccessor::SetInfo( const char* returnKey, const function_t* func, const idList& parms ) { + SetFunction( func ); + SetParms( parms ); + SetReturnKey( returnKey ); + + Verify(); +} + +void hhFuncParmAccessor::InsertParm_String( const char *text, int index ) { + InsertParm( text, index ); +} + +void hhFuncParmAccessor::InsertParm_Float( float value, int index ) { + InsertParm( va("%.2f", value), index ); +} + +void hhFuncParmAccessor::InsertParm_Int( int value, int index ) { + InsertParm( va("%d", value), index ); +} + +void hhFuncParmAccessor::InsertParm_Vector( const idVec3 &vec, int index ) { + InsertParm( va("'%.2f %.2f %.2f'", vec[0], vec[1], vec[2]), index ); +} + +void hhFuncParmAccessor::InsertParm_Entity( const idEntity *ent, int index ) { + if ( ent ) { + InsertParm( ent->GetName(), index ); + } +} + +void hhFuncParmAccessor::SetParm_Entity( const idEntity *ent, int index ) { + if ( ent ) { + parms.AssureSize(index+1); + parms[index] = ent->GetName(); + } +} + +void hhFuncParmAccessor::SetParm_String( const char *str, int index ) { + if ( str ) { + parms.AssureSize(index+1); + parms[index] = str; + } +} + +void hhFuncParmAccessor::InsertParm( const char *text, int index ) { + parms.Insert( text, index ); +} + +/* +================ +hhFuncParmAccessor::ParseFunctionKeyValue +================ +*/ +void hhFuncParmAccessor::ParseFunctionKeyValue( const char* value ) { + if( !value || !value[0] ) { + return; + } + + hhUtils::SplitString( idStr(value), GetParms(), ' ' ); + ParseFunctionKeyValue( GetParms() ); +} + +/* +================ +hhFuncParmAccessor::ParseFunctionKeyValue +================ +*/ +void hhFuncParmAccessor::ParseFunctionKeyValue( idList& valueList ) { + SetFunction( FindFunction(valueList[0]) ); + if( GetFunction() ) { + valueList.RemoveIndex( 0 );//Remove function name + } else { + gameLocal.Warning("hhFuncParmAccessor: Function %s did not exist", (const char*)valueList[0]); + SetReturnKey( valueList[0] ); + valueList.RemoveIndex( 0 );//Remove return key + SetFunction( FindFunction(valueList[0]) ); + valueList.RemoveIndex( 0 );//Remove function name + } +} + +/* +================ +hhFuncParmAccessor::FindFunction +================ +*/ +function_t* hhFuncParmAccessor::FindFunction( const char* funcname ) { + function_t* func = NULL; + + if( funcname && *funcname ) { + func = gameLocal.program.FindFunction( funcname ); + } + + return func; +} + +/* +================ +hhFuncParmAccessor::CallFunction +================ +*/ +void hhFuncParmAccessor::CallFunction( idDict& returnDict ) { + if( !GetFunction() ) { + return; + } + + hhThread* scriptThread = new hhThread; + if( !scriptThread ) { + return; + } + + scriptThread->ClearStack(); + if( !scriptThread->ParseAndPushArgsOntoStack(GetParms(), GetFunction()) ) { + SAFE_DELETE_PTR( scriptThread ); //This is needed because we won't be removed by Execute + return; + } + + scriptThread->CallFunction( GetFunction(), false ); + + scriptThread->Execute(); + + idTypeDef* returnType = GetReturnType(); + if( !GetReturnKey() || !GetReturnKey()[0] ) { + return; + } + + if( returnType == &type_void ) { + gameLocal.Warning( "Return key assigned for function (%s) that returns void\n", GetFunctionName() ); + return; + } + + returnDict.Set( GetReturnKey(), returnType->GetReturnValueAsString(gameLocal.program) ); +} + +/* +================ +hhFuncParmAccessor::Verify +================ +*/ +void hhFuncParmAccessor::Verify() { + if( !GetFunction() ) { + return; + } + + int numParms = GetFunction()->def->TypeDef()->NumParameters(); + const char* parm = NULL; + idTypeDef* parmType = NULL; + + if( numParms != GetParms().Num() ) { + gameLocal.Warning( "Wrong # of parms passed for function %s", GetFunctionName() ); + return; + } + + if( GetReturnType() == &type_void && GetReturnKey() && GetReturnKey()[0] ) { + gameLocal.Warning( "Used Return key %s for function %s that returns void", GetReturnKey(), GetFunctionName() ); + return; + } + + for( int ix = 0; ix < numParms; ++ix ) { + parmType = GetParmType( ix ); + parm = GetParm( ix ); + if( !parmType->VerifyData(parm) ) { + gameLocal.Warning( "%s is not Type %s", parm, parmType->Name() ); + return; + } + } +} + +/* +================ +hhFuncParmAccessor::GetFunctionName +================ +*/ +const char* hhFuncParmAccessor::GetFunctionName() const { + return GetFunction()->Name(); +} + +/* +================ +hhFuncParmAccessor::GetFunction +================ +*/ +const function_t* hhFuncParmAccessor::GetFunction() const { + return function; +} + +/* +================ +hhFuncParmAccessor::GetReturnKey +================ +*/ +const char* hhFuncParmAccessor::GetReturnKey() const { + return returnKey.c_str(); +} + +/* +================ +hhFuncParmAccessor::GetReturnType +================ +*/ +idTypeDef* hhFuncParmAccessor::GetReturnType() const { + return GetFunction()->def->TypeDef()->ReturnType(); +} + +/* +================ +hhFuncParmAccessor::GetParm +================ +*/ +const char* hhFuncParmAccessor::GetParm( int index ) const { + return parms[ index ]; +} + +/* +================ +hhFuncParmAccessor::GetParmType +================ +*/ +idTypeDef* hhFuncParmAccessor::GetParmType( int index ) const { + return GetFunction()->def->TypeDef()->GetParmType( index ); +} + +/* +================ +hhFuncParmAccessor::GetParms +================ +*/ +const idList& hhFuncParmAccessor::GetParms() const { + return parms; +} + +/* +================ +hhFuncParmAccessor::GetParms +================ +*/ +idList& hhFuncParmAccessor::GetParms() { + return parms; +} + +/* +================ +hhFuncParmAccessor::SetFunction +================ +*/ +void hhFuncParmAccessor::SetFunction( const function_t* func ) { + function = func; +} + +/* +================ +hhFuncParmAccessor::SetParms +================ +*/ +void hhFuncParmAccessor::SetParms( const idList& parms ) { + this->parms = parms; +} + +/* +================ +hhFuncParmAccessor::SetReturnKey +================ +*/ +void hhFuncParmAccessor::SetReturnKey( const char* key ) { + returnKey = key; +} + +/* +================ +hhFuncParmAccessor::Save +================ +*/ +void hhFuncParmAccessor::Save( idSaveGame *savefile ) const { + int num = parms.Num(); + savefile->WriteInt( num ); + for( int i = 0; i < num; i++ ) { + savefile->WriteString( parms[i] ); + } + + savefile->WriteString( returnKey ); + savefile->WriteString( function ? function->Name() : "" ); +} + +/* +================ +hhFuncParmAccessor::Restore +================ +*/ +void hhFuncParmAccessor::Restore( idRestoreGame *savefile ) { + int num; + savefile->ReadInt( num ); + idStr tmp; + parms.Clear(); + for( int i = 0; i < num; i++ ) { + savefile->ReadString( tmp ); + parms.Append( tmp ); + } + + savefile->ReadString( returnKey ); + savefile->ReadString( tmp ); + if( tmp.Length() > 0 ) { + function = FindFunction( tmp ); + HH_ASSERT( function ); + } else { + function = NULL; + } +} + +/********************************************************************** + +hhTrigger + +**********************************************************************/ + +const idEventDef EV_Trigger("", "e"); +const idEventDef EV_Retrigger("", "e"); +const idEventDef EV_UnTrigger("", NULL); +const idEventDef EV_PollForUntouch("", NULL); + +CLASS_DECLARATION( idEntity, hhTrigger ) + EVENT( EV_Enable, hhTrigger::Event_Enable ) + EVENT( EV_Disable, hhTrigger::Event_Disable ) + EVENT( EV_Activate, hhTrigger::Event_Activate) + EVENT( EV_Deactivate, hhTrigger::Event_Deactivate) + EVENT( EV_Touch, hhTrigger::Event_Touch) + EVENT( EV_Trigger, hhTrigger::Event_TriggerAction ) + EVENT( EV_Retrigger, hhTrigger::Event_Retrigger ) + EVENT( EV_UnTrigger, hhTrigger::Event_UnTriggerAction ) + EVENT( EV_PollForUntouch, hhTrigger::Event_PollForUntouch ) + EVENT( EV_PostSpawn, hhTrigger::Event_PostSpawn ) +END_CLASS + +/* +================ +hhTrigger::Activate + +the trigger was just activated by activator +================ +*/ +void hhTrigger::Activate( idEntity *activator ) { + int triggerDelay = 0; + + unTriggerActivator = activator; + + // if not enabled, return + if( !bEnabled ) { + return; + } + + bActive = true; + + if ( nextTriggerTime > gameLocal.time ) { + // can't retrigger until the wait is over + return; + } + + // see if this trigger requires an item + if ( !gameLocal.RequirementMet( activator, requires, removeItem ) ) { + return; + } + + if ( delay > 0 ) { + // don't allow it to trigger again until our delay has passed + triggerDelay = hhMath::hhMax( 0, SEC2MS(delay + randomDelay * gameLocal.random.CRandomFloat()) ); + nextTriggerTime = gameLocal.time + triggerDelay + 1; + PostEventMS( &EV_Trigger, triggerDelay, activator ); + } else { + //Changed so we check to see if the activator is still with in the trigger volume + ProcessEvent( &EV_Trigger, activator ); + } +} + +/* +================ +hhTrigger::Spawn +================ +*/ +void hhTrigger::Spawn(void) { + + unTriggerActivator=NULL; + bActive=false; + + spawnArgs.GetFloat( "wait", "0.5", wait ); + spawnArgs.GetFloat( "random", "0", random ); + spawnArgs.GetFloat( "delay", "0", delay ); + spawnArgs.GetFloat( "randomeDelay", "0", randomDelay ); + spawnArgs.GetString( "requires", "", requires ); + spawnArgs.GetInt( "removeItem", "0", removeItem ); + triggerBehavior = (triggerBehavior_t)spawnArgs.GetInt( "triggerBehavior" ); + spawnArgs.GetBool( "noTouch", "0", noTouch ); + spawnArgs.GetFloat( "uncalldelay", "0.2", untouchGranularity ); + spawnArgs.GetFloat( "refire", "-1", refire ); + spawnArgs.GetBool( "enabled", "1", initiallyEnabled ); + spawnArgs.GetBool( "untrigger", "0", bUntrigger ); + spawnArgs.GetBool( "always_trigger", "0", alwaysTrigger ); + spawnArgs.GetBool( "isSimpleBox", "1", isSimpleBox ); + spawnArgs.GetBool( "noVehicles", "0", bNoVehicles ); + + GetTriggerClasses( spawnArgs ); + + nextTriggerTime = 0; + + PostEventMS( (initiallyEnabled) ? &EV_Enable : &EV_Disable, 0); + + PostEventMS( &EV_PostSpawn, 0 ); +} + +/* +================ +hhTrigger::Save +================ +*/ +void hhTrigger::Save( idSaveGame *savefile ) const { + savefile->WriteBool( bActive ); + savefile->WriteBool( bEnabled ); + savefile->WriteFloat( untouchGranularity ); + savefile->WriteFloat( wait ); + savefile->WriteFloat( random ); + savefile->WriteFloat( delay ); + savefile->WriteFloat( randomDelay ); + savefile->WriteFloat( refire ); + savefile->WriteInt( nextTriggerTime ); + savefile->WriteBool( alwaysTrigger ); + savefile->WriteBool( isSimpleBox ); + savefile->WriteBool( bNoVehicles ); + savefile->WriteStaticObject( funcInfo ); + savefile->WriteStaticObject( unfuncInfo ); + savefile->WriteStaticObject( funcRefInfo ); + savefile->WriteStaticObject( unfuncRefInfo ); + savefile->WriteStaticObject( funcRefActivatorInfo ); + savefile->WriteStaticObject( unfuncRefActivatorInfo ); + savefile->WriteString( requires ); + savefile->WriteInt( removeItem ); + savefile->WriteBool( noTouch ); + savefile->WriteBool( initiallyEnabled ); + savefile->WriteBool( bUntrigger ); + + unTriggerActivator.Save( savefile ); + + savefile->WriteStringList( TriggerClasses ); + savefile->WriteInt( triggerBehavior ); +} + +/* +================ +hhTrigger::Restore +================ +*/ +void hhTrigger::Restore( idRestoreGame *savefile ) { + savefile->ReadBool( bActive ); + savefile->ReadBool( bEnabled ); + savefile->ReadFloat( untouchGranularity ); + savefile->ReadFloat( wait ); + savefile->ReadFloat( random ); + savefile->ReadFloat( delay ); + savefile->ReadFloat( randomDelay ); + savefile->ReadFloat( refire ); + savefile->ReadInt( nextTriggerTime ); + savefile->ReadBool( alwaysTrigger ); + savefile->ReadBool( isSimpleBox ); + savefile->ReadBool( bNoVehicles ); + savefile->ReadStaticObject( funcInfo ); + savefile->ReadStaticObject( unfuncInfo ); + savefile->ReadStaticObject( funcRefInfo ); + savefile->ReadStaticObject( unfuncRefInfo ); + savefile->ReadStaticObject( funcRefActivatorInfo ); + savefile->ReadStaticObject( unfuncRefActivatorInfo ); + savefile->ReadString( requires ); + savefile->ReadInt( removeItem ); + savefile->ReadBool( noTouch ); + savefile->ReadBool( initiallyEnabled ); + savefile->ReadBool( bUntrigger ); + + unTriggerActivator.Restore( savefile ); + + savefile->ReadStringList( TriggerClasses ); + savefile->ReadInt( reinterpret_cast ( triggerBehavior ) ); +} + +/* +================ +hhTrigger::CallFunctions +================ +*/ +void hhTrigger::CallFunctions( idEntity *activator ) { + funcInfo.CallFunction( spawnArgs ); + funcRefInfo.CallFunction( spawnArgs ); + + funcRefActivatorInfo.InsertParm_Entity( activator, 1 );//Needs to be second parm. After self + funcRefActivatorInfo.CallFunction( spawnArgs ); +} + +/* +================ +hhTrigger::UncallFunctions +================ +*/ +void hhTrigger::UncallFunctions( idEntity *activator ) { + unfuncInfo.CallFunction( spawnArgs ); + unfuncRefInfo.CallFunction( spawnArgs ); + + unfuncRefActivatorInfo.InsertParm_Entity( activator, 1 );//Needs to be second parm. After self + unfuncRefActivatorInfo.CallFunction( spawnArgs ); +} + +/* +================ +hhTrigger::TriggerAction +================ +*/ +void hhTrigger::TriggerAction( idEntity *activator ) { + + // Activate all targets + ActivateTargets( activator ); + + CallFunctions( activator ); + + if ( wait >= 0 ) { + nextTriggerTime = gameLocal.time + SEC2MS( wait + random * gameLocal.random.CRandomFloat() ); + } else { + nextTriggerTime = gameLocal.time + 1; + PostEventMS( &EV_Disable, 0 ); + } + + // Handle refire + if (refire > 0) { + PostEventSec( &EV_Retrigger, refire + random * gameLocal.random.CRandomFloat(), activator); + } +} + +/* +================ +hhTrigger::UnTriggerAction +================ +*/ +void hhTrigger::UnTriggerAction() { + if( bUntrigger ) { + ActivateTargets( unTriggerActivator.GetEntity() ); + } + + UncallFunctions( unTriggerActivator.GetEntity() ); +} + +/* +=============== +hhTrigger::IsEncroaching +=============== +*/ +bool hhTrigger::IsEncroaching( const idEntity* entity ) { + + // nla - Simplified checks to bounds when possible. + // Error was due to contents of an idActivator not hitting anything here. (it has contents 0) + // But since the collide logic has been done elsewhere, we can simplify this to just a is this in this check. + if (isSimpleBox) { + // num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), entity->GetPhysics()->GetContents(), touch, MAX_GENTITIES ); + if ( GetPhysics()->GetAbsBounds().IntersectsBounds( entity->GetPhysics()->GetAbsBounds() ) ) { + return( true ); + } + } + else { + //num = hhUtils::EntitiesTouchingClipmodel( GetPhysics()->GetClipModel(), touch, MAX_GENTITIES, entity->GetPhysics()->GetContents() ); + if ( entity->GetPhysics()->ClipContents( GetPhysics()->GetClipModel() ) ) { + return( true ); + } + } + + return false; +} + +/* +=============== +hhTrigger::IsEncroached +=============== +*/ +bool hhTrigger::IsEncroached() { + idEntity *touch[ MAX_GENTITIES ]; + int num; + + if (isSimpleBox) { + num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), MASK_SHOT_BOUNDINGBOX, touch, MAX_GENTITIES ); + } + else { + num = hhUtils::EntitiesTouchingClipmodel( GetPhysics()->GetClipModel(), touch, MAX_GENTITIES ); + } + + for (int i = 0; i < num; i++ ) { + if( touch[ i ] == this ) { + continue; + } + + if( CheckUnTriggerClass(touch[ i ]) ) { + return true; + } + } + return false; +} + +/* +=============== +hhTrigger::SetTriggerClasses +=============== +*/ +void hhTrigger::SetTriggerClasses( idList& list ) { + TriggerClasses.Clear(); + + TriggerClasses.SetNum(list.Num()); + + for(int iIndex = 0; iIndex < list.Num(); ++iIndex) { + TriggerClasses[iIndex] = list[iIndex]; + } +} + +/* +=============== +hhTrigger::CheckTriggerClass +=============== +*/ +bool hhTrigger::CheckTriggerClass( idEntity* activator ) { + if( !activator ) { + return false; + } + + if(!TriggerClasses.Num() || triggerBehavior == TB_ANY) { + return true; + } + + + for(int iIndex = 0; iIndex < TriggerClasses.Num(); ++iIndex) { + //Look for exact match then try for prefix match + if( !idStr(activator->spawnArgs.GetString("classname")).Icmp(TriggerClasses[iIndex].c_str()) || + !idStr(activator->spawnArgs.GetString("classname")).IcmpPrefix(TriggerClasses[iIndex].c_str()) ) { + + if( activator->IsType(hhPlayer::Type) ) {//Player needs client + hhPlayer* player = static_cast(activator); + if ( player->noclip || !player->ShouldTouchTrigger(this) ) { + return false; + } + if ( bNoVehicles && player->InVehicle() ) { + return false; + } + } + if( activator->IsType(idActor::Type) ) { // Dead monsters shouldn't keep refires going + if (activator->health <= 0) { + return false; + } + } + return true; + } + } + + return false; +} + +/* +=============== +hhTrigger::CheckUnTriggerClass + +Helper function used for overriding +=============== +*/ +bool hhTrigger::CheckUnTriggerClass(idEntity* activator) { + return CheckTriggerClass(activator); +} + +/* +=============== +hhTrigger::GetTriggerClasses +=============== +*/ +void hhTrigger::GetTriggerClasses(idDict& Args) { + const idKeyValue* pKeyValue = NULL; + int iNumKeyValues = Args.GetNumKeyVals(); + + TriggerClasses.Clear(); + + if(triggerBehavior == TB_PLAYER_ONLY) { + TriggerClasses.AddUnique( "player" ); + + } else if(triggerBehavior == TB_FRIENDLIES_ONLY) { + TriggerClasses.AddUnique( "player" ); + TriggerClasses.AddUnique( "character" ); + + } else if(triggerBehavior == TB_MONSTERS_ONLY) { + TriggerClasses.AddUnique( "monster" ); + + } else if(triggerBehavior == TB_PLAYER_MONSTERS_FRIENDLIES) { + TriggerClasses.AddUnique( "player" ); + TriggerClasses.AddUnique( "monster" ); + TriggerClasses.AddUnique( "character" ); + + } else if(triggerBehavior == TB_SPECIFIC_ENTITIES){ + for( int iIndex = 0; iIndex < iNumKeyValues; ++iIndex ) { + pKeyValue = Args.GetKeyVal( iIndex ); + if ( !pKeyValue->GetKey().Cmpn( "trigger_class", 13 ) ) { + TriggerClasses.AddUnique( pKeyValue->GetValue() ); + } + } + } +} + +/* +=============== +hhTrigger::Enable +=============== +*/ +void hhTrigger::Enable() { + GetPhysics()->SetContents( CONTENTS_TRIGGER ); + GetPhysics()->EnableClip(); + + bEnabled = true; +} + +/* +=============== +hhTrigger::Disable +=============== +*/ +void hhTrigger::Disable() { + // we may be relinked if we're bound to another object, so clear the contents as well + GetPhysics()->SetContents( 0 ); + GetPhysics()->DisableClip(); + + CancelEvents( &EV_Retrigger ); + bEnabled = false; +} + +/* +=============== +hhTrigger::Event_PostSpawn +=============== +*/ +void hhTrigger::Event_PostSpawn() { + funcInfo.ParseFunctionKeyValue( spawnArgs.GetString("call") ); + funcInfo.Verify(); + unfuncInfo.ParseFunctionKeyValue( spawnArgs.GetString("uncall") ); + unfuncInfo.Verify(); + + funcRefInfo.ParseFunctionKeyValue( spawnArgs.GetString("callRef") ); + funcRefInfo.InsertParm_Entity( this, 0 ); + funcRefInfo.Verify(); + + unfuncRefInfo.ParseFunctionKeyValue( spawnArgs.GetString("uncallRef") ); + unfuncRefInfo.InsertParm_Entity( this, 0 ); + unfuncRefInfo.Verify(); + + funcRefActivatorInfo.ParseFunctionKeyValue( spawnArgs.GetString("callRefActivator") ); + funcRefActivatorInfo.InsertParm_Entity( this, 0 ); + // NOTE: Activator parm inserted at time of function call, therefore don't verify now + + unfuncRefActivatorInfo.ParseFunctionKeyValue( spawnArgs.GetString("uncallRefActivator") ); + unfuncRefActivatorInfo.InsertParm_Entity( this, 0 ); + // NOTE: Activator parm inserted at time of function call, therefore don't verify now +} + +/* +=============== +hhTrigger::Event_PollForUntouch +=============== +*/ +void hhTrigger::Event_PollForUntouch() { + if (!IsEncroached()) { + CancelEvents(&EV_PollForUntouch); + PostEventMS(&EV_Deactivate, 0); + } + else { + PostEventSec(&EV_PollForUntouch, untouchGranularity); + } +} + +/* +=============== +hhTrigger::Event_Touch +=============== +*/ +void hhTrigger::Event_Touch( idEntity *other, trace_t *trace ) { + + if( noTouch || !CheckTriggerClass(other) ) { + return; + } + + if( !IsActive() ) { + Activate( other ); + + // If this trigger uses any of the unTrigger mechanisms, start polling + if (bUntrigger || refire || unfuncInfo.GetFunction() || unfuncRefInfo.GetFunction() || unfuncRefActivatorInfo.GetFunction()) { + PostEventSec(&EV_PollForUntouch, untouchGranularity); + } + } + // If we have already triggered the first time, but should always trigger + else if ( alwaysTrigger ) { + Activate( other ); + } +} + +/* +================ +hhTrigger::Retrigger +================ +*/ +void hhTrigger::Event_Retrigger( idEntity *activator ) { + //AOB - needed to stop infinite retrigger when triggered from gui + //rww - activator can be null if removed before event is serviced. actually, isn't this a bad idea? maybe it's better to use an index. + if (!activator || (!noTouch && !IsEncroaching(activator)) ) { + CancelEvents( &EV_Retrigger ); + PostEventMS( &EV_Deactivate, 0 ); + return; + } + + // This is seperate event so we can cull them seperately from normal trigger events + + TriggerAction( activator ); +} + +/* +================ +hhTrigger::Event_UnTriggerAction +================ +*/ +void hhTrigger::Event_UnTriggerAction() { + UnTriggerAction(); +} + +/* +================ +hhTrigger::Event_TriggerAction +================ +*/ +void hhTrigger::Event_TriggerAction( idEntity *activator ) { + //HUMANHEAD rww - we are running into a null activator situation when the spirit proxy for the player runs into a trigger, + //and then the player switches back to his body and removes the proxy before the trigger fires (due to a relay/delay/whatever), + //as well as possibly any other situation where a delayed trigger is activated by an entity that gets removed before firing. + //it has been decided that in these situations, the world should be used as the activator. + if (!activator) { + activator = gameLocal.world; + } + //HUMANHEAD END + // Added noTouch && !IsEncroached to fix the issue with retriggered hurt constantly damaging the player, even when they weren't in it. + // nla - Added check to allow 'delayed' triggers to function when you weren't in them. + if (!noTouch && !IsEncroaching(activator) && !delay ) { + CancelEvents( &EV_Retrigger ); + PostEventMS( &EV_Deactivate, 0 ); + return; + } + + TriggerAction( activator ); +} + +/* +================ +hhTrigger::Event_Enable +================ +*/ +void hhTrigger::Event_Enable( void ) { + Enable(); +} + +/* +================ +hhTrigger::Event_Disable +================ +*/ +void hhTrigger::Event_Disable( void ) { + Disable(); +} + +/* +================ +hhTrigger::Event_Activate +================ +*/ +void hhTrigger::Event_Activate( idEntity *activator ) { + Activate( activator ); +} + +/* +================ +hhTrigger::Event_Deactivate +================ +*/ +void hhTrigger::Event_Deactivate() { + CancelEvents( &EV_Retrigger ); + PostEventMS( &EV_UnTrigger, 0 ); + bActive=false; +} + +/*********************************************************************** + +hhDamageTrigger + +***********************************************************************/ + +CLASS_DECLARATION( hhTrigger, hhDamageTrigger ) + EVENT( EV_Enable, hhDamageTrigger::Event_Enable ) + EVENT( EV_Disable, hhDamageTrigger::Event_Disable ) +END_CLASS + + +void hhDamageTrigger::Spawn(void) { + funcRefInfoDamage.ParseFunctionKeyValue( spawnArgs.GetString("damageCallRef") ); + funcRefInfoDamage.InsertParm_Entity( this, 0 ); +} + +void hhDamageTrigger::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject( funcRefInfoDamage ); +} + +void hhDamageTrigger::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject( funcRefInfoDamage ); +} + +void hhDamageTrigger::Damage(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location) { + // Call script if requested with damageDef, used for spherebrain + if (funcRefInfoDamage.GetFunction() != NULL) { + funcRefInfoDamage.InsertParm_Entity( attacker, 1 ); + spawnArgs.Set( "last_damage", damageDefName ); + funcRefInfoDamage.CallFunction( spawnArgs ); + } + if ( spawnArgs.GetBool( "activate_targets" ) ) { + ActivateTargets( attacker ); + } + hhTrigger::Damage(inflictor, attacker, dir, damageDefName, damageScale, location); +} + +void hhDamageTrigger::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if (!CheckTriggerClass(attacker)) { + health += damage; + return; + } + + if ( gameLocal.time < nextTriggerTime ) { + health += damage; + return; + } + + Activate( attacker ); +} + +void hhDamageTrigger::Event_Enable() { + bEnabled = true; + fl.takedamage = true; + GetPhysics()->SetContents( CONTENTS_SHOOTABLE|CONTENTS_SHOOTABLEBYARROW ); +} + +void hhDamageTrigger::Event_Disable() { + GetPhysics()->SetContents( 0 ); + fl.takedamage = false; + bEnabled = false; +} + +/*********************************************************************** + +hhTriggerPain + +***********************************************************************/ + +const idEventDef EV_DamageBox("damageBox", "e"); + +CLASS_DECLARATION( hhTrigger, hhTriggerPain ) + EVENT( EV_Activate, hhTriggerPain::Event_Activate ) + EVENT( EV_DamageBox, hhTriggerPain::Event_DamageBox ) +END_CLASS + +/* +================ +hhTriggerPain + + Damages activator + Can be turned on or off by enable/disable + Fires events, triggers targets +================ +*/ +void hhTriggerPain::Spawn(void) { + spawnArgs.GetBool( "apply_impulse", "0", applyImpulse ); +} + +void hhTriggerPain::Event_DamageBox( idEntity *activator ) { + idEntity *touch[ MAX_GENTITIES ]; + int num; + + if (isSimpleBox) { + num = gameLocal.clip.EntitiesTouchingBounds( GetPhysics()->GetAbsBounds(), MASK_SHOT_BOUNDINGBOX, touch, MAX_GENTITIES ); + } + else { + num = hhUtils::EntitiesTouchingClipmodel( GetPhysics()->GetClipModel(), touch, MAX_GENTITIES ); + } + + for (int i = 0; i < num; i++ ) { + if( touch[ i ] == this ) { + continue; + } + + if( CheckTriggerClass( touch[i] ) ) { + touch[i]->Damage(activator, activator, vec3_origin, spawnArgs.GetString("def_damage"), 1.0f, INVALID_JOINT ); + } + } +} + +/* +================ +hhTriggerPain::TriggerAction +================ +*/ +void hhTriggerPain::TriggerAction(idEntity *activator) { + + if(activator) { + // If a player in a vehicle, apply pain to vehicle + if (activator->IsType(hhPlayer::Type)) { + hhPlayer *player = static_cast(activator); + if (player->InVehicle()) { + activator = player->GetVehicleInterface()->GetVehicle(); + } + } + + idVec3 Velocity = activator->GetPhysics()->GetLinearVelocity(); + + activator->Damage(NULL, NULL, -Velocity, spawnArgs.GetString("def_damage"), 1.0f, INVALID_JOINT ); + + if ( applyImpulse ) { +// activator->ApplyImpulse( this, 0, activator->GetPhysics()->GetOrigin(), -Velocity / 10.0 ); + activator->ApplyImpulse( this, 0, activator->GetPhysics()->GetOrigin(), -Velocity * (activator->GetPhysics()->GetMass()*1.5f) ); + } + + } + + // Handle default trigger behavior + hhTrigger::TriggerAction(activator); +} + +/* +================ +hhTriggerPain::Event_Activate +================ +*/ +void hhTriggerPain::Event_Activate( idEntity *activator ) { + //AOB - allow toggling +// if( IsEnabled() && (IsActive() || !IsEncroached()) ) { + if( IsEnabled() ) { + ProcessEvent( &EV_Disable ); + ProcessEvent( &EV_Deactivate ); + } else { + if( !IsEnabled() ) { + ProcessEvent( &EV_Enable ); + } + Activate( activator ); + } +} + +/* +================ +hhTriggerPain::Save +================ +*/ +void hhTriggerPain::Save( idSaveGame *savefile ) const { + savefile->WriteBool( applyImpulse ); +} + +/* +================ +hhTriggerPain::Restore +================ +*/ +void hhTriggerPain::Restore( idRestoreGame *savefile ) { + savefile->ReadBool( applyImpulse ); +} + +/*********************************************************************** + +hhTriggerEnabler + +***********************************************************************/ + +CLASS_DECLARATION( hhTrigger, hhTriggerEnabler ) +END_CLASS + +/* +================ +hhTriggerEnabler + + When it is entered, target is enabled + When it is exited, target is disabled. +================ +*/ +void hhTriggerEnabler::Spawn(void) { +} + +/* +================ +hhTriggerEnabler::TriggerAction +================ +*/ +void hhTriggerEnabler::TriggerAction(idEntity *activator) { + + // Enable all targets + for (int t=0; tPostEventMS( &EV_Enable, 0 ); + } + } + + // Handle default trigger behavior + hhTrigger::TriggerAction(activator); +} + + +/* +================ +hhTriggerEnabler::UnTriggerAction +================ +*/ +void hhTriggerEnabler::UnTriggerAction() { + + // Disable all targets + for (int t=0; tPostEventMS( &EV_Disable, 0 ); + } + } + + // Handle default trigger behavior + hhTrigger::UnTriggerAction(); +} + + +/*********************************************************************** + +hhTriggerSight + + Activates targets when seen +***********************************************************************/ + +CLASS_DECLARATION( hhTrigger, hhTriggerSight ) + EVENT( EV_Enable, hhTriggerSight::Event_Enable ) + EVENT( EV_Disable, hhTriggerSight::Event_Disable ) +END_CLASS + + +void hhTriggerSight::Spawn(void) { + BecomeActive(TH_THINK); +} + +void hhTriggerSight::Think(void) { + + if (thinkFlags & TH_THINK) { + hhPlayer *player = NULL; + for (int i = 0; i < gameLocal.numClients; i++ ) { + player = static_cast(gameLocal.entities[ i ]); + if ( !player ) { + continue; + } + + // if there is no way we can see this player + if ( !gameLocal.InPlayerPVS(this) ) { + continue; + } + + if (player->CanSee(this, true)) { + + // activate + PostEventMS(&EV_Activate, 0, player); + + // Automatically disables itself, re-enable for repeat + PostEventMS(&EV_Disable, 0); + } + } + } + + Present(); // Need this so it get's removed from active list +} + +void hhTriggerSight::Event_Enable( void ) { + bEnabled = true; + BecomeActive(TH_THINK); +} + +void hhTriggerSight::Event_Disable( void ) { + bEnabled = false; + BecomeInactive(TH_THINK); +} + +/* +================ +hhTriggerSight::Save +================ +*/ +void hhTriggerSight::Save( idSaveGame *savefile ) const { + savefile->WriteInt( pvsArea ); +} + +/* +================ +hhTriggerSight::Restore +================ +*/ +void hhTriggerSight::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( pvsArea ); + Spawn(); +} + +/* +=============================================================================== + + hhTrigger_Count + +=============================================================================== +*/ + +CLASS_DECLARATION( hhTrigger, hhTrigger_Count ) +END_CLASS + +/* +================ +hhTrigger_Count::Save +================ +*/ +void hhTrigger_Count::Save( idSaveGame *savefile ) const { + savefile->WriteInt( goal ); + savefile->WriteInt( count ); + savefile->WriteFloat( delay ); +} + +/* +================ +hhTrigger_Count::Restore +================ +*/ +void hhTrigger_Count::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( goal ); + savefile->ReadInt( count ); + savefile->ReadFloat( delay ); +} + +/* +================ +hhTrigger_Count::Spawn +================ +*/ +void hhTrigger_Count::Spawn( void ) { + spawnArgs.GetInt( "count", "1", goal ); + spawnArgs.GetFloat( "delay", "0", delay ); + count = 0; +} + +/* +================ +hhTrigger_Count::Activate +================ +*/ +void hhTrigger_Count::Activate( idEntity *activator ) { + int triggerDelay = 0; + + unTriggerActivator = activator; + + // if not enabled, return + if (!bEnabled) { + return; + } + + bActive=true; + + if ( nextTriggerTime > gameLocal.time ) { + // can't retrigger until the wait is over + return; + } + + // see if this trigger requires an item + if ( !gameLocal.RequirementMet( activator, requires, removeItem ) ) { + return; + } + + if ( delay >= 0 ) { + // goal of -1 means trigger has been exhausted + if (goal >= 0) { + count++; + if ( count >= goal ) { + if (spawnArgs.GetBool("repeat")) { + count = 0; + } else { + goal = -1; + } + + // don't allow it to trigger again until our delay has passed + triggerDelay = hhMath::hhMax( 0, SEC2MS(delay + randomDelay * gameLocal.random.CRandomFloat()) ); + nextTriggerTime = gameLocal.time + triggerDelay; + PostEventMS( &EV_Trigger, triggerDelay, activator ); + } + } + } +} + +/* +================ +hhTrigger_Count::TriggerAction +================ +*/ +void hhTrigger_Count::TriggerAction( idEntity *activator ) { + hhTrigger::TriggerAction( activator ); + + if (goal == -1) { + PostEventMS( &EV_Remove, 0 ); + } +} + + +/* +=============================================================================== + + hhTrigger_Event + +=============================================================================== +*/ + +CLASS_DECLARATION( hhTrigger, hhTrigger_Event ) +END_CLASS + +/* +================ +hhTrigger_Event::Spawn +================ +*/ +void hhTrigger_Event::Spawn( void ) { + eventDef = FindEventDef( spawnArgs.GetString("eventDef") ); +} + +/* +================ +hhTrigger_Event::FindEventDef +================ +*/ +const idEventDef* hhTrigger_Event::FindEventDef( const char* eventDefName ) const { + function_t *func = hhTrigger_Event::FindFunction( eventDefName ); + if( !func || !func->eventdef ) { + gameLocal.Error( "Cannot find event %s on trigger %s\n", eventDefName, name.c_str() ); + } + + return func->eventdef; +} + +/* +================ +hhTrigger_Event::FindFunction +================ +*/ +function_t* hhTrigger_Event::FindFunction( const char* funcname ) { + function_t* func = NULL; + + func = gameLocal.program.FindFunction( funcname, gameLocal.program.FindType(funcname) ); + if( !func ) { + gameLocal.Error( "Cannot find function '%s' in hhTrigger", funcname ); + } + + return func; +} + +/* +================ +hhTrigger_Event::ActivateTargets +================ +*/ +void hhTrigger_Event::ActivateTargets( idEntity *activator ) const { + idEntity *ent; + int i; + + HH_ASSERT( eventDef ); + + for( i = 0; i < targets.Num(); i++ ) { + ent = targets[ i ].GetEntity(); + if ( !ent ) { + continue; + } + if ( ent->RespondsTo( *eventDef ) ) { + ent->ProcessEvent( eventDef, activator ); + } + for (int ix=0; ixGetRenderEntity()->gui[ix]->Trigger(gameLocal.time); + } + } + } +} + +/*********************************************************************** + + hhMineTrigger + +***********************************************************************/ + +CLASS_DECLARATION( hhTrigger, hhMineTrigger ) +END_CLASS + +bool hhMineTrigger::CheckTriggerClass( idEntity* activator ) { + if( !activator ) { + return false; + } + + if(!TriggerClasses.Num() || triggerBehavior == TB_ANY) { + return true; + } + + for(int iIndex = 0; iIndex < TriggerClasses.Num(); ++iIndex) { + //Look for exact match then try for prefix match + if( !idStr(activator->spawnArgs.GetString("classname")).Icmp(TriggerClasses[iIndex].c_str()) || + !idStr(activator->spawnArgs.GetString("classname")).IcmpPrefix(TriggerClasses[iIndex].c_str()) ) { + + if( activator->IsType(hhPlayer::Type) ) {//Player needs client + hhPlayer* player = static_cast(activator); + if ( player->noclip || !player->ShouldTouchTrigger(this) ) { + return false; + } + if ( bNoVehicles && player->InVehicle() ) { + return false; + } + } + if( activator->IsType(idActor::Type) ) { // Dead monsters shouldn't keep refires going + if (activator->health <= 0) { + return false; + } + } + + return true; + } + } + + if ( activator->IsType(hhMountedGun::Type) ) { + return true; + } + + //HUMANHEAD jsh PCF 4/28/06 hardcode monsters to trigger mines + if ( !gameLocal.isMultiplayer && activator->IsType( idAI::Type ) && activator->health > 0 ) { + return true; + } + + return false; +} \ No newline at end of file diff --git a/src/Prey/game_trigger.h b/src/Prey/game_trigger.h new file mode 100644 index 0000000..db11b85 --- /dev/null +++ b/src/Prey/game_trigger.h @@ -0,0 +1,258 @@ +#ifndef __PREY_TRIGGER_H +#define __PREY_TRIGGER_H + +class hhFuncParmAccessor : public idClass { + CLASS_PROTOTYPE( hhFuncParmAccessor ); + +public: + hhFuncParmAccessor(); + explicit hhFuncParmAccessor( const hhFuncParmAccessor* accessor ); + explicit hhFuncParmAccessor( const hhFuncParmAccessor& accessor ); + + void SetInfo( const char* returnKey, const function_t* func, const idList& parms ); + + const char* GetFunctionName() const; + const function_t* GetFunction() const; + const char* GetReturnKey() const; + idTypeDef* GetReturnType() const; + const char* GetParm( int index ) const; + idTypeDef* GetParmType( int index ) const; + const idList& GetParms() const; + idList& GetParms(); + + void Verify(); + + static function_t* FindFunction( const char* funcname ); + void CallFunction( idDict& returnDict ); + void ParseFunctionKeyValue( const char* value ); + void ParseFunctionKeyValue( idList& valueList ); + +public: + void SetFunction( const function_t* func ); + void SetParms( const idList& parms ); + void SetReturnKey( const char* key ); + + void SetParm_Entity( const idEntity *ent, int index ); + void SetParm_String( const char *str, int index ); + + void InsertParm_String( const char *text, int index ); + void InsertParm_Float( float value, int index ); + void InsertParm_Int( int value, int index ); + void InsertParm_Vector( const idVec3 &vec, int index ); + void InsertParm_Entity( const idEntity *ent, int index ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void InsertParm( const char *text, int index ); + +protected: + idList parms; + idStr returnKey; + const function_t* function; +}; + +enum triggerBehavior_t { + TB_PLAYER_ONLY, + TB_FRIENDLIES_ONLY, + TB_MONSTERS_ONLY, + TB_PLAYER_MONSTERS_FRIENDLIES, + TB_SPECIFIC_ENTITIES, + TB_ANY, + NUM_BEHAVIORS +}; + +class hhTrigger : public idEntity +{ +public: + CLASS_PROTOTYPE( hhTrigger ); + + void Spawn( void ); + + virtual void Activate( idEntity *activator ); + virtual void TriggerAction( idEntity *activator ); + virtual void UnTriggerAction(); + bool IsEncroaching( const idEntity* entity ); + bool IsEncroached(); + + void SetTriggerClasses(idList& list); + virtual bool CheckTriggerClass(idEntity* activator); + virtual bool CheckUnTriggerClass(idEntity* activator); + void GetTriggerClasses(idDict& Args); + + virtual void Enable(); + virtual void Disable(); + + const bool IsActive() const { return bActive; } + const bool IsEnabled() const { return bEnabled; } + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void CallFunctions( idEntity *activator ); + void UncallFunctions( idEntity *activator ); + + virtual void Event_Enable(); + virtual void Event_Disable(); + void Event_Activate( idEntity *activator ); + void Event_Deactivate(); + virtual void Event_Touch( idEntity *other, trace_t *trace ); + void Event_TriggerAction( idEntity *activator ); + virtual void Event_Retrigger( idEntity *activator ); + void Event_PollForUntouch(); + virtual void Event_UnTriggerAction(); + void Event_PostSpawn(); + +public: + bool bActive; // Whether the trigger is currently touched + bool bEnabled; // whether the trigger is currently triggerable + float untouchGranularity; // Seconds between untouch polls + float wait; // Seconds before trigger is triggerable again + float random; // Random wait variance + float delay; // Seconds to delay the triggering + float randomDelay; + float refire; // Seconds before refiring trigger + int nextTriggerTime; // Time in ms when the trigger will be triggerable again + bool alwaysTrigger; // Do we want to always trigger, instead of just once + bool isSimpleBox; // Simple boxes can use cheaper bounds test for encroach checks + bool bNoVehicles; + + hhFuncParmAccessor funcInfo; + hhFuncParmAccessor unfuncInfo; + hhFuncParmAccessor funcRefInfo; + hhFuncParmAccessor unfuncRefInfo; + hhFuncParmAccessor funcRefActivatorInfo; + hhFuncParmAccessor unfuncRefActivatorInfo; + + idStr requires; // item that the trigger requires of the player + int removeItem; // whether to remove the required item from players inventory + bool noTouch; // whether to disregard touch as triggering mechanism + bool initiallyEnabled; // whether the trigger is initially triggerable + bool bUntrigger; // whether to resend the trigger message when untriggered + idEntityPtr unTriggerActivator; // Used solely for sending to Activate() when bUntrigger is set + + idList TriggerClasses; + + triggerBehavior_t triggerBehavior; +}; + +class hhDamageTrigger : public hhTrigger +{ +public: + CLASS_PROTOTYPE( hhDamageTrigger ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Damage(idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + +protected: + virtual void Event_Enable(); + void Event_Disable(); + + hhFuncParmAccessor funcRefInfoDamage; +}; + + +class hhTriggerPain : public hhTrigger { +public : + CLASS_PROTOTYPE( hhTriggerPain ); + + void Spawn( void ); + virtual void TriggerAction(idEntity *activator); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + void Event_Activate( idEntity *activator ); + void Event_DamageBox( idEntity *activator ); + +protected: + bool applyImpulse; +}; + + +class hhTriggerEnabler : public hhTrigger { +public : + CLASS_PROTOTYPE( hhTriggerEnabler ); + + void Spawn( void ); + virtual void TriggerAction(idEntity *activator); + virtual void UnTriggerAction(void); +}; + + +class hhTriggerSight : public hhTrigger { +public: + CLASS_PROTOTYPE( hhTriggerSight ); + + void Spawn( void ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + virtual void Think( void ); + virtual void Event_Enable( void ); + void Event_Disable( void ); + + int pvsArea; +}; + +/* +=============================================================================== + + Trigger which fires targets after being activated a specific number of times. + +=============================================================================== +*/ +class hhTrigger_Count : public hhTrigger { +public: + CLASS_PROTOTYPE( hhTrigger_Count ); + + // save games + void Save( idSaveGame *savefile ) const; // archives object for save game file + void Restore( idRestoreGame *savefile ); // unarchives object from save game file + + void Spawn( void ); + + virtual void Activate( idEntity *activator ); + virtual void TriggerAction( idEntity *activator ); + +protected: + int goal; + int count; + float delay; +}; + +class hhTrigger_Event : public hhTrigger { +public: + CLASS_PROTOTYPE( hhTrigger_Event ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const { } + void Restore( idRestoreGame *savefile ) { Spawn(); } + +protected: + virtual void ActivateTargets( idEntity *activator ) const; + + const idEventDef* FindEventDef( const char* eventDefName ) const; + static function_t* FindFunction( const char* funcname ); + +protected: + const idEventDef* eventDef; +}; + +class hhMineTrigger : public hhTrigger { + CLASS_PROTOTYPE( hhMineTrigger ); + +public: + virtual bool CheckTriggerClass(idEntity* activator); +}; + +#endif diff --git a/src/Prey/game_tripwire.cpp b/src/Prey/game_tripwire.cpp new file mode 100644 index 0000000..dd41dfb --- /dev/null +++ b/src/Prey/game_tripwire.cpp @@ -0,0 +1,336 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +/********************************************************************** + +hhTriggerTripwire + +**********************************************************************/ +CLASS_DECLARATION( idEntity, hhTriggerTripwire ) + EVENT( EV_Enable, hhTriggerTripwire::Event_Enable ) + EVENT( EV_Disable, hhTriggerTripwire::Event_Disable ) + EVENT( EV_Activate, hhTriggerTripwire::Event_Activate ) + EVENT( EV_Deactivate, hhTriggerTripwire::Event_Deactivate ) +END_CLASS + +/* +================ +hhTriggerTripwire::Spawn +================ +*/ +void hhTriggerTripwire::Spawn( void ) { + m_fMaxBeamDistance = spawnArgs.GetFloat("lengthBeam", "4096"); + + m_CallFunc.ParseFunctionKeyValue( spawnArgs.GetString("call") ); + + m_CallFuncRef.ParseFunctionKeyValue( spawnArgs.GetString("callRef") ); + m_CallFuncRef.InsertParm_Entity( this, 0 ); + + m_CallFuncRefActivator.ParseFunctionKeyValue( spawnArgs.GetString("callRefActivator") ); + m_CallFuncRefActivator.InsertParm_Entity( this, 0 ); + + m_TriggerBehavior = (triggerBehavior_t)spawnArgs.GetInt("triggerBehavior"); + + GetPhysics()->GetContents( 0 ); + GetPhysics()->DisableClip(); + + // Spawn the eyebeam + m_pBeamEntity = hhBeamSystem::SpawnBeam( GetOrigin(), spawnArgs.GetString("beam") ); + if (m_pBeamEntity.IsValid()) { + m_pBeamEntity->SetOrigin( GetOrigin() ); + m_pBeamEntity->SetAxis( GetAxis() ); + m_pBeamEntity->Bind( this, true ); + } + + GetTriggerClasses( spawnArgs ); + + ToggleBeam( true ); + + m_pOwner = NULL; +} + +void hhTriggerTripwire::Save(idSaveGame *savefile) const { + savefile->WriteFloat(m_fMaxBeamDistance); + + savefile->WriteStaticObject(m_CallFunc); + savefile->WriteStaticObject(m_CallFuncRef); + savefile->WriteStaticObject(m_CallFuncRefActivator); + + m_pBeamEntity.Save(savefile); + + savefile->WriteInt(m_TriggerClasses.Num()); // idList + for (int i=0; iWriteString(m_TriggerClasses[i]); + } + + savefile->WriteInt(m_TriggerBehavior); + savefile->WriteObject( m_pOwner ); +} + +void hhTriggerTripwire::Restore( idRestoreGame *savefile ) { + int num; + + savefile->ReadFloat(m_fMaxBeamDistance); + + savefile->ReadStaticObject(m_CallFunc); + savefile->ReadStaticObject(m_CallFuncRef); + savefile->ReadStaticObject(m_CallFuncRefActivator); + + m_pBeamEntity.Restore(savefile); + + m_TriggerClasses.Clear(); + savefile->ReadInt(num); // idList + m_TriggerClasses.SetNum(num); + for (int i=0; iReadString(m_TriggerClasses[i]); + } + + savefile->ReadInt((int&)m_TriggerBehavior); + savefile->ReadObject( reinterpret_cast( m_pOwner ) ); +} + +/* +=============== +hhTriggerTripwire::~hhTriggerTripwire +=============== +*/ + +hhTriggerTripwire::~hhTriggerTripwire() { + SAFE_REMOVE( m_pBeamEntity ); +} + +/* +================ +hhSecurityEyeTripwire::SetOwner +================ +*/ +void hhTriggerTripwire::SetOwner( idEntity* pOwner ) { + m_pOwner = pOwner; +} + +/* +=============== +hhTriggerTripwire::NotifyTargets +=============== +*/ +void hhTriggerTripwire::NotifyTargets( idEntity* pActivator ) { + ActivateTargets( pActivator ); + + if( m_pOwner ) { + m_pOwner->ProcessEvent( &EV_Notify, pActivator ); + } +} + +/* +=============== +hhTriggerTripwire::ToggleBeam +=============== +*/ +void hhTriggerTripwire::ToggleBeam( bool bOn ) { + if( bOn ) { + BecomeActive( TH_TICKER ); + } + else { + BecomeInactive( TH_TICKER ); + } + + if( m_pBeamEntity.IsValid() ) { + m_pBeamEntity->Activate( bOn ); + } +} + +/* +=============== +hhTriggerTripwire::Ticker +=============== +*/ +void hhTriggerTripwire::Ticker() { + trace_t TraceInfo; + idEntity* pTraceTarget = NULL; + idVec3 TraceLength = GetAxis()[0] * m_fMaxBeamDistance; + idVec3 TraceEndPoint = GetOrigin() + TraceLength; + + gameLocal.clip.TracePoint( TraceInfo, GetOrigin(), TraceEndPoint, MASK_VISIBILITY, this ); + + // Update the beam system + if( m_pBeamEntity.IsValid() ) { + m_pBeamEntity->SetTargetLocation( TraceInfo.endpos ); + } + + if( TraceInfo.fraction < 1.0f && TraceInfo.c.entityNum < ENTITYNUM_MAX_NORMAL ) { + pTraceTarget = gameLocal.GetTraceEntity(TraceInfo); + if( CheckTriggerClass(pTraceTarget) ) { + CallFunctions( pTraceTarget ); + + NotifyTargets( pTraceTarget ); + + if( spawnArgs.GetBool("triggerOnce") ) { + PostEventMS( &EV_Disable, 0 ); + } + return; + } + } + + if(p_tripwireDebug.GetBool()) { + gameRenderWorld->DebugLine(colorRed, GetPhysics()->GetOrigin(), GetPhysics()->GetOrigin() + TraceLength * TraceInfo.fraction); + + gameRenderWorld->DebugBox( colorBlue, idBox(GetPhysics()->GetBounds(), GetPhysics()->GetOrigin(), GetPhysics()->GetAxis()) ); + } +} + +/* +================ +hhTriggerTripwire::CallFunctions +================ +*/ +void hhTriggerTripwire::CallFunctions( idEntity *activator ) { + m_CallFunc.CallFunction( spawnArgs ); + m_CallFuncRef.CallFunction( spawnArgs ); + + m_CallFuncRefActivator.InsertParm_Entity( activator, 1 );//Needs to be second parm. After self + m_CallFuncRefActivator.CallFunction( spawnArgs ); +} + +/* +=============== +hhTriggerTripwire::SetTriggerClasses +=============== +*/ +void hhTriggerTripwire::SetTriggerClasses( idList& List ) { + m_TriggerClasses.Clear(); + + m_TriggerClasses.SetNum( List.Num() ); + + for(int iIndex = 0; iIndex < List.Num(); ++iIndex) { + m_TriggerClasses[iIndex] = List[iIndex]; + } +} + +/* +=============== +hhTriggerTripwire::CheckTriggerClass +=============== +*/ +bool hhTriggerTripwire::CheckTriggerClass( idEntity* pActivator ) { + if( !pActivator ) { + return false; + } + + if(!m_TriggerClasses.Num() || m_TriggerBehavior == TB_ANY) { + return true; + } + + for(int iIndex = 0; iIndex < m_TriggerClasses.Num(); ++iIndex) { + //Look for exact match then try for prefix match + if( !idStr(pActivator->spawnArgs.GetString("classname")).Icmp(m_TriggerClasses[iIndex].c_str()) || + !idStr(pActivator->spawnArgs.GetString("classname")).IcmpPrefix(m_TriggerClasses[iIndex].c_str()) ) { + if( pActivator && pActivator->IsType(hhPlayer::Type) ) {//Player needs client + hhPlayer* pPlayer = static_cast(pActivator); + if( !pPlayer || pPlayer->noclip || (pPlayer->GetPhysics()->GetClipMask() & MASK_SPIRITPLAYER) == MASK_SPIRITPLAYER ) { + return false; + } + } + // Vehicles are added for trigger behaviors including players, so here we check if the pilot is a player and should touch triggers + // Monsters piloting vehicles currently do not get scanned by tripwires + if( pActivator && pActivator->IsType(hhVehicle::Type) ) { + hhVehicle *pVehicle = static_cast(pActivator); + if( !pVehicle || pVehicle->IsNoClipping() || !pVehicle->GetPilot() || !pVehicle->GetPilot()->IsType(hhPlayer::Type) ) { + return false; + } + } + return true; + } + } + + return false; +} + +/* +=============== +hhTriggerTripwire::GetTriggerClasses +=============== +*/ +void hhTriggerTripwire::GetTriggerClasses( idDict& Args ) { + const idKeyValue* pKeyValue = NULL; + int iNumKeyValues = Args.GetNumKeyVals(); + + m_TriggerClasses.Clear(); + + switch( m_TriggerBehavior ) { + case TB_PLAYER_ONLY: + m_TriggerClasses.AddUnique( "player" ); + m_TriggerClasses.AddUnique( "vehicle" ); + break; + + case TB_FRIENDLIES_ONLY: + m_TriggerClasses.AddUnique( "player" ); + m_TriggerClasses.AddUnique( "vehicle" ); + m_TriggerClasses.AddUnique( "character" ); + break; + + case TB_MONSTERS_ONLY: + m_TriggerClasses.AddUnique( "monster" ); + break; + + case TB_PLAYER_MONSTERS_FRIENDLIES: + m_TriggerClasses.AddUnique( "player" ); + m_TriggerClasses.AddUnique( "vehicle" ); + m_TriggerClasses.AddUnique( "monster" ); + m_TriggerClasses.AddUnique( "character" ); + break; + + case TB_SPECIFIC_ENTITIES: + for( int iIndex = 0; iIndex < iNumKeyValues; ++iIndex ) { + pKeyValue = Args.GetKeyVal( iIndex ); + if ( !pKeyValue->GetKey().Cmpn( "trigger_class", 13 ) ) { + m_TriggerClasses.AddUnique( pKeyValue->GetValue() ); + } + } + break; + + default: + HH_ASSERT(!"Invalid trigger behavior!\n"); + break; + } +} + +/* +================ +hhTriggerTripwire::Event_Enable +================ +*/ +void hhTriggerTripwire::Event_Enable( void ) { + ToggleBeam( true ); +} + +/* +================ +hhTriggerTripwire::Event_Disable +================ +*/ +void hhTriggerTripwire::Event_Disable( void ) { + ToggleBeam( false ); +} + +/* +================ +hhTriggerTripwire::Event_Activate +================ +*/ +void hhTriggerTripwire::Event_Activate( idEntity *activator ) { + ToggleBeam( true ); +} + +/* +================ +hhTriggerTripwire::Event_Deactivate +================ +*/ +void hhTriggerTripwire::Event_Deactivate() { + ToggleBeam( false ); +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build diff --git a/src/Prey/game_tripwire.h b/src/Prey/game_tripwire.h new file mode 100644 index 0000000..9eb17f4 --- /dev/null +++ b/src/Prey/game_tripwire.h @@ -0,0 +1,50 @@ +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#ifndef __HH_TRIGGER_TRIPWIRE_H +#define __HH_TRIGGER_TRIPWIRE_H + +class hhTriggerTripwire : public idEntity { + CLASS_PROTOTYPE( hhTriggerTripwire ); + +public: + ~hhTriggerTripwire(); + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void ToggleBeam( bool bOn ); + void SetOwner( idEntity* pOwner ); + const idEntity* GetOwner() const { return m_pOwner; } + +protected: + virtual void NotifyTargets( idEntity* pActivator ); + + void SetTriggerClasses( idList& List ); + virtual bool CheckTriggerClass( idEntity* pActivator ); + void GetTriggerClasses( idDict& Args ); + + void CallFunctions( idEntity* activator ); + +protected: + virtual void Ticker(); + void Event_Enable(); + void Event_Disable(); + void Event_Activate( idEntity *activator ); + void Event_Deactivate(); + +public: + float m_fMaxBeamDistance; + +protected: + idEntity* m_pOwner; + hhFuncParmAccessor m_CallFunc; + hhFuncParmAccessor m_CallFuncRef; + hhFuncParmAccessor m_CallFuncRefActivator; + + idEntityPtr m_pBeamEntity; + + idList m_TriggerClasses; + triggerBehavior_t m_TriggerBehavior; +}; + +#endif +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/game_utils.cpp b/src/Prey/game_utils.cpp new file mode 100644 index 0000000..1031d42 --- /dev/null +++ b/src/Prey/game_utils.cpp @@ -0,0 +1,749 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +////////////////////// +// hhMatterEventDefPartner +////////////////////// +hhMatterEventDefPartner::hhMatterEventDefPartner( const char* eventNamePrefix ) { + const char* event = NULL; + const idEventDef* eventDef = NULL; + surfTypes_t type = SURFTYPE_NONE; + + for( int ix = 0; ix < NUM_SURFACE_TYPES; ++ix ) { + type = (surfTypes_t)ix; + event = va( "<%s_%s>", eventNamePrefix, gameLocal.MatterTypeToMatterName(type) ); + eventDef = idEventDef::FindEvent( event ); + AddPartner( eventDef, type ); + } +} + + +////////////////////// +// hhUtils +////////////////////// + + + +int hhUtils::ContentsOfBounds(const idBounds &localBounds, const idVec3 &location, const idMat3 &axis, idEntity *pass) { + idTraceModel trm; + trm.SetupBox(localBounds); + idClipModel *clipModel = new idClipModel(trm); + int contents = gameLocal.clip.Contents(location, clipModel, axis, MASK_ALL, pass); + delete clipModel; + return contents; +} + +/* +==================== +hhUtils::EntityDefWillFit + Check whether a given entity def will fit in a location if spawned, pass the contents that would block it +==================== +*/ +bool hhUtils::EntityDefWillFit( const char *defName, const idVec3 &location, const idMat3 &axis, int contentMask, idEntity *passEntity ) { + idBounds localBounds; + idVec3 size; + + // Calculate a local bounds for object + const idDict *dict = gameLocal.FindEntityDefDict(defName, false); + localBounds.Zero(); + if (dict) { + if ( dict->GetVector( "mins", NULL, localBounds[0] ) && + dict->GetVector( "maxs", NULL, localBounds[1] ) ) { + } else if ( dict->GetVector( "size", NULL, size ) ) { + localBounds[0].Set( size.x * -0.5f, size.y * -0.5f, 0.0f ); + localBounds[1].Set( size.x * 0.5f, size.y * 0.5f, size.z ); + } else { // Default bounds + localBounds.Expand( 1.0f ); + } + } + + // Determine contents of the bounds + idTraceModel trm; + trm.SetupBox(localBounds); + idClipModel *clipModel = new idClipModel(trm); + int contents = gameLocal.clip.Contents(location, clipModel, axis, contentMask, passEntity); + delete clipModel; + +/* if (contents & CONTENTS_SOLID) { + gameRenderWorld->DebugBounds( colorRed, localBounds, location, 5000); + } + else if (contents == 0) { + gameRenderWorld->DebugBounds( colorGreen, localBounds, location, 5000); + } + else { + gameRenderWorld->DebugBounds( colorYellow, localBounds, location, 5000); + }*/ + + return (contents == 0); +} + + +/* +==================== +hhUtils::SpawnDebrisMass +==================== +*/ +void hhUtils::SpawnDebrisMass( const char *debrisMassEntity, + const idVec3 &origin, + const idVec3 *orientation, + const idVec3 *velocity, + const int power, + bool nonsolid, + float *duration, + idEntity *entForBounds ) { //HUMANHEAD rww - added entForBounds + + idDict args; + idVec3 powerVector; + + + if ( !debrisMassEntity || !debrisMassEntity[0] ) { + gameLocal.Warning( "Invalid Debris Entity called (%p)", debrisMassEntity ); + return; + } + + args.SetVector( "origin", origin ); + + // Set the values to the dictionary + if ( orientation ) { + args.SetVector( "orientation", *orientation ); + } + + if ( power >= 0 ) { + powerVector.x = powerVector.y = powerVector.z = power; + args.SetVector( "power", powerVector ); + } + + if ( nonsolid ) { + args.Set( "nonsolid", "1" ); + } + + if ( velocity ) { + args.SetVector( "velocity", *velocity ); + } + + if (entForBounds) { //HUMANHEAD rww + args.SetBool("spawnUsingEntity", true); + args.SetBool("useAFBounds", true); + } + + // Spawn the object + idEntity *ent = gameLocal.SpawnObject( debrisMassEntity, &args ); + + if ( duration && ent->IsType( hhDebrisSpawner::Type ) ) { + hhDebrisSpawner *dspawner = static_cast ( ent ); + + if (entForBounds) { //HUMANHEAD rww + dspawner->Activate(entForBounds); + } + + *duration = dspawner->GetDuration(); + } +} + + +/* +==================== +hhUtils::SpawnDebrisMass +==================== +*/ +void hhUtils::SpawnDebrisMass( const char *debrisMassEntity, + idEntity *sourceEntity, + const int power ) { + + idEntity *mass; + idDict args; + + + if ( !debrisMassEntity || !debrisMassEntity[0] ) { + gameLocal.Warning( "Invalid Debris Entity called (%p).", debrisMassEntity ); + return; + } + + args.SetVector( "origin", sourceEntity->GetOrigin() ); + args.SetVector( "orientation", sourceEntity->GetPhysics()->GetAxis()[0] ); + args.SetVector( "velocity", sourceEntity->GetPhysics()->GetLinearVelocity() ); + + if ( power >= 0 ) { + args.SetInt( "power", power ); + } + + args.SetInt( "spawnUsingEntity" , 1 ); + + + // Spawn the object + mass = gameLocal.SpawnObject( debrisMassEntity, &args ); + if ( mass ) { + if ( mass->IsType( hhDebrisSpawner::Type ) ) { + ( ( hhDebrisSpawner * ) mass )->Activate( sourceEntity ); + } + } + else { + gameLocal.Printf("Error spawning debris mass: %s\n", debrisMassEntity ); + } + +} + + +hhProjectile *hhUtils::LaunchProjectile(idEntity *attacker, + const char *projectile, + const idMat3 &axis, + const idVec3 &origin ) { + + hhProjectile *proj=NULL; + idDict args; + args.Clear(); + args.Set( "classname", projectile ); + args.Set( "origin", origin.ToString() ); + + idEntity *ent=NULL; + if (gameLocal.SpawnEntityDef( args, &ent ) && ent && ent->IsType(hhProjectile::Type)) { + proj = ( hhProjectile * )ent; + proj->Create( attacker, origin, axis ); + proj->Launch( origin, axis, vec3_origin ); + } + + return proj; +} + +void hhUtils::DebugCross( const idVec4 &color, const idVec3 &start, int size, const int lifetime ) { + static idVec3 xaxis(1.0f,0.0f,0.0f); + static idVec3 yaxis(0.0f,1.0f,0.0f); + static idVec3 zaxis(0.0f,0.0f,1.0f); + gameRenderWorld->DebugLine(color, start-xaxis*size, start+xaxis*size, lifetime); + gameRenderWorld->DebugLine(color, start-yaxis*size, start+yaxis*size, lifetime); + gameRenderWorld->DebugLine(color, start-zaxis*size, start+zaxis*size, lifetime); +} + +void hhUtils::DebugAxis( const idVec3 &origin, const idMat3 &axis, int size, const int lifetime ) { + gameRenderWorld->DebugArrow(colorRed, origin, origin+axis[0]*size, 5, lifetime); + gameRenderWorld->DebugArrow(colorGreen, origin, origin+axis[1]*size, 5, lifetime); + gameRenderWorld->DebugArrow(colorBlue, origin, origin+axis[2]*size, 5, lifetime); +} + + +idVec3 hhUtils::RandomVector() { + idVec3 vec; + vec.x = gameLocal.random.CRandomFloat(); + vec.y = gameLocal.random.CRandomFloat(); + vec.z = gameLocal.random.CRandomFloat(); + return vec; +} + +float hhUtils::RandomSign() { + return gameLocal.random.RandomFloat() < 0.5f ? -1.0f : 1.0f; +} + +idVec3 hhUtils::RandomPointInBounds(idBounds &bounds) { + idVec3 point; + point.x = bounds[0].x + (bounds[1].x - bounds[0].x) * gameLocal.random.RandomFloat(); + point.y = bounds[0].y + (bounds[1].y - bounds[0].y) * gameLocal.random.RandomFloat(); + point.z = bounds[0].z + (bounds[1].z - bounds[0].z) * gameLocal.random.RandomFloat(); + return point; +} + +idVec3 hhUtils::RandomPointInShell( const float innerRadius, const float outerRadius ) { + idAngles dir( gameLocal.random.RandomFloat() * 360.0f, gameLocal.random.RandomFloat() * 360.0f, gameLocal.random.RandomFloat() * 360.0f ); + return dir.ToForward() * (innerRadius + (outerRadius - innerRadius) * gameLocal.random.RandomFloat()); +} + +/* +================= +hhUtils::SplitString + Strips out the first element which is usually the original command +================= +*/ +void hhUtils::SplitString( const idCmdArgs& input, idList& pieces ) { + int numParms = input.Argc() - 1; + for( int ix = 0; ix < numParms; ++ix ) { + pieces.Append( input.Argv(ix + 1) ); + } +} + +/* +================= +hhUtils::SplitString + Takes a comma seperated string, and returns the bits between the commas + Spaces on either side of the comma are stripped off + The 'pieces' list is not cleared. +================= +*/ +void hhUtils::SplitString( const idStr& input, idList& pieces, const char delimiter ) { + idStr element; + int endIndex = -1; + const char groupDelimiter = '\''; + char currentChar = '\0'; + + for( int startIndex = 0; startIndex <= input.Length(); ++startIndex ) { + currentChar = input[ startIndex ]; + if (currentChar) { + if( currentChar == groupDelimiter ) { + endIndex = input.Find( currentChar, startIndex + 1 ); + element = input.Mid( startIndex + 1, endIndex - startIndex - 1 ); + + startIndex = endIndex; + + pieces.Append( element ); + element.Clear(); + continue; + } else if( currentChar == delimiter ) { + element += '\0'; + pieces.Append( element ); + element.Clear(); + continue; + } + + element += currentChar; + } + } + + if( element.Length() ) { + pieces.Append( element ); + } +} + + +/* +================= +hhUtils::GetValues +Takes a source dict, and a key base string, and returns all values for keys with that base. ie: + + "base" "b" + "base1" "b1" + "base4" "b4" + "base_bob" "bob" + + is the dict. + +The option 'numericOnly' restricts the keys to be either an exact match, or a numeric addition + + hhUtils::GetValues( dict, "base", strList, true ); + +strList would contain: "b", "b1", "b4" + +while: + + hhUtils::GetValues( dict, "base", strList ); + +strList would contain: "b", "b1", "b4", "bob" + +================= +*/ +void hhUtils::GetValues( idDict &source, const char *keyBase, idList &values, bool numericOnly ) { + idList keys; + + GetKeysAndValues( source, keyBase, keys, values, numericOnly ); +} // hhUtils::GetValues( idDict &, const char *, idList, [bool] ) + + +/* +================= +hhUtils::GetKeys +Takes a source dict, and a key base string, and returns all keys with that base. ie: + + "base" "b" + "base1" "b1" + "base4" "b4" + "base_bob" "bob" + + is the dict. + +The option 'numericOnly' restricts the keys to be either an exact match, or a numeric addition + + hhUtils::GetValues( dict, "base", strList, true ); + +strList would contain: "base", "base1", "base4" + +while: + + hhUtils::GetValues( dict, "base", strList ); + +strList would contain: "base", "base1", "base4", "base_bob" + +================= +*/ +void hhUtils::GetKeys( idDict &source, const char *keyBase, idList &keys, bool numericOnly ) { + idList values; + + GetKeysAndValues( source, keyBase, keys, values, numericOnly ); +} + + +/* +============== +hhUtils::getKeysAndValues +============== +*/ +void hhUtils::GetKeysAndValues( idDict &source, const char *keyBase, idList &keys, idList &values, bool numericOnly ) { + const idKeyValue * kv = NULL; + idStr keyEnd; + + + for ( kv = source.MatchPrefix( keyBase, kv ); + kv && kv->GetValue().Length(); + kv = source.MatchPrefix( keyBase, kv ) ) { + keyEnd = kv->GetKey(); + keyEnd.Strip( keyBase ); + + // Is a valid debris base And it isn't a variation. (ie, contains a .X postfix) + if ( !numericOnly || ( idStr::IsNumeric( keyEnd ) && keyEnd.Find( '.' ) < 0 ) ) { + //gameLocal.Printf( "Adding %s => %s\n", (const char *) kv->GetKey(), (const char *) kv->GetValue() ); + keys.Append( kv->GetKey() ); + values.Append( kv->GetValue() ); + } + + } + +} + +/* +================= +hhUtils::RandomSpreadDir +================= +*/ +idVec3 hhUtils::RandomSpreadDir( const idMat3& baseAxis, const float spread ) { + float ang = hhMath::Sin( spread * gameLocal.TimeBasedRandomFloat() ); + float spin = hhMath::TWO_PI * gameLocal.TimeBasedRandomFloat(); + idVec3 dir = baseAxis[ 0 ] + baseAxis[ 2 ] * ( ang * hhMath::Sin(spin) ) - baseAxis[ 1 ] * ( ang * hhMath::Cos(spin) ); + dir.Normalize(); + + return dir; +} + +//----------------------------------------------------- +// ProjectOntoScreen +// +// Project a world position onto screen +//----------------------------------------------------- +idVec3 hhUtils::ProjectOntoScreen(idVec3 &world, const renderView_t &renderView) { + idVec3 pdc(-1000.0f, -1000.0f, -1.0f); + + // Convert world -> camera + idVec3 view = ( world - renderView.vieworg ) * renderView.viewaxis.Inverse(); + + // Orient from doom coords to camera coords (look down +Z) + idVec3 cam; + cam.x = -view[1]; + cam.y = -view[2]; + cam.z = view[0]; + + if (cam.z > 0.0f) { + // Adjust for differing FOVs + float halfwidth = renderView.width * 0.5f; + float halfheight = renderView.height * 0.5f; + float f = halfwidth / tan( renderView.fov_x * 0.5f * idMath::M_DEG2RAD ); + float g = halfheight / tan( renderView.fov_y * 0.5f * idMath::M_DEG2RAD ); + + // Project onto screen + pdc[0] = (cam.x * f / cam.z) + halfwidth; + pdc[1] = (cam.y * g / cam.z) + halfheight; + pdc[2] = cam.z; + } + + return pdc; // negative Z indicates behind the view +} + + +/* +================= +hhUtils::GetLocalGravity +================= +*/ +idVec3 hhUtils::GetLocalGravity( const idVec3& origin, const idBounds& bounds, const idVec3& defaultGravity ) { + idEntity* entityList[ MAX_GENTITIES ]; + idEntity* entity = NULL; + hhGravityZoneBase* zone = NULL; + + int numEntities = gameLocal.clip.EntitiesTouchingBounds( bounds.Translate(origin), CONTENTS_TRIGGER, entityList, MAX_GENTITIES ); + for( int ix = 0; ix < numEntities; ++ix ) { + entity = entityList[ ix ]; + + if( !entity ) { + continue; + } + + if( !entity->IsType(hhGravityZoneBase::Type) ) { + continue; + } + + zone = static_cast( entity ); + if( !zone->IsActive() || !zone->IsEnabled() ) { + continue; + } + + // More expensive check if non-simple zone + if ( !zone->isSimpleBox ) { + idTraceModel *playerTrm = new idTraceModel(bounds); + idClipModel *playerModel = new idClipModel( *playerTrm ); + playerModel->SetContents(CONTENTS_TRIGGER); + + idClipModel *zoneModel = zone->GetPhysics()->GetClipModel(); + + int contents = gameLocal.clip.ContentsModel( playerModel->GetOrigin(), playerModel, playerModel->GetAxis(), -1, + zoneModel->Handle(), zoneModel->GetOrigin(), zoneModel->GetAxis() ); + + delete playerModel; + delete playerTrm; + if ( !contents ) { + continue; + } + } + + return zone->GetCurrentGravity( origin ); + } + + return defaultGravity; +} + +//============================================================================= +// +// PointToAngle +// +// Point should be about the origin +//============================================================================= +float hhUtils::PointToAngle(float x, float y) +{ + if ( x == 0 && y == 0 ) { + return 0; + } + + if ( x >= 0 ) { // x >= 0 + if (y >= 0 ) { // y >= 0 + if ( x > y ) { + return atan( y / x ); // octant 0 + } else { + return DEG2RAD(90) - atan( x / y ); // octant 1 + } + } + else { // y < 0 + y = -y; + if ( x > y ) { + return -atan( y / x ); // octant 8 + } else { + return DEG2RAD(270) + atan( x / y ); // octant 7 + } + } + } + else { // x < 0 + x = -x; + if ( y >= 0 ) { // y>= 0 + if ( x > y ) { + return DEG2RAD(180) - atan( y / x ); // octant 3 + } + else { + return DEG2RAD(90) + atan( x / y ); // octant 2 + } + } + else { // y < 0 + y = -y; + if ( x > y ) { + return DEG2RAD(180) + atan( y / x ); // octant 4 + } else { + return DEG2RAD(270) - atan( x / y ); // octant 5 + } + } + } + + return 0; +} + +float hhUtils::DetermineFinalFallVelocityMagnitude( const float totalFallDist, const float gravity ) { + if( hhMath::Fabs(gravity) <= VECTOR_EPSILON ) { + return 0.0f; + } + + return gravity * hhMath::Sqrt( 2.0f * totalFallDist / gravity ); +} + +/* +====================== +hhUtils::ChannelName2Num +returns -1 if not found +HUMANHEAD nla +====================== +*/ +int hhUtils::ChannelName2Num( const char *name, const idDict *entityDef ) { + int num; + + entityDef->GetInt( va( "channel2num_%s", name ), "-1", num ); + + return( num ); +} + + +/* +================ +hhUtils::CreateFxDefinition + +//HUMANHEAD: aob - used for our wound system +================ +*/ +void hhUtils::CreateFxDefinition( idStr &definition, const char* smokeName, const float duration ) { + + definition.Clear(); + + if( !smokeName || !smokeName[0] ) { + return; + } + + definition = va( + "fx %s // IMPLICITLY GENERATED\n" + "{ {\n" + "name \"%s\"\n" + "duration %.2f\n" + "delay 0\n" + "restart 0\n" + "particle \"%s.prt\"\n" + "} }\n", smokeName, smokeName, duration, smokeName); +} + +/* +================ +hhUtils::CalculateSoundVolume + Calculate a linear volume from a velocity magnitude and min/max range +================ +*/ +float hhUtils::CalculateSoundVolume( const float value, const float min, const float max ) { + float linear; + float decibels; + + if( min == max ) { + return 0.0f; + } + + linear = hhMath::ClampFloat( 0.0f, 1.0f, (value - min) / (max - min) ); + linear = (linear * 0.95f) + 0.05f; // Make lower end -30dB not -60dB + + decibels = hhMath::Scale2dB(linear); + decibels -= 3; + linear = hhMath::dB2Scale(decibels); + + return linear; +} + +/* +================ +hhUtils::CalculateScale +================ +*/ +float hhUtils::CalculateScale( const float value, const float min, const float max ) { + if( min == max ) { + return 0.0f; + } + + return hhMath::ClampFloat( 0.0f, 1.0f, (value - min) / (max - min) ); +} + +// Find all entities overlapping this clipmodel +// Pass a contents to limit the entities found to those matching that contents +int hhUtils::EntitiesTouchingClipmodel( idClipModel *clipModel, idEntity **entityList, int maxCount, int contents ) { + int i, j, numClipModels, numEntities; + idClipModel * clipModels[ MAX_GENTITIES ]; + idClipModel * cm; + idEntity * ent; + + numClipModels = gameLocal.clip.ClipModelsTouchingBounds( clipModel->GetAbsBounds(), contents, clipModels, MAX_GENTITIES ); + numEntities = 0; + + // For each entity in bounds of our clipModel + // test if entity overlaps the clipModel + for ( i = 0; i < numClipModels; i++ ) { + cm = clipModels[ i ]; + + if (cm == clipModel) { + continue; + } + + ent = cm->GetEntity(); + + if (!ent || !ent->GetPhysics()->GetClipModel()->IsTraceModel()) { + // Can only test versus trace models + continue; + } + + // if the entity is not yet in the list + for ( j = 0; j < numEntities; j++ ) { + if ( entityList[j] == ent ) { + break; + } + } + if ( j < numEntities ) { + continue; + } + + // Test if entity clipmodel overlaps our clipmodel + if ( !ent->GetPhysics()->ClipContents( clipModel ) ) { + continue; + } + + // add entity to the list + if ( numEntities >= maxCount ) { + gameLocal.Warning( "hhUtils::EntitiesTouchingClipmodel: max count" ); + break; + } + entityList[numEntities++] = ent; + + } + + return numEntities; +} + +idBounds hhUtils::ScaleBounds( const idBounds& bounds, float scale ) { + idBounds localBounds; + + if( scale <= 0.0 || scale == 1.0f ) { + return bounds; + } + + //Put bounds at origin so our scale is done correctly + localBounds = bounds.Translate( -bounds.GetCenter() ); + + for( int ix = 0; ix < 2; ++ix ) { + localBounds[ix] *= scale; + } + + return localBounds; +} + +idVec3 hhUtils::DetermineOppositePointOnBounds( const idVec3& start, const idVec3& dir, const idBounds& bounds ) { + float scale = 0.0f; + float radius = bounds.GetRadius(); + + if( !bounds.RayIntersection(start, dir, scale) ) { + return start; + } + + if( !bounds.RayIntersection(start + dir * radius, -dir, scale) ) { + return start; + } + + return start + dir * (radius - scale); +} + +/* +============= +hhUtils::PassArgs + Take any args that begin with prefix, and pass them into dict +============= +*/ + +void hhUtils::PassArgs( const idDict &source, idDict &dest, const char *passPrefix ) { + const idKeyValue * kv = NULL; + idStr indexStr; + + // Loop through looking for the next valid key to pass + for ( kv = source.MatchPrefix( passPrefix, kv ); + kv && kv->GetValue().Length(); + kv = source.MatchPrefix( passPrefix, kv ) ) { + indexStr = kv->GetKey(); + indexStr.Strip( passPrefix ); + +// gameLocal.Printf( "Passing %s => %s\n", +// (const char *) indexStr, +// (const char *) kv->GetValue() ); + + dest.Set( indexStr, kv->GetValue() ); + } +} + diff --git a/src/Prey/game_utils.h b/src/Prey/game_utils.h new file mode 100644 index 0000000..ca97c29 --- /dev/null +++ b/src/Prey/game_utils.h @@ -0,0 +1,318 @@ + +#ifndef __GAME_UTILS_H__ +#define __GAME_UTILS_H__ + +//Helper defines to allow line commenting with certain defines. +#define COMMENT SLASH(/) +#define SLASH(s) /##s + +////////////////////// +// hhMatterPartner +////////////////////// +template< class PartnerType > +class hhMatterPartner { +public: + const PartnerType& GetPartner( const idEntity* ent, const idMaterial* material ) const; + const PartnerType& GetPartner( surfTypes_t type ) const; + +protected: + void AddPartner( PartnerType partner, surfTypes_t type ); + +protected: + // HUMANHEAD mdl: This doesn't need to be saved because it's generated automatically by the constructor. I think. + idList< PartnerType > matterPartnerList; +}; + +template< class PartnerType > +const PartnerType& hhMatterPartner::GetPartner( const idEntity* ent, const idMaterial* material ) const { + return GetPartner( gameLocal.GetMatterType(ent, material) ); +} + +template< class PartnerType > +const PartnerType& hhMatterPartner::GetPartner( surfTypes_t type ) const { + return matterPartnerList[ (int)type ]; +} + +template< class PartnerType > +void hhMatterPartner::AddPartner( PartnerType partner, surfTypes_t type ) { + matterPartnerList.Insert( partner, (int)type ); +} + +////////////////////// +// hhMatterEventDefPartner +////////////////////// +class hhMatterEventDefPartner : public hhMatterPartner { +public: + hhMatterEventDefPartner( const char* eventNamePrefix ); +}; + + +////////////////////// +// hhUtils +////////////////////// +class hhUtils { +public: + static int ContentsOfBounds(const idBounds &localBounds, const idVec3 &location, const idMat3 &axis, idEntity *pass); + static bool EntityDefWillFit( const char *defName, const idVec3 &location, const idMat3 &axis, int contentMask, idEntity *pass ); + static void SpawnDebrisMass( const char *debrisEntity, + const idVec3 &origin, + const idVec3 *orientation = NULL, + const idVec3 *velocity = NULL, + const int power = -1, + bool nonsolid = false, + float *duration = NULL, + idEntity *entForBounds = NULL ); //HUMANHEAD rww - added entForBounds + + static void SpawnDebrisMass( const char *debrisEntity, + idEntity *sourceEntity, + const int power = -1 ); + + static hhProjectile * LaunchProjectile( idEntity *attacker, + const char *projectile, + const idMat3 &axis, + const idVec3 &origin ); + + static void DebugAxis( const idVec3 &origin, const idMat3 &axis, int size, const int lifetime ); + static void DebugCross( const idVec4 &color, + const idVec3 &start, + int size, + const int lifetime = 0 ); + + static idVec3 RandomVector(); + static float RandomSign(); + static idVec3 RandomSpreadDir( const idMat3& baseAxis, const float spread ); + + static idVec3 RandomPointInBounds(idBounds &bounds); + static idVec3 RandomPointInShell( const float innerRadius, const float outerRadius ); + + static void SplitString( const idCmdArgs& input, idList& pieces ); + static void SplitString( const idStr& input, idList& pieces, const char delimiter = ',' ); + + static idVec3 GetLocalGravity( const idVec3& origin, const idBounds& bounds, const idVec3& defaultGravity = gameLocal.GetGravity() ); + + static idVec3 ProjectOntoScreen(idVec3 &world, const renderView_t &renderView); + + static void GetValues( idDict& source, const char *keyBase, idList &values, bool numericOnly = false ); + + static void GetKeys( idDict& source, const char *keyBase, idList &keys, bool numericOnly = false ); + + static void GetKeysAndValues( idDict& source, const char *keyBase, idList &keys, idList &values, bool numericOnly = false ); + + static float DetermineFinalFallVelocityMagnitude( const float totalFallDist, const float gravity ); + + static int ChannelName2Num( const char *name, const idDict *entityDef ); + + static int EntitiesTouchingClipmodel(idClipModel *clipModel, idEntity **entityList, int maxCount, int contents=MASK_SHOT_BOUNDINGBOX ); + + static void PassArgs( const idDict &source, idDict &dest, const char *passPrefix = "pass_" ); + + static float PointToAngle(float x, float y); + + static idMat3 SwapXZ( const idMat3& axis ); + + static void CreateFxDefinition( idStr &definition, const char* smokeName, const float duration ); + + static float CalculateSoundVolume( const float value, const float min, const float max ); + + static float CalculateScale( const float value, const float min, const float max ); + + static idBounds ScaleBounds( const idBounds& bounds, float scale ); + + static idVec3 DetermineOppositePointOnBounds( const idVec3& start, const idVec3& dir, const idBounds& bounds ); + + template< class Type > + static void Swap( Type& Val1, Type& Val2 ); + + template< class listType > static ID_INLINE void RemoveContents( idList& list, bool clear ) { + for( int index = list.Num() - 1; index >= 0; --index ) { + SAFE_REMOVE( list[index] ); + } + if( clear ) { + list.Clear(); + } else { + memset( list.Ptr(), 0, list.MemoryUsed() ); + } + } +}; + +/* +================ +hhUtils::SwapXZ + +HUMANHEAD: aob +================ +*/ +ID_INLINE idMat3 hhUtils::SwapXZ( const idMat3& axis ) { + return idMat3( -axis[2], axis[1], axis[0] ); +} + + +/* +=============== +hhMath::Swap +=============== +*/ +template< class Type > +ID_INLINE void hhUtils::Swap( Type& Val1, Type& Val2 ) { + Type Temp; + + Temp = Val1; + Val1 = Val2; + Val2 = Temp; +} + +template< class Type > +class hhCycleList { + public: + hhCycleList(); + virtual ~hhCycleList(); + + void Clear(); + void Append( Type& obj ); + void AddUnique( Type& obj ); + const Type& Next(); + const Type& Previous(); + const Type& Random(); + const Type& Get() const; + int Num() const; + //rww - needed for networking sync + int GetCurrentIndex(void) { return currentIndex; } + void SetCurrentIndex(int newIndex) { currentIndex = newIndex; } + + const Type & operator[]( int index ) const; + Type & operator[]( int index ); + protected: + int currentIndex; + idList list; +}; + +/* +=============== +hhCycleList::hhCycleList +=============== +*/ +template< class Type > +ID_INLINE hhCycleList::hhCycleList() { + Clear(); +} + +/* +=============== +hhCycleList::~hhCycleList +=============== +*/ +template< class Type > +ID_INLINE hhCycleList::~hhCycleList() { + Clear(); +} + +/* +=============== +hhCycleList::Clear +=============== +*/ +template< class Type > +ID_INLINE void hhCycleList::Clear() { + list.Clear(); + currentIndex = 0; +} + +/* +=============== +hhCycleList::Append +=============== +*/ +template< class Type > +ID_INLINE void hhCycleList::Append( Type& obj ) { + list.Append( obj ); +} + +/* +=============== +hhCycleList::AddUnique +=============== +*/ +template< class Type > +ID_INLINE void hhCycleList::AddUnique( Type& obj ) { + list.AddUnique( obj ); +} + +/* +=============== +hhCycleList::Next +=============== +*/ +template< class Type > +ID_INLINE const Type& hhCycleList::Next() { + assert( list.Num() ); + + currentIndex = (currentIndex + 1) % list.Num(); + return Get(); +} + +/* +=============== +hhCycleList::Previous +=============== +*/ +template< class Type > +ID_INLINE const Type& hhCycleList::Previous() { + --currentIndex; + if( currentIndex < 0 ) { + currentIndex = (list.Num() - 1); + } + return Get(); +} + +/* +=============== +hhCycleList::Random +=============== +*/ +template< class Type > +ID_INLINE const Type& hhCycleList::Random() { + currentIndex = gameLocal.random.RandomInt( list.Num() ); + return Get(); +} + +/* +=============== +hhCycleList::Get +=============== +*/ +template< class Type > +ID_INLINE const Type& hhCycleList::Get() const { + return list[currentIndex]; +} + +/* +=============== +hhCycleList::Num +=============== +*/ +template< class Type > +ID_INLINE int hhCycleList::Num() const { + return list.Num(); +} + +/* +=============== +hhCycleList::operator[] +=============== +*/ +template< class Type > +ID_INLINE const Type & hhCycleList::operator[]( int index ) const { + return list[index]; +} + +/* +=============== +hhCycleList::operator[] +=============== +*/ +template< class Type > +ID_INLINE Type & hhCycleList::operator[]( int index ) { + return list[index]; +} + +#endif /* __GAME_UTILS_H__ */ diff --git a/src/Prey/game_vehicle.cpp b/src/Prey/game_vehicle.cpp new file mode 100644 index 0000000..cfd827e --- /dev/null +++ b/src/Prey/game_vehicle.cpp @@ -0,0 +1,1922 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//========================================================================== +// +// hhVehicleThruster +// +//========================================================================== +CLASS_DECLARATION( idEntity, hhVehicleThruster ) + EVENT( EV_Broadcast_AssignFx, hhVehicleThruster::Event_AssignFxSmoke ) +END_CLASS + +hhVehicleThruster::hhVehicleThruster() { + fxSmoke = NULL; +} + +void hhVehicleThruster::Spawn() { + owner = NULL; + GetPhysics()->SetContents(0); + soundDistance = spawnArgs.GetFloat("soundDistance"); + bSomeThrusterActive = false; + bSoundMaster = spawnArgs.GetBool("soundmaster"); + localVelocity = localOffset = localDirection = vec3_origin; + fl.networkSync = true; +} + +void hhVehicleThruster::Save(idSaveGame *savefile) const { + owner.Save(savefile); + fxSmoke.Save(savefile); + savefile->WriteVec3(localOffset); + savefile->WriteVec3(localDirection); + savefile->WriteFloat(soundDistance); + savefile->WriteBool(bSomeThrusterActive); + savefile->WriteBool(bSoundMaster); + savefile->WriteVec3(localVelocity); +} + +void hhVehicleThruster::Restore( idRestoreGame *savefile ) { + owner.Restore(savefile); + fxSmoke.Restore(savefile); + savefile->ReadVec3(localOffset); + savefile->ReadVec3(localDirection); + savefile->ReadFloat(soundDistance); + savefile->ReadBool(bSomeThrusterActive); + savefile->ReadBool(bSoundMaster); + savefile->ReadVec3(localVelocity); +} + +void hhVehicleThruster::WriteToSnapshot( idBitMsgDelta &msg ) const { + WriteBindToSnapshot(msg); + GetPhysics()->WriteToSnapshot(msg); + + msg.WriteBits(owner.GetSpawnId(), 32); + msg.WriteBits(fxSmoke.GetSpawnId(), 32); + + msg.WriteFloat(localOffset.x); + msg.WriteFloat(localOffset.y); + msg.WriteFloat(localOffset.z); + + msg.WriteFloat(localDirection.x); + msg.WriteFloat(localDirection.y); + msg.WriteFloat(localDirection.z); + + msg.WriteFloat(soundDistance); + + msg.WriteBits(bSomeThrusterActive, 1); + msg.WriteBits(bSoundMaster, 1); + + msg.WriteFloat(localVelocity.x); + msg.WriteFloat(localVelocity.y); + msg.WriteFloat(localVelocity.z); + + msg.WriteBits(IsHidden(), 1); +} + +void hhVehicleThruster::ReadFromSnapshot( const idBitMsgDelta &msg ) { + ReadBindFromSnapshot(msg); + GetPhysics()->ReadFromSnapshot(msg); + + owner.SetSpawnId(msg.ReadBits(32)); + fxSmoke.SetSpawnId(msg.ReadBits(32)); + + localOffset.x = msg.ReadFloat(); + localOffset.y = msg.ReadFloat(); + localOffset.z = msg.ReadFloat(); + + localDirection.x = msg.ReadFloat(); + localDirection.y = msg.ReadFloat(); + localDirection.z = msg.ReadFloat(); + + soundDistance = msg.ReadFloat(); + + bSomeThrusterActive = !!msg.ReadBits(1); + bSoundMaster = !!msg.ReadBits(1); + + localVelocity.x = msg.ReadFloat(); + localVelocity.y = msg.ReadFloat(); + localVelocity.z = msg.ReadFloat(); + + bool hidden = !!msg.ReadBits(1); + if (hidden != IsHidden()) { + if (hidden) { + Hide(); + } else { + Show(); + } + } + + if (fxSmoke.IsValid() && fxSmoke.GetEntity() && fxSmoke->IsType(idEntityFx::Type)) { + if (fxSmoke->IsHidden() != IsHidden()) { + fxSmoke->BecomeActive( TH_THINK ); + if (IsHidden()) { + fxSmoke->Nozzle(false); + fxSmoke->fl.hidden = true; + } else { + fxSmoke->Nozzle(true); + fxSmoke->fl.hidden = false; + } + } + } +} + +void hhVehicleThruster::ClientPredictionThink( void ) { + Think(); +} + +void hhVehicleThruster::SetSmoker(bool bSmoker, idVec3 &offset, idVec3 &dir) { + localOffset = offset; + localDirection = dir; + if ( bSmoker && (!fxSmoke.IsValid() || !fxSmoke.GetEntity()) ) { + hhFxInfo fxInfo; + const char *smokeName; + smokeName = spawnArgs.GetString( "fx_smoke" ); + if (smokeName && *smokeName) { + fxInfo.SetStart(false); + fxInfo.RemoveWhenDone(false); + fxInfo.SetEntity( this ); + idVec3 loc = owner->GetOrigin() + localOffset * owner->GetAxis(); + idMat3 axis = (localDirection * owner->GetAxis()).ToMat3(); + BroadcastFxInfo( smokeName, loc, axis, &fxInfo, &EV_Broadcast_AssignFx, false ); + } + } + else if (!bSmoker && fxSmoke.IsValid() && fxSmoke.GetEntity()) { + fxSmoke->PostEventMS(&EV_Remove, 0); + fxSmoke = NULL; + } +} + +void hhVehicleThruster::SetThruster(bool on) { + + if (fxSmoke.IsValid() && fxSmoke.GetEntity()) { + fxSmoke->Nozzle( on ); + } + + on ? Show() : Hide(); +} + +void hhVehicleThruster::SetDying(bool bDying) { + if (bSoundMaster) { + SetSmoker(true, localOffset, localDirection); + } + else { + SetSmoker(bDying, localOffset, localDirection); + } +} + +void hhVehicleThruster::Update( const idVec3 &vel ) { + if (bSoundMaster) { + localVelocity = vel; + bool on = localVelocity != vec3_origin; + if (on && !bSomeThrusterActive) { + // Just became active + StartSound("snd_thrust", SND_CHANNEL_THRUSTERS); + bSomeThrusterActive = true; + } + else if (!on) { + // Just became inactive + StopSound(SND_CHANNEL_THRUSTERS); + bSomeThrusterActive = false; + } + } + +} + +bool hhVehicleThruster::GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis ) { + idVec3 toSound; + idVec3 toPilot; + if (owner.IsValid()) { + axis.Identity(); + toSound = -localVelocity; // Position thrust sounds in the opposite direction as thrust velocity + toSound.Normalize(); + toPilot = (owner->GetOrigin() - GetOrigin()) + (owner->spawnArgs.GetVector("offset_pilot") + idVec3(0,0,1)*owner->spawnArgs.GetFloat("pilot_eyeHeight"))*owner->GetAxis(); + origin = toPilot + (toSound * owner->GetAxis() * soundDistance); + return true; + } + return false; + +} + +void hhVehicleThruster::Event_AssignFxSmoke( hhEntityFx* fx ) { + fxSmoke = fx; +} + +//========================================================================== +// +// hhPilotVehicleInterface +// +//========================================================================== +CLASS_DECLARATION( idClass, hhPilotVehicleInterface ) +END_CLASS + +hhPilotVehicleInterface::hhPilotVehicleInterface() { + UnderScriptControl( false ); +} + +//rww - added to handle when a player is destroyed before a vehicle (only in mp i guess) +hhPilotVehicleInterface::~hhPilotVehicleInterface() { + if (pilot.IsValid() && pilot.GetEntity()) { + pilot->SetVehicleInterface(NULL); + } + if (vehicle.IsValid() && vehicle.GetEntity()) { + vehicle->SetPilotInterface(NULL); + } +} + +void hhPilotVehicleInterface::Save(idSaveGame *savefile) const { + pilot.Save(savefile); + vehicle.Save(savefile); + savefile->WriteBool(underScriptControl); +} + +void hhPilotVehicleInterface::Restore( idRestoreGame *savefile ) { + pilot.Restore(savefile); + vehicle.Restore(savefile); + savefile->ReadBool(underScriptControl); +} + +void hhPilotVehicleInterface::RetrievePilotInput( usercmd_t& cmds, idAngles& viewAngles ) { + if( pilot.IsValid() ) { + pilot->GetPilotInput( cmds, viewAngles ); + } +} + +void hhPilotVehicleInterface::TakeControl( hhVehicle* theVehicle, idActor* thePilot ) { + vehicle = theVehicle; + pilot = thePilot; + + if( theVehicle ) { + theVehicle->AcceptPilot( this ); + } +} + +void hhPilotVehicleInterface::ReleaseControl() { + if (vehicle.IsValid()) { + vehicle->ReleaseControl(); + } + vehicle = NULL; + pilot = NULL; +} + +idVec3 hhPilotVehicleInterface::DeterminePilotOrigin() const { + assert( vehicle.IsValid() ); + + return vehicle->DeterminePilotOrigin(); +} + +idMat3 hhPilotVehicleInterface::DeterminePilotAxis() const { + assert( vehicle.IsValid() ); + + return vehicle->DeterminePilotAxis(); +} + +bool hhPilotVehicleInterface::ControllingVehicle() const { + return vehicle.IsValid() && vehicle->IsVehicle(); +} + +hhVehicle* hhPilotVehicleInterface::GetVehicle() const { + return vehicle.GetEntity(); +} + +idActor* hhPilotVehicleInterface::GetPilot() const { + return pilot.GetEntity(); +} + +bool hhPilotVehicleInterface::InvalidVehicleImpulse( int impulse ) { + switch( impulse ) { + case IMPULSE_0: + case IMPULSE_1: + case IMPULSE_2: + case IMPULSE_3: + case IMPULSE_4: + case IMPULSE_5: + case IMPULSE_6: + case IMPULSE_7: + case IMPULSE_8: + case IMPULSE_9: + case IMPULSE_10: + case IMPULSE_11: + case IMPULSE_12: + case IMPULSE_13: + case IMPULSE_14: + case IMPULSE_15: + case IMPULSE_16: + case IMPULSE_19: + case IMPULSE_25: + case IMPULSE_40: + case IMPULSE_54: + return true; + } + + return false; +} + +//========================================================================== +// +// hhAIVehicleInterface +// +//========================================================================== +CLASS_DECLARATION( hhPilotVehicleInterface, hhAIVehicleInterface ) +END_CLASS + +hhAIVehicleInterface::hhAIVehicleInterface(void) { + stateFiring = false; + stateAltFiring = false; +} + +void hhAIVehicleInterface::Save(idSaveGame *savefile) const { + savefile->WriteUsercmd(bufferedCmds); + savefile->WriteAngles(bufferedViewAngles); + savefile->WriteBool(stateFiring); + savefile->WriteBool(stateAltFiring); + savefile->WriteFloat(stateOrientSpeed); + savefile->WriteFloat(stateThrustSpeed); + savefile->WriteVec3(stateOrientDestination); + savefile->WriteVec3(stateThrustDestination); + +} + +void hhAIVehicleInterface::Restore( idRestoreGame *savefile ) { + savefile->ReadUsercmd(bufferedCmds); + savefile->ReadAngles(bufferedViewAngles); + savefile->ReadBool(stateFiring); + savefile->ReadBool(stateAltFiring); + savefile->ReadFloat(stateOrientSpeed); + savefile->ReadFloat(stateThrustSpeed); + savefile->ReadVec3(stateOrientDestination); + savefile->ReadVec3(stateThrustDestination); +} + +void hhAIVehicleInterface::TakeControl( hhVehicle* vehicle, idActor* pilot ) { + hhPilotVehicleInterface::TakeControl( vehicle, pilot ); + + ClearBufferedCmds(); + bufferedViewAngles = (pilot) ? pilot->GetAxis().ToAngles() : ang_zero; +} + +void hhAIVehicleInterface::Fire(bool on) { + stateFiring = on; +} + +void hhAIVehicleInterface::AltFire(bool on) { + stateAltFiring = on; +} + +void hhAIVehicleInterface::OrientTowards( const idVec3 &loc, float speed ) { + stateOrientDestination = loc; + stateOrientSpeed = idMath::ClampFloat( 0.0f, 1.0f, speed ); +} + +void hhAIVehicleInterface::ThrustTowards( const idVec3 &loc, float speed ) { + stateThrustDestination = loc; + stateThrustSpeed = idMath::ClampFloat( 0.0f, 1.0f, speed ); +} + +void hhAIVehicleInterface::RetrievePilotInput( usercmd_t& cmds, idAngles& viewAngles ) { + + if( !ControllingVehicle() ) { + return; + } + + // First grab any buffered commands + cmds = bufferedCmds; + viewAngles = bufferedViewAngles; + + idVec3 eyePos = GetPilot()->GetEyePosition(); + + // Now, Override the buffered input with any script/ai requests + // Based on current script requests, construct a command packet + cmds.buttons |= (stateFiring ? BUTTON_ATTACK : 0); + cmds.buttons |= (stateAltFiring ? BUTTON_ATTACK_ALT : 0); + + // Handle thrusting + if ( stateThrustSpeed > 0.0f ) { + idVec3 targetDir = stateThrustDestination - eyePos; + + // Use proportional control system to ease into destination + const float proportionalGain = 0.7f; + float error = idMath::ClampFloat(0.0f, 1.0f, targetDir.Normalize() / 512.0f); // error only taken into consideration when within 512 units of destination + if (error < 1.0f) { + error *= proportionalGain; + } + + targetDir *= vehicle->GetAxis().Inverse(); + + cmds.forwardmove = targetDir.x * 127.0f * error * stateThrustSpeed; + cmds.rightmove = -targetDir.y * 127.0f * error * stateThrustSpeed; + cmds.upmove = targetDir.z * 127.0f * error * stateThrustSpeed; + } + + // Handle orienting: Using direction vectors to interpolate our orientation to our moving target orientation + viewAngles.pitch = -idMath::AngleNormalize180( vehicle->GetAxis()[0].ToPitch() ); + viewAngles.yaw = vehicle->GetAxis()[0].ToYaw(); + viewAngles.roll = 0.0f; + + if ( stateOrientSpeed > 0.0f ) { + idVec3 localDir; + idAngles idealViewAngles( ang_zero ); + idVec3 targetDir = stateOrientDestination - eyePos; + targetDir.Normalize(); + float degrees = stateOrientSpeed * 360.0f * MS2SEC(gameLocal.msec); + + vehicle->GetPhysics()->GetAxis().ProjectVector( targetDir, localDir ); + idealViewAngles.yaw = localDir.ToYaw(); + idealViewAngles.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ); + + idAngles deltaViewAngles = (idealViewAngles - viewAngles).Normalize180(); + deltaViewAngles.Clamp( idAngles(-degrees, -degrees, 0.0f), idAngles(degrees, degrees, 0.0f) ); + if( !deltaViewAngles.Compare(ang_zero, VECTOR_EPSILON) ) { + viewAngles += deltaViewAngles; + bufferedViewAngles = viewAngles; // Save for next time + } + } +} + +void hhAIVehicleInterface::BufferPilotCmds( const usercmd_t* cmds, const idAngles* viewAngles ) { + if( cmds ) { + bufferedCmds = *cmds; + } + + if( viewAngles ) { + bufferedViewAngles = *viewAngles; + } +} + +void hhAIVehicleInterface::ClearBufferedCmds() { + memset( &bufferedCmds, 0, sizeof(usercmd_t) ); + stateFiring = false; + stateAltFiring = false; + stateOrientSpeed = 0.0f; + stateThrustSpeed = 0.0f; +} + +bool hhAIVehicleInterface::IsVehicleDocked() const { + return vehicle.IsValid() && vehicle->IsDocked(); +} + +//========================================================================== +// +// hhPlayerVehicleInterface +// +//========================================================================== +CLASS_DECLARATION( hhPilotVehicleInterface, hhPlayerVehicleInterface ) +END_CLASS + +hhPlayerVehicleInterface::hhPlayerVehicleInterface() { + hud = NULL; + uniqueHud = true; + translationAlpha.Init(gameLocal.time, 0, 0.0f, 0.0f); +} + +hhPlayerVehicleInterface::~hhPlayerVehicleInterface() { +} + +void hhPlayerVehicleInterface::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject(weaponHandState); + controlHand.Save(savefile); + savefile->WriteBool(uniqueHud); + savefile->WriteUserInterface(hud, uniqueHud); + + savefile->WriteFloat( translationAlpha.GetStartTime() ); // idInterpolate + savefile->WriteFloat( translationAlpha.GetDuration() ); + savefile->WriteFloat( translationAlpha.GetStartValue() ); + savefile->WriteFloat( translationAlpha.GetEndValue() ); +} + +void hhPlayerVehicleInterface::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadStaticObject(weaponHandState); + controlHand.Restore(savefile); + savefile->ReadBool(uniqueHud); + savefile->ReadUserInterface(hud); + + savefile->ReadFloat( set ); // idInterpolate + translationAlpha.SetStartTime( set ); + savefile->ReadFloat( set ); + translationAlpha.SetDuration( set ); + savefile->ReadFloat( set ); + translationAlpha.SetStartValue(set); + savefile->ReadFloat( set ); + translationAlpha.SetEndValue( set ); +} + +void hhPlayerVehicleInterface::UpdateControlHand( const usercmd_t& cmds ) { + if( controlHand.IsValid() ) { + controlHand->UpdateControlDirection( idVec3(Sign(cmds.forwardmove), Sign(cmds.rightmove), Sign(cmds.upmove)) ); + } +} + +void hhPlayerVehicleInterface::CreateControlHand( hhPlayer* pilot, const char* handName ) { + //RemoveHand(); + + if( !handName || !handName[0] ) { + return; + } + + weaponHandState.SetPlayer( pilot ); + weaponHandState.SetWeaponTransition( 1 ); + weaponHandState.Archive( NULL, 0, handName, 1 ); + + if ( pilot->hand.IsValid() && pilot->hand->IsType( hhControlHand::Type ) ) { + controlHand = static_cast( pilot->hand.GetEntity() ); + } + else { + controlHand = NULL; + } + + //controlHand = static_cast( hhControlHand::AddHand(pilot, handName) ); +} + +void hhPlayerVehicleInterface::RemoveHand() { + if( controlHand.IsValid() ) { + //controlHand->RemoveHand(); + controlHand = NULL; + } + weaponHandState.RestoreFromArchive(); +} + +void hhPlayerVehicleInterface::RetrievePilotInput( usercmd_t& cmds, idAngles& viewAngles ) { + if( pilot.IsValid() ) { + hhPilotVehicleInterface::RetrievePilotInput( cmds, viewAngles ); + UpdateControlHand( cmds ); + } +} + +void hhPlayerVehicleInterface::TakeControl( hhVehicle* vehicle, idActor* pilot ) { + hhPilotVehicleInterface::TakeControl( vehicle, pilot ); + + if( vehicle ) { + idStr hudName = vehicle->spawnArgs.GetString( "gui_hud" ); + if( hudName.Length() ) { + uniqueHud = vehicle->spawnArgs.GetBool("uniqueguihud", "1"); + hud = uiManager->FindGui( hudName.c_str(), true, uniqueHud, !uniqueHud ); + + bool isPilotLocal = false; + idPlayer *localPl = gameLocal.GetLocalPlayer(); + if (localPl) { + if (localPl->entityNumber == pilot->entityNumber || (localPl->spectating && localPl->spectator == pilot->entityNumber)) { + isPilotLocal = true; + } + } + if (isPilotLocal) { + translationAlpha.Init( gameLocal.time, 0, 0.0f, 0.0f ); + hud->Activate( true, gameLocal.GetTime() ); + pilot->fl.clientEvents = true; //rww - hack + pilot->PostEventMS( &EV_StartHudTranslation, 1000 ); + pilot->fl.clientEvents = false; //rww - hack + } + } + + if (!gameLocal.isClient) { + CreateControlHand( static_cast(pilot), vehicle->spawnArgs.GetString("def_hand") ); + } + } +} + +void hhPlayerVehicleInterface::ReleaseControl() { + if( vehicle.IsValid() ) { + hud = NULL; + uniqueHud = true; + if (!gameLocal.isClient) { + RemoveHand(); + } else { + if (controlHand.IsValid() && controlHand.GetEntity()) { + controlHand->Hide(); + } + } + } + + hhPilotVehicleInterface::ReleaseControl(); +} + +void hhPlayerVehicleInterface::DrawHUD( idUserInterface* _hud ) { + if( vehicle.IsValid() && vehicle->IsVehicle() && _hud ) { + float alpha = translationAlpha.GetCurrentValue(gameLocal.GetTime()); + _hud->SetStateFloat( "translationAlpha", alpha ); + _hud->SetStateBool( "translationProject", false ); + + vehicle->DrawHUD( _hud ); + } +} + +void hhPlayerVehicleInterface::StartHUDTranslation() { + translationAlpha.Init( gameLocal.time, 1000, 0.0f, 1.0f ); +} + + +//========================================================================== +// +// hhVehicle +// +//========================================================================== + +const idEventDef EV_VehicleExplode("", "efff"); +const idEventDef EV_Vehicle_FireCannon( "fireCannon" ); + +const idEventDef EV_VehicleGetIn("getIn", "e"); // Script Commands +const idEventDef EV_VehicleGetOut("getOut"); +const idEventDef EV_VehicleFire("fire", "d"); +const idEventDef EV_VehicleAltFire("altFire", "d"); +const idEventDef EV_VehicleOrientTowards("orientTowards", "vf"); +const idEventDef EV_VehicleStopOrientingTOwards("stopOrientingTowards"); +const idEventDef EV_VehicleThrustTowards("thrustTowards", "vf"); +const idEventDef EV_VehicleStopThrustingTOwards("stopThrustingTowards"); +const idEventDef EV_VehicleReleaseControl("releaseControl"); +const idEventDef EV_Vehicle_EjectPilot("ejectPilot"); + +ABSTRACT_DECLARATION( hhRenderEntity, hhVehicle ) + EVENT( EV_Vehicle_FireCannon, hhVehicle::Event_FireCannon ) + EVENT( EV_VehicleExplode, hhVehicle::Event_Explode ) + EVENT( EV_VehicleGetIn, hhVehicle::Event_GetIn ) + EVENT( EV_VehicleGetOut, hhVehicle::Event_GetOut ) + EVENT( EV_VehicleFire, hhVehicle::Event_Fire ) + EVENT( EV_VehicleAltFire, hhVehicle::Event_AltFire ) + EVENT( EV_VehicleOrientTowards, hhVehicle::Event_OrientTowards ) + EVENT( EV_VehicleThrustTowards, hhVehicle::Event_ThrustTowards ) + EVENT( EV_VehicleReleaseControl, hhVehicle::Event_ReleaseScriptControl ) + EVENT( EV_Vehicle_EjectPilot, hhVehicle::Event_EjectPilot ) + EVENT( EV_ResetGravity, hhVehicle::Event_ResetGravity ) +END_CLASS + +hhVehicle::~hhVehicle() { + if (GetPilotInterface()) { + idActor *actor = GetPilotInterface()->GetPilot(); + if (actor && actor->IsType(idActor::Type)) { //if it still has a valid pilot, eject them. + EjectPilot(); + } + } +} + +void hhVehicle::Spawn() { + memset( &oldCmds, 0, sizeof(usercmd_t) ); + thrusterCost = spawnArgs.GetInt( "thrusterCost" ); + currentPower = spawnArgs.GetInt( "maxPower" ); + thrustFactor = spawnArgs.GetFloat( "thrustFactor" ); + dockBoostFactor = spawnArgs.GetFloat( "dockBoostFactor" ); + if ( gameLocal.isMultiplayer ) { + noDamage = false; + } else { + noDamage = spawnArgs.GetBool( "noDamage", "0" ); + } + + bDamageSelfOnCollision = spawnArgs.GetBool( "damageSelfOnCollision" ); + bDamageOtherOnCollision = spawnArgs.GetBool( "damageOtherOnCollision" ); + + lastAttackTime = 0; + fl.neverDormant = false; + fl.refreshReactions = true; + fireController = NULL; + pilotInterface = NULL; + bHeadlightOn = false; + validThrustTime = gameLocal.time; + + bDisallowAttackUntilRelease = false; + bDisallowAltAttackUntilRelease = false; + + InitializeAttackFuncs(); + + //rww - i see no reason why the dock should be set upon spawning the vehicle. + //also would cause issues in mp where only one shuttle should be docked at a time. + //startDock is set by the dock who created us + /* + hhDock *dock = (hhDock *)gameLocal.FindEntity(spawnArgs.GetString("startDock")); + SetDock( dock ); + if (dock && dock->IsType(hhShuttleDock::Type)) { + hhShuttleDock *shDock = static_cast(dock); + if (shDock->GetDockedShuttle()) { //if the dock already has a shuttle, do not dock this ship there. + SetDock(NULL); + } + } + */ + + InitPhysics(); + + BecomeConsole(); + + physicsObj.SetOrigin( GetPhysics()->GetOrigin() ); + + SetAxis( GetPhysics()->GetAxis() ); + physicsObj.SetAxis( GetPhysics()->GetAxis() ); + SetPhysics( &physicsObj ); + + if ( spawnArgs.GetInt( "nodrop" ) ) { + physicsObj.PutToRest(); + } + else { + physicsObj.DropToFloor(); + } +} + +void hhVehicle::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject(physicsObj); + savefile->WriteMat3(modelAxis); + + if( fireController ) { + savefile->WriteBool(true); + savefile->WriteStaticObject(*fireController); + } else { + savefile->WriteBool(false); + } + + savefile->WriteUsercmd(oldCmds); + + headlight.Save(savefile); + domelight.Save(savefile); + + savefile->WriteBool(bHeadlightOn); + savefile->WriteInt(currentPower); + savefile->WriteFloat(thrustFactor); + savefile->WriteFloat(thrustMin); + savefile->WriteFloat(thrustMax); + savefile->WriteFloat(thrustAccel); + savefile->WriteFloat(thrustScale); + savefile->WriteFloat(dockBoostFactor); + + dock.Save(savefile); + lastAttacker.Save(savefile); + + savefile->WriteInt(lastAttackTime); + savefile->WriteInt(vehicleClipMask); + savefile->WriteInt(vehicleContents); + savefile->WriteBool(bDamageSelfOnCollision); + savefile->WriteBool(bDamageOtherOnCollision); + savefile->WriteBool(bDisallowAttackUntilRelease); + savefile->WriteBool(bDisallowAltAttackUntilRelease); + savefile->WriteInt(thrusterCost); + savefile->WriteInt(validThrustTime); + + savefile->WriteEventDef(attackFunc); + savefile->WriteEventDef(finishedAttackingFunc); + savefile->WriteEventDef(altAttackFunc); + savefile->WriteEventDef(finishedAltAttackingFunc); + + savefile->WriteBool(noDamage); +} + +void hhVehicle::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject(physicsObj); + RestorePhysics(&physicsObj); + + savefile->ReadMat3(modelAxis); + + bool test; + savefile->ReadBool( test ); + if (test) { + fireController = CreateFireController(); + savefile->ReadStaticObject(*fireController); + } else { + SAFE_DELETE_PTR(fireController); + } + + savefile->ReadUsercmd(oldCmds); + + headlight.Restore(savefile); + domelight.Restore(savefile); + + savefile->ReadBool(bHeadlightOn); + savefile->ReadInt(currentPower); + savefile->ReadFloat(thrustFactor); + savefile->ReadFloat(thrustMin); + savefile->ReadFloat(thrustMax); + savefile->ReadFloat(thrustAccel); + savefile->ReadFloat(thrustScale); + savefile->ReadFloat(dockBoostFactor); + + dock.Restore(savefile); + lastAttacker.Restore(savefile); + + savefile->ReadInt(lastAttackTime); + savefile->ReadInt(vehicleClipMask); + savefile->ReadInt(vehicleContents); + savefile->ReadBool(bDamageSelfOnCollision); + savefile->ReadBool(bDamageOtherOnCollision); + savefile->ReadBool(bDisallowAttackUntilRelease); + savefile->ReadBool(bDisallowAltAttackUntilRelease); + savefile->ReadInt(thrusterCost); + savefile->ReadInt(validThrustTime); + + savefile->ReadEventDef(attackFunc); + savefile->ReadEventDef(finishedAttackingFunc); + savefile->ReadEventDef(altAttackFunc); + savefile->ReadEventDef(finishedAltAttackingFunc); + + savefile->ReadBool(noDamage); +} + +const idEventDef* hhVehicle::GetAttackFunc( const char* funcName ) { + function_t* function = NULL; + + if( !funcName || !funcName[0] ) { + return NULL; + } + + idTypeDef *funcType = gameLocal.program.FindType(funcName); + if (!funcType) { + return NULL; + } + + function = gameLocal.program.FindFunction( funcName, funcType ); + if( !function || !function->eventdef ) { + return NULL; + } + + HH_ASSERT( RespondsTo(*function->eventdef) ); + + return function->eventdef; +} + +void hhVehicle::InitializeAttackFuncs() { + idStr funcName; + attackFunc = finishedAttackingFunc = altAttackFunc = finishedAltAttackingFunc = NULL; + + funcName = spawnArgs.GetString("attackFunc"); + if (funcName.Length()) { + attackFunc = GetAttackFunc( funcName.c_str() ); + finishedAttackingFunc = GetAttackFunc( (funcName + "Done").c_str() ); + } + funcName = spawnArgs.GetString("altAttackFunc"); + if (funcName.Length()) { + altAttackFunc = GetAttackFunc( funcName.c_str() ); + finishedAltAttackingFunc = GetAttackFunc( (funcName + "Done").c_str() ); + } +} + +void hhVehicle::Think() { + usercmd_t pilotCmds; + idAngles pilotViewAngles; + + if( IsVehicle() && GetPilotInterface() ) { + GetPilotInterface()->RetrievePilotInput( pilotCmds, pilotViewAngles ); + ProcessPilotInput( &pilotCmds, &pilotViewAngles ); + GetPhysics()->SetAxis( mat3_identity ); + } + + hhRenderEntity::Think(); +} + +void hhVehicle::Present() { + if (fireController) { + fireController->UpdateMuzzleFlash(); + } + + hhRenderEntity::Present(); +} + +void hhVehicle::AcceptPilot( hhPilotVehicleInterface* pilotInterface ) { + //assure that the vehicle is showing upon accepting a new pilot + Show(); + + this->pilotInterface = pilotInterface; + + fl.neverDormant = true; + fl.takedamage = true; + StartSound( "snd_activation", SND_CHANNEL_ANY ); + StartSound( "snd_inuse", SND_CHANNEL_MISC1 ); + + CreateDomeLight(); + CreateHeadLight(); + BecomeActive( TH_TICKER ); + + const idDict* infoDict = NULL; + if ( GetPilot() && GetPilot()->IsType(idAI::Type) ) { + infoDict = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_fireInfoAI"), false ); + } else { + infoDict = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_fireInfo"), false ); + } + fireController = CreateFireController(); + HH_ASSERT( fireController ); + fireController->Init( infoDict, this, GetPilot() ); + + // supress model in player views, but allow it in mirrors and remote views + if (GetPilot()->IsType(hhPlayer::Type)) { + GetRenderEntity()->suppressSurfaceInViewID = GetPilot()->entityNumber + 1; + } + + BecomeVehicle(); +} + +void hhVehicle::RestorePilot( hhPilotVehicleInterface* pilotInterface) { + const idDict *infoDict = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_fireInfo"), false ); + assert( infoDict ); + fireController->SetWeaponDict( infoDict ); + this->pilotInterface = pilotInterface; +} + + +void hhVehicle::EjectPilot() { + assert( IsVehicle() ); + + SAFE_DELETE_PTR( fireController ); + + fl.takedamage = false; + fl.neverDormant = false; + StopSound( SND_CHANNEL_MISC1 ); + StartSound( "snd_deactivation", SND_CHANNEL_ANY ); + + FreeDomeLight(); + FreeHeadLight(); + BecomeInactive( TH_TICKER ); + + + if (GetPilotInterface()->GetPilot()) { //rww - make sure pilot is valid + GetPilotInterface()->GetPilot()->ExitVehicle( this ); + pilotInterface = NULL; + } + + RemoveVehicle(); +} + +idVec3 hhVehicle::DeterminePilotOrigin() const { + return GetOrigin() + spawnArgs.GetVector("offset_pilot") * GetAxis(); +} + +idMat3 hhVehicle::DeterminePilotAxis() const { + return GetAxis(); +} + +void hhVehicle::BecomeVehicle() { + SetVehiclePhysics(); + + SetVehicleModel(); +} + +bool hhVehicle::CanBecomeVehicle(idActor *pilot) { + // Check to see if shuttle will fit + idEntity *touch[ MAX_GENTITIES ]; + idBounds bounds, localBounds; + + idVec3 location = GetOrigin(); + localBounds[0] = spawnArgs.GetVector("mins"); + localBounds[1] = spawnArgs.GetVector("maxs"); + +// idBox box(localBounds, location, GetAxis()); +// gameRenderWorld->DebugBox(colorRed, box, 8000); + + idTraceModel trm; + trm.SetupBox( localBounds ); + idClipModel *clipModel = new idClipModel( trm ); + clipModel->Link(gameLocal.clip, this, 254, location, GetAxis()); + + int pilotClipMask = pilot ? pilot->GetPhysics()->GetClipMask() : 0; + pilotClipMask &= (~CONTENTS_HUNTERCLIP); // Vehicles don't collide with hunterclip so they don't get hung up on dock borders + int num = hhUtils::EntitiesTouchingClipmodel( clipModel, touch, MAX_GENTITIES, CLIPMASK_VEHICLE | pilotClipMask ); + bool blocked = false; + for (int i=0; iGetBindMaster() != pilot && (touch[i]->GetPhysics()->GetContents() & CLIPMASK_VEHICLE)) { + blocked = true; + //gameLocal.Printf("Blocked by %s\n", touch[i]->GetName()); + break; + } + } + + clipModel->Unlink(); + delete clipModel; + + return !blocked; +} + +bool hhVehicle::IsVehicle() const { + return fireController != NULL && GetPilotInterface() && GetPilotInterface()->GetPilot(); +} + +void hhVehicle::BecomeConsole() { + SetConsolePhysics(); + + if( IsDocked() ) { + //Feels like a hack. Shouldn't the dock do this or at least be in SetConsolePhysics + SetAxis( dock->GetAxis() ); + GetPhysics()->SetAxis( dock->GetAxis() ); + SetOrigin( dock->GetOrigin() + dock->spawnArgs.GetVector("offset_console") * dock->GetAxis() ); + + physicsObj.PutToRest(); + } + + SetConsoleModel(); +} + +bool hhVehicle::IsConsole() const { + return !IsVehicle(); +} + +idVec3 hhVehicle::GetPortalPoint() { + idVec3 offset = spawnArgs.GetVector("offset_pilot") + idVec3(0,0,1)*spawnArgs.GetFloat("pilot_eyeHeight"); + return GetOrigin() + offset * GetAxis(); +} + +void hhVehicle::Portalled(idEntity *portal) { + idActor *pilot = GetPilot(); + if (pilot) { + // Update the view angles of the player so next time we get input, it won't reset our angles + if (pilot->IsType(hhPlayer::Type)) { + hhPlayer* player = static_cast( pilot ); + idVec3 origin = DeterminePilotOrigin(); + idVec3 viewDir = GetAxis()[0]; + + // Don't know if all this is necessary, might be overkill, definitely need some of it though for 'shuttle through portal' + player->Unbind(); + player->SetOrientation( origin, mat3_identity, viewDir, viewDir.ToAngles() ); + player->SetUntransformedViewAxis( mat3_identity ); + player->Bind(this, true); + } + } +} + +void hhVehicle::ResetGravity() { + if( IsVehicle() ) { + physicsObj.SetGravity( spawnArgs.GetVector("activeGravity") ); + } + else { + physicsObj.SetGravity( spawnArgs.GetVector("inactiveGravity") ); + } +} + +void hhVehicle::SetAxis( const idMat3& axis ) { + modelAxis = axis; + + UpdateVisuals(); +} + +void hhVehicle::InitPhysics() { + physicsObj.SetSelf( this ); + + physicsObj.SetFriction( + spawnArgs.GetFloat("friction_linear"), + spawnArgs.GetFloat("friction_angular"), + spawnArgs.GetFloat("friction_contact") ); +} + +void hhVehicle::SetConsolePhysics() { + idTraceModel trm; + const char *clipModelName = spawnArgs.GetString( "clipmodel" ); + assert( clipModelName[0] ); + + if ( !collisionModelManager->TrmFromModel( clipModelName, trm ) ) { + gameLocal.Error( "hhVehicle '%s' at (%s): cannot load collision model %s\n", + name.c_str(), GetPhysics()->GetOrigin().ToString(0), clipModelName ); + return; + } + + physicsObj.SetClipModel( new idClipModel(trm), spawnArgs.GetFloat("density") ); + physicsObj.DisableImpact(); + physicsObj.SetBouncyness( 0.0f ); + physicsObj.SetContents( CONTENTS_VEHICLE ); + physicsObj.SetClipMask( CLIPMASK_VEHICLE | CONTENTS_PLAYERCLIP | CONTENTS_MONSTERCLIP ); + physicsObj.SetLinearVelocity( vec3_zero ); + physicsObj.SetAngularVelocity( vec3_zero ); + + //Used as a cache for when noclipping + vehicleClipMask = physicsObj.GetClipMask(); + vehicleContents = physicsObj.GetContents(); + + ResetGravity(); +} + +void hhVehicle::SetVehiclePhysics() { + idBounds bounds( spawnArgs.GetVector("mins"), spawnArgs.GetVector("maxs") ); + + physicsObj.SetClipModel( new idClipModel(idTraceModel(bounds)), spawnArgs.GetFloat("density") ); + physicsObj.SetAxis( mat3_identity ); + physicsObj.EnableImpact(); + physicsObj.SetBouncyness( spawnArgs.GetFloat("bouncyness") ); + physicsObj.SetContents( CONTENTS_VEHICLE ); + + SetThrustBooster( spawnArgs.GetFloat("thrustMin"), spawnArgs.GetFloat("thrustMax"), spawnArgs.GetFloat("thrustAccel") ); + + int pilotClipMask = (GetPilotInterface() && GetPilotInterface()->GetPilot()) ? GetPilotInterface()->GetPilot()->GetPhysics()->GetClipMask() : 0; + pilotClipMask &= (~CONTENTS_HUNTERCLIP); // Vehicles don't collide with hunterclip so they don't get hung up on dock borders + physicsObj.SetClipMask( CLIPMASK_VEHICLE | pilotClipMask ); + + //Used as a cache for when noclipping + vehicleClipMask = physicsObj.GetClipMask(); + vehicleContents = physicsObj.GetContents(); + + ResetGravity(); +} + +void hhVehicle::SetConsoleModel() { + SetModel( spawnArgs.GetString("model") ); +} + +void hhVehicle::SetVehicleModel() { + SetModel( spawnArgs.GetString("model_active") ); +} + +float hhVehicle::CmdScale( const usercmd_t* cmd ) const { + float scale = 0.0f; + + if( !cmd ) { + return scale; + } + + idVec3 abscmd(abs(cmd->forwardmove), abs(cmd->rightmove), abs(cmd->upmove)); + + // Bound the cmd vector to a sphere whose radius is equal to the longest cmd axis + int desiredLength = max(max(abscmd[0], abscmd[1]), abscmd[2]); + if ( desiredLength != 0.0f ) { + float currentLength = abscmd.Length(); + scale = desiredLength / currentLength; + } + + return scale; +} + +void hhVehicle::SetThrustBooster(float minBooster, float maxBooster, float accelBooster) { + thrustMin = minBooster; + thrustMax = maxBooster; + thrustAccel = accelBooster; + + thrustScale = 0.1f; +} + +void hhVehicle::ProcessPilotInput( const usercmd_t* cmds, const idAngles* viewAngles ) { + idVec3 impulse; + + if( viewAngles ) { + SetAxis( viewAngles->ToMat3() ); + } + + if( !cmds ) { + return; + } + + ProcessButtons( *cmds ); + + //Check vehicle here because we could have exited the vehicle in ProcessButtons + if( !IsVehicle() ) { + return; + } + + ProcessImpulses( *cmds ); + + if ( gameLocal.time >= validThrustTime ) { + impulse = GetAxis()[0] * cmds->forwardmove * thrustFactor; + impulse -= GetAxis()[1] * cmds->rightmove * thrustFactor; + impulse += GetAxis()[2] * cmds->upmove * thrustFactor; + } + else { + impulse = vec3_origin; + } + + // Apply booster to allow key taps to be very low thrust + thrustScale = idMath::ClampFloat( thrustMin, thrustMax, thrustScale * thrustAccel ); + if( impulse.LengthSqr() >= VECTOR_EPSILON ) { + FireThrusters( impulse * CmdScale(cmds) * thrustScale * (60.0f * USERCMD_ONE_OVER_HZ) ); + } else { + thrustScale = thrustMin; + } + + memcpy( &oldCmds, cmds, sizeof(usercmd_t) ); +} + +void hhVehicle::ProcessButtons( const usercmd_t& cmds ) { + + //This is needed in case we enter a dock while firing or using the tractor beam + if( (cmds.buttons & BUTTON_ATTACK) && !(oldCmds.buttons & BUTTON_ATTACK) ) { + if (IsDocked() && dock->AllowsExit() && spawnArgs.GetBool("fireToExit")) { + if (!gameLocal.isClient) { + EjectPilot(); + } + return; + } + } else if( (cmds.buttons & BUTTON_ATTACK_ALT) && !(oldCmds.buttons & BUTTON_ATTACK_ALT) ) { + if (IsDocked() && dock->AllowsExit() && spawnArgs.GetBool("altFireToExit")) { + if (!gameLocal.isClient) { + EjectPilot(); + } + return; + } + } + + if (bDisallowAttackUntilRelease) { + if (!(cmds.buttons & BUTTON_ATTACK)) { + bDisallowAttackUntilRelease = false; + } + if( oldCmds.buttons & BUTTON_ATTACK ) { + oldCmds.buttons &= ~BUTTON_ATTACK; + if( finishedAttackingFunc ) { + ProcessEvent( finishedAttackingFunc ); + } + } + } + else { + if( (cmds.buttons & BUTTON_ATTACK) ) { + if( attackFunc ) { + ProcessEvent( attackFunc ); + } + } else if( oldCmds.buttons & BUTTON_ATTACK ) { + if( finishedAttackingFunc ) { + ProcessEvent( finishedAttackingFunc ); + } + } + } + + if (bDisallowAltAttackUntilRelease) { + if (!(cmds.buttons & BUTTON_ATTACK_ALT)) { + bDisallowAltAttackUntilRelease = false; + } + if ( oldCmds.buttons & BUTTON_ATTACK_ALT ) { + oldCmds.buttons &= ~BUTTON_ATTACK_ALT; + if( finishedAltAttackingFunc ) { + ProcessEvent( finishedAltAttackingFunc ); + } + } + } + else { + if( cmds.buttons & BUTTON_ATTACK_ALT ) { + if( altAttackFunc ) { + ProcessEvent( altAttackFunc ); + } + } else if( oldCmds.buttons & BUTTON_ATTACK_ALT ) { + if( finishedAltAttackingFunc ) { + ProcessEvent( finishedAltAttackingFunc ); + } + } + } +} + +void hhVehicle::DoPlayerImpulse(int impulse) { + if ( gameLocal.isClient && GetPilot() && GetPilot()->IsType(hhPlayer::Type) ) { + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + hhPlayer *pl = static_cast(GetPilot()); + + assert( pl->entityNumber == gameLocal.localClientNum ); + msg.Init( msgBuf, sizeof( msgBuf ) ); + msg.BeginWriting(); + msg.WriteBits( impulse, 6 ); + pl->ClientSendEvent( hhPlayer::EVENT_IMPULSE, &msg ); + } + + switch (impulse) { + case IMPULSE_16: + if (!gameLocal.isClient) { + Headlight( !bHeadlightOn ); + } + break; + } +} + +void hhVehicle::ProcessImpulses( const usercmd_t& cmds ) { + if( (cmds.flags & UCF_IMPULSE_SEQUENCE) == (oldCmds.flags & UCF_IMPULSE_SEQUENCE) ) { + return; + } + + DoPlayerImpulse(cmds.impulse); //rww - seperated into its own function because of networked impulse events +} + +void hhVehicle::ApplyImpulse( idEntity* ent, int id, const idVec3& point, const idVec3& impulse ) { + hhRenderEntity::ApplyImpulse( ent, id, point, impulse ); +} + +void hhVehicle::ApplyImpulse( const idVec3& impulse ) { + ApplyImpulse( gameLocal.world, 0, GetOrigin() + physicsObj.GetCenterOfMass(), impulse * physicsObj.GetMass() ); +} + +void hhVehicle::UpdateModel( void ) { + idVec3 origin; + idMat3 axis; + + if ( GetPhysicsToVisualTransform(origin, axis) ) { + //HUMANHEAD: aob + GetRenderEntity()->axis = axis * GetAxis(); + GetRenderEntity()->origin = GetOrigin() + origin * GetRenderEntity()->axis; + //HUMANHEAD END + } else { + //HUMANHEAD: aob + GetRenderEntity()->axis = GetAxis(); + GetRenderEntity()->origin = GetOrigin(); + //HUMANHEAD END + } + + // set to invalid number to force an update the next time the PVS areas are retrieved + ClearPVSAreas(); + + // ensure that we call Present this frame + BecomeActive( TH_UPDATEVISUALS ); +} + +void hhVehicle::CreateDomeLight() { + if (spawnArgs.GetBool("domelight")) { + // This method is more oriented for flexibility + idVec3 light_offset = spawnArgs.GetVector("offset_domelight"); + const char *objName = spawnArgs.GetString("def_domelight"); + + idDict args; + idVec3 lightOrigin = GetPhysics()->GetOrigin() + light_offset * GetPhysics()->GetAxis(); + args.SetVector( "origin", lightOrigin ); + + domelight = (idLight *)gameLocal.SpawnObject(objName, &args); + domelight->Bind( this, true ); + domelight->SetLightParm(SHADERPARM_TIMEOFFSET, -MS2SEC(gameLocal.time)); + } +} + +void hhVehicle::FreeDomeLight() { + SAFE_REMOVE( domelight ); +} + +void hhVehicle::CreateHeadLight() { + if ( spawnArgs.GetBool("headlight") ) { + // This method is more oriented for presenting our own lights + // Depending on the speed cost of having seperate entities for lights, we may + // want to present our own, like the weapons + idStr light_shader = spawnArgs.GetString("mtr_headlight"); + idVec3 light_color = spawnArgs.GetVector("headlight_color"); + idVec3 light_offset = spawnArgs.GetVector("offset_headlight"); + idVec3 light_target = spawnArgs.GetVector("offset_headlighttarget"); + idVec3 light_frustum = spawnArgs.GetVector("headlight_frustum"); + + idDict args; + idVec3 lightOrigin = GetPhysics()->GetOrigin() + light_offset * GetPhysics()->GetAxis(); + light_target.Normalize(); + idMat3 lightAxis = (light_target * GetPhysics()->GetAxis()).hhToMat3(); + + if ( light_shader.Length() ) { + args.Set( "texture", light_shader ); + } + args.SetVector( "origin", lightOrigin ); + args.Set ("angles", lightAxis.ToAngles().ToString()); + args.SetVector( "_color", light_color ); + args.SetVector( "light_target", lightAxis[0] * light_frustum.x ); + args.SetVector( "light_right", lightAxis[1] * light_frustum.y ); + args.SetVector( "light_up", lightAxis[2] * light_frustum.z ); + headlight = ( idLight * )gameLocal.SpawnEntityTypeClient( idLight::Type, &args ); + + //rww - headlight is a pure local entity + assert(headlight.IsValid() && headlight.GetEntity()); + headlight->fl.networkSync = false; + headlight->fl.clientEvents = true; + + headlight->Bind(this, true); + + headlight->SetLightParm( 6, 0.0f ); // fade out + headlight->SetLightParm( SHADERPARM_TIMEOFFSET, 0 ); // Initially faded out already + } +} + +void hhVehicle::FreeHeadLight() { + SAFE_REMOVE( headlight ); +} + +void hhVehicle::Headlight( bool on ) { + bHeadlightOn = on; + float timeOffset = -MS2SEC(gameLocal.time); + + if( headlight.IsValid() ) { + headlight->SetLightParm(SHADERPARM_TIMEOFFSET, timeOffset); + SetShaderParm(7, timeOffset); + + if (bHeadlightOn) { + headlight->SetLightParm(6, 1.0f); // Fade light in + SetShaderParm(6, 1.0f); // Fade light cone in + } + else { + headlight->SetLightParm(6, 0.0f); // Fade out + SetShaderParm(6, 0.0f); // Fade out + } + } +} + +void hhVehicle::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + if (InGodMode() || InDialogMode()) { + return; + } + + if (gameLocal.isMultiplayer) { //rww - don't let shuttles damage themselves or their teammates + if (attacker && attacker->IsType(hhPlayer::Type) && GetPilot() == attacker) { + return; + } + + hhPlayer *playerPilot = (GetPilot() && GetPilot()->IsType( hhPlayer::Type )) ? static_cast(GetPilot()) : NULL; + hhPlayer *player = (attacker && attacker->IsType( hhPlayer::Type )) ? static_cast(attacker) : NULL; + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if ( (gameLocal.gameType == GAME_TDM + && playerPilot + && !gameLocal.serverInfo.GetBool( "si_teamDamage" ) + && damageDef + && !damageDef->GetBool( "noTeam" ) + && player + && player->team == playerPilot->team) ) { + return; + } + } else { + if ( noDamage ) { + return; + } + if (attacker && attacker->IsType(idAI::Type) && GetPilot() == attacker) { + return; + } + } + + if (GetPilotInterface() && GetPilotInterface()->GetPilot()) { + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + + if (gameLocal.isMultiplayer && IsType(hhShuttle::Type)) { //rww - in mp, let's try flickering the tractor beam on and off when hit + hhShuttle *shtlSelf = static_cast(this); + if (shtlSelf->noTractorTime <= gameLocal.time && shtlSelf->TractorIsActive()) { //don't let it stay off for a long time from constant attack + float dmg = ((float)damageDef->GetInt("damage", "100"))*damageScale; + int dmgTime = (int)(dmg*12); + //cap it to something reasonable on both ends + if (dmgTime < 100) { + dmgTime = 100; + } + else if (dmgTime > 1200) { + dmgTime = 1200; + } + shtlSelf->noTractorTime = gameLocal.time + dmgTime; + } + } + + // Let playerview know about the impact direction + if ( !damageDef ) { + gameLocal.Warning( "Unknown damageDef '%s'", damageDefName ); + return; + } + idVec3 damage_from, localDamageVector; + damage_from = dir; + damage_from.Normalize(); + if ( GetPilotInterface()->GetPilot()->IsType(hhPlayer::Type)) { + // Pass this on so we can get directional damage + hhPlayer *playerPilot = static_cast(GetPilotInterface()->GetPilot()); + playerPilot->viewAxis.ProjectVector( damage_from, localDamageVector ); + playerPilot->playerView.DamageImpulse( localDamageVector, damageDef ); + + // Track last attacker for use in displaying HUD hit indicator + if (!gameLocal.isClient) { + playerPilot->ReportAttack(attacker); + } + + if (gameLocal.isMultiplayer) { //rww - in MP, we want to damage the player a little from shuttle damage + float plDmgScale = damageScale*0.2f; + bool pilotWasTakingDamage = playerPilot->fl.takedamage; + playerPilot->fl.takedamage = true; + playerPilot->Damage(inflictor, attacker, dir, damageDefName, plDmgScale, INVALID_JOINT); + playerPilot->fl.takedamage = pilotWasTakingDamage; + if (playerPilot->health == 1) { //if the player died (or almost, kind of a hack) as a result, blow me up too + damageDefName = "damage_suicide"; + } + } + } + } + + // Tell HUD to show impact direction + if( IsVehicle() ) {//AI may want to know who hit them + lastAttacker = attacker; + lastAttackTime = gameLocal.time; + } + idEntity::Damage( inflictor, attacker, dir, damageDefName, 1.0f, location ); +} + +void hhVehicle::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if (gameLocal.isClient) { + return; + } + if( gameLocal.isMultiplayer || health < spawnArgs.GetInt("gibhealth") ) { //rww - always explode in mp + CancelEvents(&EV_VehicleExplode); + PostEventMS(&EV_VehicleExplode, 0, attacker, dir.x, dir.y, dir.z); + } + else if( health + damage > 0 ) { // Only do this the first death + ConsumePower(currentPower); + + if( domelight.IsValid() ) { + domelight->SetLightParm(7, 1.0f); + } + + StopSound( SND_CHANNEL_DYING, true ); + StartSound( "snd_dying", SND_CHANNEL_DYING, 0, true ); + + // control release during the damage pipe was causing a crash when damaged by splash damage + // the clip model is invalidated, then radius damage tries to apply to it. + // Should be okay to switch back if "killed" is made into an event + CancelEvents( &EV_VehicleExplode ); + PostEventSec( &EV_VehicleExplode, spawnArgs.GetFloat("explodedelay"), attacker, dir.x, dir.y, dir.z ); + } +} + +bool hhVehicle::Collide( const trace_t &collision, const idVec3 &velocity ) { + static const float minCollisionVelocity = 40.0f; + static const float maxCollisionVelocity = 650.0f; + + // Velocity in normal direction + float len = velocity * -collision.c.normal; + + if ( len > minCollisionVelocity && collision.c.material && !(collision.c.material->GetSurfaceFlags() & SURF_NOIMPACT) ) { + if (StartSound( "snd_bounce", SND_CHANNEL_BODY3 )) { + // Change volume only after we know the sound played + float volume = hhUtils::CalculateSoundVolume( len, minCollisionVelocity, maxCollisionVelocity ); + HH_SetSoundVolume(volume, SND_CHANNEL_BODY3); + } + + if (len > 200.0f) { + // Damage other + idEntity *other = gameLocal.GetTraceEntity(collision); + if (other && bDamageOtherOnCollision) { + idEntity *collideAttacker = this; + + if (GetPilotInterface() && GetPilotInterface()->GetPilot()) { //use pilot as attacker if we have one + collideAttacker = GetPilotInterface()->GetPilot(); + } + float damageScale = hhUtils::CalculateScale(len, 200, 600); + damageScale = (damageScale * 4.0f) + 1.0f; // damageScale in range [1..5] + damageScale *= other->spawnArgs.GetFloat("vehicledamagescale"); // for fine tuning + + // NOTE: Assuming damage_vehicleCollision is set to 20! + if (20 * damageScale >= other->health) { + other->Damage(this, collideAttacker, vec3_origin, "damage_vehicleCollision_gib", damageScale, 0); + } + else { + other->Damage(this, collideAttacker, vec3_origin, "damage_vehicleCollision", damageScale, 0); + } + } + + // Damage self + if (bDamageSelfOnCollision) { + Damage(other, other, collision.c.normal, "damage_vehicleCollision", 1.0f, 0); + } + } + } + + return false; +} + +bool hhVehicle::IsNoClipping() const { + return GetPilotInterface() && GetPilotInterface()->GetPilot() && GetPilotInterface()->GetPilot()->IsType(hhPlayer::Type) && static_cast(GetPilotInterface()->GetPilot())->noclip; +} + +bool hhVehicle::InGodMode() const { + return GetPilotInterface() && GetPilotInterface()->GetPilot() && GetPilotInterface()->GetPilot()->IsType(hhPlayer::Type) && static_cast(GetPilotInterface()->GetPilot())->godmode; +} + +bool hhVehicle::InDialogMode() const { + return GetPilotInterface() && GetPilotInterface()->GetPilot() && GetPilotInterface()->GetPilot()->IsType(hhPlayer::Type) && static_cast(GetPilotInterface()->GetPilot())->InDialogDamageMode(); +} + +void hhVehicle::GiveHealth( int amount ) { + health = idMath::ClampInt( 0, spawnHealth, health + amount ); +} + +void hhVehicle::GivePower( int amount ) { + currentPower = idMath::ClampInt( 0, spawnArgs.GetInt("maxPower"), currentPower + amount ); +} + +bool hhVehicle::HandleSingleGuiCommand( idEntity *entityGui, idLexer *src ) { + idToken token; + if( !src->ReadToken(&token) || token == ";" ) { + return false; + } + if( !token.Icmp("controlvehicle") ) { + if (IsHidden()) { //rww - was possible to get back in a vehicle once it had been hidden for removal + return true; + } + if( entityGui->IsType(idActor::Type) ) { + static_cast( entityGui )->EnterVehicle( this ); + } + return true; + } + return false; +} + +void hhVehicle::DrawHUD( idUserInterface* _hud ) { + if( _hud ) { + //rww - transfer necessary hud variables from the player hud to the vehicle hud + if (GetPilotInterface() && GetPilotInterface()->GetPilot() && GetPilotInterface()->GetPilot()->IsType(hhPlayer::Type)) { + hhPlayer *plPilot = static_cast(GetPilotInterface()->GetPilot()); + if (plPilot->hud) { + //aiming + _hud->SetStateFloat("aim_R", plPilot->hud->GetStateFloat("aim_R", "")); + _hud->SetStateFloat("aim_G", plPilot->hud->GetStateFloat("aim_G", "")); + _hud->SetStateFloat("aim_B", plPilot->hud->GetStateFloat("aim_B", "")); + _hud->SetStateString("aim_text", plPilot->hud->GetStateString("aim_text", "")); + //set in UpdateHud: + /* + _hud->SetStateString("playername", plPilot->hud->GetStateString("playername", "")); + _hud->SetStateFloat("team_R", plPilot->hud->GetStateFloat("team_R", "")); + _hud->SetStateFloat("team_G", plPilot->hud->GetStateFloat("team_G", "")); + _hud->SetStateFloat("team_B", plPilot->hud->GetStateFloat("team_B", "")); + _hud->SetStateBool("ismultiplayer", plPilot->hud->GetStateBool("ismultiplayer", "")); + */ + + //set in UpdateHud: + /* + //top 4 + for (int i = 0; i < 4; i++) { + _hud->SetStateString( va( "player%i", i+1 ), plPilot->hud->GetStateString( va( "player%i", i+1 ), "")); + _hud->SetStateString( va( "player%i_portrait", i+1 ), plPilot->hud->GetStateString(va( "player%i_portrait", i+1 ), "")); + _hud->SetStateString( va( "player%i_score", i+1 ), plPilot->hud->GetStateString( va( "player%i_score", i+1 ), "")); + _hud->SetStateString( va( "rank%i", i+1 ), plPilot->hud->GetStateString( va( "rank%i", i+1 ), "")); + } + _hud->SetStateString("rank_self", plPilot->hud->GetStateString("rank_self", "")); + */ + } + } + _hud->SetStateBool( "dying", health <= 0 ); + _hud->SetStateFloat( "healthfraction", ((float)health)/(float)spawnHealth ); + _hud->SetStateFloat( "powerfraction", (currentPower)/spawnArgs.GetFloat("maxPower") ); + idAngles angles = GetAxis().ToAngles(); + _hud->SetStateFloat( "pitch", angles.pitch ); + _hud->SetStateFloat( "yaw", angles.yaw ); + _hud->SetStateFloat( "roll", angles.roll ); + _hud->Redraw( gameLocal.realClientTime ); + } +} + +void hhVehicle::PerformDeathAction(int deathAction, idActor *savedPilot, idEntity *attacker, idVec3 &dir) { + switch(deathAction) { + case 0: // Drop pilot + break; + case 1: // Kill pilot + savedPilot->Damage(this, attacker, dir, spawnArgs.GetString("def_killpilotdamage"), 1.0f, 0); + break; + } +} + +void hhVehicle::Explode( idEntity *attacker, idVec3 dir ) { + if (gameLocal.isClient) { + return; + } + if (!GetPilotInterface()) { + return; + } + + idEntityPtr savedPilot = GetPilotInterface()->GetPilot(); + + EjectPilot(); + + if (savedPilot.IsValid()) { + int deathAction = savedPilot->IsType(hhPlayer::Type) ? spawnArgs.GetInt("DeathActionPlayer") : spawnArgs.GetInt("DeathActionAI"); + PerformDeathAction(deathAction, savedPilot.GetEntity(), attacker, dir); + } + + const char *deathExplosionDef = spawnArgs.GetString("def_deathexplosion", ""); + if (deathExplosionDef && deathExplosionDef[0]) { + gameLocal.RadiusDamage(GetOrigin(), this, this, this, this, deathExplosionDef); + } + + // Explode into gibs + idVec3 vel = dir * 300; + hhUtils::SpawnDebrisMass(spawnArgs.GetString("def_debrisspawner"), + GetPhysics()->GetOrigin(), NULL, &vel, 1); + StopSound(SND_CHANNEL_DYING, true); + StartSound("snd_death", SND_CHANNEL_ANY); +} + +bool hhVehicle::HasPower( int amount ) const { + return currentPower >= amount; +} + +bool hhVehicle::ConsumePower( int amount ) { + if( InGodMode() ) { + return true; + } + + if (currentPower >= amount) { + currentPower -= amount; + return true; + } + + currentPower = 0; + return false; +} + +void hhVehicle::RemoveVehicle() { + //rww - stop sounds too + StopSound(SND_CHANNEL_THRUSTERS, true); + StopSound(SND_CHANNEL_MISC1, true); + StopSound(SND_CHANNEL_DYING, true); + + Hide(); + GetPhysics()->SetContents( 0 ); + GetPhysics()->SetClipMask( 0 ); + PostEventMS( &EV_Remove, 3000 ); // Give anything targetting it a chance to retarget +} + +void hhVehicle::WriteToSnapshot( idBitMsgDelta &msg ) const { + physicsObj.WriteToSnapshot( msg ); + assert(currentPower < (1<<20)); + msg.WriteBits(currentPower, 20); + assert(health < (1<<12)); + msg.WriteBits(health, 12); + msg.WriteBits(bHeadlightOn, 1); + + idCQuat modelQuat = modelAxis.ToCQuat(); + msg.WriteFloat(modelQuat.x); + msg.WriteFloat(modelQuat.y); + msg.WriteFloat(modelQuat.z); + + msg.WriteBits(renderEntity.suppressSurfaceInViewID, GENTITYNUM_BITS); + + /* + msg.WriteBits(currentPower, 32); + msg.WriteFloat(thrustFactor); + msg.WriteFloat(thrustMin); + msg.WriteFloat(thrustMax); + msg.WriteFloat(thrustAccel); + msg.WriteFloat(thrustScale); + msg.WriteFloat(dockBoostFactor); + */ + + msg.WriteBits(IsNoClipping() ? 0 : vehicleClipMask, 32); + msg.WriteBits(IsNoClipping() ? 0 : vehicleContents, 32); + msg.WriteBits(IsHidden(), 1); + + msg.WriteBits(dock.GetSpawnId(), 32); + msg.WriteBits(domelight.GetSpawnId(), 32); + WriteBindToSnapshot( msg ); + + if (fireController) { //fire controller can be null at this point. + msg.WriteBits(fireController->barrelOffsets.GetCurrentIndex(), 8); + } + else { + msg.WriteBits(0, 8); + } +} + +void hhVehicle::ReadFromSnapshot( const idBitMsgDelta &msg ) { + physicsObj.ReadFromSnapshot( msg ); + currentPower = msg.ReadBits(20); + health = msg.ReadBits(12); + bool headlightOn = !!msg.ReadBits(1); + if (headlightOn != bHeadlightOn) { + Headlight(headlightOn); + } + + idCQuat modelQuat; + modelQuat.x = msg.ReadFloat(); + modelQuat.y = msg.ReadFloat(); + modelQuat.z = msg.ReadFloat(); + modelAxis = modelQuat.ToMat3(); + + renderEntity.suppressSurfaceInViewID = msg.ReadBits(GENTITYNUM_BITS); + + /* + currentPower = msg.ReadBits(32); + thrustFactor = msg.ReadFloat(); + thrustMin = msg.ReadFloat(); + thrustMax = msg.ReadFloat(); + thrustAccel = msg.ReadFloat(); + thrustScale = msg.ReadFloat(); + dockBoostFactor = msg.ReadFloat(); + */ + + int newVehicleClipMask = msg.ReadBits(32); + if (newVehicleClipMask != vehicleClipMask) { + physicsObj.SetClipMask(newVehicleClipMask); + vehicleClipMask = newVehicleClipMask; + } + int newVehicleContents = msg.ReadBits(32); + if (newVehicleContents != vehicleContents) { + physicsObj.SetContents(newVehicleContents); + vehicleContents = newVehicleContents; + } + + bool hidden = !!msg.ReadBits(1); + if (hidden != IsHidden()) { + if (hidden) { + Hide(); + } else { + Show(); + } + } + + int spawnId; + + spawnId = msg.ReadBits(32); //rwwFIXME why is 0 check needed? something checking the container strangely? + if (!spawnId) { + dock = NULL; + } + else { + dock.SetSpawnId(spawnId); + } + spawnId = msg.ReadBits(32); + if (!spawnId) { + domelight = NULL; + } + else { + if (domelight.SetSpawnId(spawnId)) { + domelight->Bind( this, true ); + domelight->SetLightParm(SHADERPARM_TIMEOFFSET, -MS2SEC(gameLocal.time)); + } + } + ReadBindFromSnapshot( msg ); + + int barrelIndex = msg.ReadBits(8); + if (fireController) { + fireController->barrelOffsets.SetCurrentIndex(barrelIndex); + } +} + +void hhVehicle::ClientPredictionThink( void ) { + Think(); +} + +bool hhVehicle::ClientReceiveEvent( int event, int time, const idBitMsg &msg ) { + switch ( event ) { + case EVENT_EJECT_PILOT: + EjectPilot(); + return true; + default: + return hhRenderEntity::ClientReceiveEvent( event, time, msg ); + } +} + +void hhVehicle::Event_Explode( idEntity *attacker, float dx, float dy, float dz ) { + if (InDialogMode()) { + // Death not allowed, repost + CancelEvents(&EV_VehicleExplode); + PostEventMS(&EV_VehicleExplode, 1000, attacker, dx, dy, dz); + return; + } + + idVec3 dir(dx, dy, dz); + Explode(attacker, dir); +} + +void hhVehicle::Event_ResetGravity() { + ResetGravity(); +} + +void hhVehicle::Event_FireCannon() { + if( fireController && fireController->LaunchProjectiles(vec3_zero) ) { + StartSound( "snd_cannon", SND_CHANNEL_ANY ); + fireController->MuzzleFlash(); + if (GetPilot() && GetPilot()->IsType(hhPlayer::Type)) { + // Only players get weapon fire feedback, so monster shuttles aren't pushed back into docks + fireController->WeaponFeedback(); + } + } +} + +// Script control interfaces +void hhVehicle::Event_GetIn( idEntity *ent ) { + if (ent->IsType(idActor::Type)) { + static_cast( ent )->EnterVehicle( this ); + GetPilotInterface()->UnderScriptControl( true ); + } +} + +void hhVehicle::Event_GetOut() { + if (GetPilotInterface()) { + GetPilotInterface()->UnderScriptControl( true ); + EjectPilot(); + } +} + +void hhVehicle::Event_Fire( bool start ) { + if( GetPilotInterface() ) { + GetPilotInterface()->UnderScriptControl( true ); + GetPilotInterface()->Fire( start ); + } +} + +void hhVehicle::Event_AltFire( bool start ) { + if( GetPilotInterface() ) { + GetPilotInterface()->UnderScriptControl( true ); + GetPilotInterface()->AltFire( start ); + } +} + +void hhVehicle::Event_OrientTowards( idVec3 &point, float speed ) { + if( GetPilotInterface() ) { + GetPilotInterface()->UnderScriptControl( true ); + GetPilotInterface()->OrientTowards( point, speed ); // passed as percentage of max + } +} + +void hhVehicle::Event_StopOrientingTowards() { + if( GetPilotInterface() ) { + GetPilotInterface()->UnderScriptControl( true ); + GetPilotInterface()->OrientTowards( vec3_origin, 0.0f ); + } +} + +void hhVehicle::Event_ThrustTowards( idVec3 &point, float speed ) { + idVec3 pt = point; + if( GetPilotInterface() ) { + GetPilotInterface()->UnderScriptControl( true ); + GetPilotInterface()->ThrustTowards( pt, speed ); // passed as percentage of max + } +} + +void hhVehicle::Event_StopThrustingTowards() { + if( GetPilotInterface() ) { + GetPilotInterface()->UnderScriptControl( true ); + GetPilotInterface()->ThrustTowards( vec3_origin, 0.0f ); + } +} + +void hhVehicle::Event_ReleaseScriptControl() { + if( GetPilotInterface() ) { + GetPilotInterface()->UnderScriptControl( false ); + GetPilotInterface()->ClearBufferedCmds(); + } +} + +void hhVehicle::Event_EjectPilot() { + EjectPilot(); +} + diff --git a/src/Prey/game_vehicle.h b/src/Prey/game_vehicle.h new file mode 100644 index 0000000..8e32330 --- /dev/null +++ b/src/Prey/game_vehicle.h @@ -0,0 +1,375 @@ +#ifndef __GAME_VEHICLE_H__ +#define __GAME_VEHICLE_H__ + +#define CONTENTS_VEHICLE (CONTENTS_BODY|CONTENTS_MONSTERCLIP|CONTENTS_MOVEABLECLIP) +#define CLIPMASK_VEHICLE (CONTENTS_BODY|CONTENTS_VEHICLECLIP|CONTENTS_SOLID|CONTENTS_FORCEFIELD) + +extern const idEventDef EV_Vehicle_FireCannon; +extern const idEventDef EV_VehicleExplode; + +class idLight; +class hhVehicle; +class hhPlayer; +class hhDock; +class hhShuttleDock; + +enum EFireMode{ + FIREMODE_NOTHING=0, + FIREMODE_CANNON, + FIREMODE_TRACTOR, + FIREMODE_EXIT, + NUM_FIREMODES +}; + +// Support for direction masks, as used for thrusters +enum { + // The order of these is important and is laid out as: + THRUSTER_FRONT=0, // x < 0 + THRUSTER_BACK, // x > 0 + THRUSTER_LEFT, // y < 0 + THRUSTER_RIGHT, // y > 0 + THRUSTER_TOP, // z < 0 + THRUSTER_BOTTOM, // z > 0 + THRUSTER_DIRECTIONS +}; + + + +//========================================================================== +// +// hhVehicleThruster +// +//========================================================================== + +class hhVehicleThruster : public idEntity { + CLASS_PROTOTYPE( hhVehicleThruster ); +public: + hhVehicleThruster(); + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + void SetOwner(hhVehicle *v) { owner = v; } + void SetSmoker(bool s, idVec3 &offset, idVec3 &dir); + void SetThruster(bool on); + void Update( const idVec3 &vel ); + void SetDying(bool dying); + virtual bool GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis ); + +protected: + void Event_AssignFxSmoke( hhEntityFx* fx ); + +protected: + //rww - changed to idEntityPtr's + idEntityPtr owner; + idEntityPtr fxSmoke; + idVec3 localOffset; + idVec3 localDirection; + float soundDistance; + bool bSomeThrusterActive; + bool bSoundMaster; + idVec3 localVelocity; +}; + + +//========================================================================== +// +// hhPilotVehicleInterface +// +// +//========================================================================== +class hhPilotVehicleInterface : public idClass { + CLASS_PROTOTYPE( hhPilotVehicleInterface ); + + public: + hhPilotVehicleInterface(); + ~hhPilotVehicleInterface(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //Called from vehicle + virtual void RetrievePilotInput( usercmd_t& cmds, idAngles& viewAngles ); + virtual idActor* GetPilot() const; + virtual void UnderScriptControl( bool yes ) { underScriptControl = yes; } + virtual bool UnderScriptControl() const { return underScriptControl; } + virtual void OrientTowards( const idVec3 &loc, float speed ) {} + virtual void ThrustTowards( const idVec3 &loc, float speed ) {} + virtual void Fire(bool on) {} + virtual void AltFire(bool on) {} + virtual void ClearBufferedCmds() {} + + //Called from pilot + virtual bool ControllingVehicle() const; + virtual void ReleaseControl(); + virtual hhVehicle* GetVehicle() const; + virtual void TakeControl( hhVehicle* vehicle, idActor* pilot ); + virtual idVec3 DeterminePilotOrigin() const; + virtual idMat3 DeterminePilotAxis() const; + + virtual bool InvalidVehicleImpulse( int impulse ); + int GetVehicleSpawnId() const { return vehicle.GetSpawnId(); } + bool SetVehicleSpawnId( int id ) { return vehicle.SetSpawnId( id ); } + protected: + idEntityPtr pilot; + idEntityPtr vehicle; + bool underScriptControl; +}; + +class hhAIVehicleInterface : public hhPilotVehicleInterface { + CLASS_PROTOTYPE( hhAIVehicleInterface ); + + public: + hhAIVehicleInterface(void); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //Called from pilot + void BufferPilotCmds( const usercmd_t* cmds, const idAngles* viewAngles ); + virtual void OrientTowards( const idVec3 &loc, float speed ); + virtual void ThrustTowards( const idVec3 &loc, float speed ); + virtual bool IsVehicleDocked() const; + virtual void TakeControl( hhVehicle* vehicle, idActor* pilot ); + virtual void Fire(bool on); + virtual void AltFire(bool on); + + //Called from vehicle + virtual void RetrievePilotInput( usercmd_t& cmds, idAngles& viewAngles ); + + protected: + void ClearBufferedCmds(); + + protected: + usercmd_t bufferedCmds; + idAngles bufferedViewAngles; + bool stateFiring; + bool stateAltFiring; + idVec3 stateOrientDestination; + float stateOrientSpeed; + idVec3 stateThrustDestination; + float stateThrustSpeed; +}; + +class hhPlayerVehicleInterface : public hhPilotVehicleInterface { + CLASS_PROTOTYPE( hhPlayerVehicleInterface ); + + public: + hhPlayerVehicleInterface(); + virtual ~hhPlayerVehicleInterface(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //Called from vehicle + virtual void RetrievePilotInput( usercmd_t& cmds, idAngles& viewAngles ); + + //Called from pilot + virtual void ReleaseControl(); + virtual void TakeControl( hhVehicle* vehicle, idActor* pilot ); + void StartHUDTranslation(); // HUMANHEAD mdl: Moved these into hhPlayer events to prevent loadgame crash + // (events must be on entities, not just idClasses, in order to save properly) + int GetHandSpawnId() const { return controlHand.GetSpawnId(); } + bool SetHandSpawnId( int id ) { return controlHand.SetSpawnId( id ); } + hhControlHand *GetHandEntity() const { return controlHand.GetEntity(); } + const hhWeaponHandState *GetWeaponHandState() const { return &weaponHandState; } + hhWeaponHandState *GetWeaponHandState() { return &weaponHandState; } + + //Called from playerView + virtual idUserInterface* GetHUD() const { return hud; } + virtual void DrawHUD( idUserInterface* _hud ); + + protected: + void UpdateControlHand( const usercmd_t& cmds ); + void CreateControlHand( hhPlayer* pilot, const char* handName ); + void RemoveHand(); + + protected: + hhWeaponHandState weaponHandState; + idEntityPtr controlHand; + + idUserInterface* hud; + bool uniqueHud; //rww + idInterpolate translationAlpha; // interpolator for hud translation +}; + +//========================================================================== +// +// hhVehicle +// +//========================================================================== + +class hhVehicle : public hhRenderEntity { + ABSTRACT_PROTOTYPE( hhVehicle ); + +public: + hhVehicle() : fireController(NULL), pilotInterface(NULL) {} + ~hhVehicle(); + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think(); + virtual void ClientPredictionThink( void ); + virtual void Present(); + enum { + EVENT_EJECT_PILOT = idEntity::EVENT_MAXEVENTS, + EVENT_MAXEVENTS + }; + virtual bool ClientReceiveEvent( int event, int time, const idBitMsg &msg ); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual bool Collide( const trace_t &collision, const idVec3 &velocity ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual bool HandleSingleGuiCommand( idEntity *entityGui, idLexer *src ); + virtual const idMat3& GetAxis( int id = 0 ) const { return modelAxis; } + virtual void SetAxis( const idMat3& axis ); + virtual void Portalled(idEntity *portal); + virtual idVec3 GetPortalPoint( void ); + virtual idVec3 DeterminePilotOrigin() const; + virtual idMat3 DeterminePilotAxis() const; + + virtual void FireThrusters( const idVec3& impulse ) {} + virtual void ApplyImpulse( idEntity* ent, int id, const idVec3& point, const idVec3& impulse ); + virtual void ApplyImpulse( const idVec3& impulse ); + + virtual void SetDock( const hhDock* dock ) { this->dock = dock; } + virtual void Undock() { dock.Clear(); } + virtual bool IsDocked() const { return dock.IsValid(); } + virtual idActor* GetPilot() const { return (GetPilotInterface()) ? GetPilotInterface()->GetPilot() : NULL; } + virtual hhPilotVehicleInterface* GetPilotInterface() const { return pilotInterface; } + virtual void SetPilotInterface(hhPilotVehicleInterface *pInterface) { pilotInterface = pInterface; } + + bool IsNoClipping() const; + bool InGodMode() const; + bool InDialogMode() const; + + virtual float GetThrustFactor() const { return thrustFactor; } + + void GiveHealth( int amount ); + void GivePower( int amount ); + bool ConsumePower( int amount ); + bool HasPower( int amount ) const; + bool NeedsPower() const { return currentPower < spawnArgs.GetFloat("maxPower"); } + + virtual bool WillAcceptPilot( idActor *act ) = 0; + virtual void AcceptPilot( hhPilotVehicleInterface* pilotInterface ); + void RestorePilot( hhPilotVehicleInterface* pilotInterface); + virtual void ReleaseControl() {} + virtual void EjectPilot(); + virtual bool CanBecomeVehicle(idActor *pilot); + virtual void BecomeVehicle(); + virtual void BecomeConsole(); + virtual bool IsVehicle() const; + virtual bool IsConsole() const; + + virtual idVec3 GetFireOrigin() { return GetOrigin(); } // Called by firecontroller + virtual idMat3 GetFireAxis() { return GetAxis(); } + + virtual void ResetGravity(); + + virtual void ProcessPilotInput( const usercmd_t* cmds, const idAngles* viewAngles ); + + virtual float GetWeaponRecoil() const { return (fireController) ? fireController->GetRecoil() : 0.0f; } + virtual float GetWeaponFireDelay() const { return (fireController) ? fireController->GetFireDelay() : 0.0f; } + + virtual void DrawHUD( idUserInterface* _hud ); + + virtual void DoPlayerImpulse(int impulse); //rww - seperated into its own function because of networked impulse events + +protected: + virtual void UpdateModel(); + virtual const idEventDef* GetAttackFunc( const char* funcName ); + virtual void InitializeAttackFuncs(); + + virtual void InitPhysics(); + virtual void SetConsolePhysics(); + virtual void SetVehiclePhysics(); + + virtual void SetConsoleModel(); + virtual void SetVehicleModel(); + + void ProcessButtons( const usercmd_t& cmds ); + void ProcessImpulses( const usercmd_t& cmds ); + float CmdScale( const usercmd_t* cmd ) const; + void SetThrustBooster( float minBooster, float maxBooster, float accelBooster ); + + hhVehicleFireController * CreateFireController() { return new hhVehicleFireController; } + + virtual void CreateHeadLight(); + virtual void FreeHeadLight(); + virtual void Headlight( bool on ); + virtual void CreateDomeLight(); + virtual void FreeDomeLight(); + + virtual void RemoveVehicle(); + virtual void PerformDeathAction( int deathAction, idActor *savedPilot, idEntity *attacker, idVec3 &dir ); + virtual void Explode( idEntity *attacker, idVec3 dir ); + virtual void Event_FireCannon(); + void Event_Explode( idEntity *attacker, float dx, float dy, float dz ); + void Event_GetIn( idEntity *ent ); + void Event_GetOut(); + void Event_ResetGravity(); + void Event_Fire( bool start ); + void Event_AltFire( bool start ); + void Event_OrientTowards( idVec3 &point, float speed ); + void Event_StopOrientingTowards(); + void Event_ThrustTowards( idVec3 &point, float speed ); + void Event_StopThrustingTowards(); + void Event_ReleaseScriptControl(); + void Event_EjectPilot(); + + +protected: + hhPhysics_Vehicle physicsObj; // physics object for vehicle + idMat3 modelAxis; + + hhPilotVehicleInterface* pilotInterface; + + hhVehicleFireController* fireController; + usercmd_t oldCmds; + + //FIXME: make these renderLight structs + idEntityPtr headlight; // Head light + idEntityPtr domelight; // Dome light + bool bHeadlightOn; + + int currentPower; + float thrustFactor; + float thrustMin; + float thrustMax; + float thrustAccel; + float thrustScale; + float dockBoostFactor; + + idEntityPtr dock; + + idEntityPtr lastAttacker; + int lastAttackTime; + + int vehicleClipMask; // clipmask of vehicle when not noclipping + int vehicleContents; // contents of vehicle when not noclipping + + bool bDamageSelfOnCollision; // Vehicle takes damage when colliding + bool bDamageOtherOnCollision;// Vehicle takes damage when colliding + + bool bDisallowAttackUntilRelease; + bool bDisallowAltAttackUntilRelease; + + int thrusterCost; + int validThrustTime; // Delay thrust capability a couple seconds after entering vehicle + + const idEventDef* attackFunc; + const idEventDef* finishedAttackingFunc; + const idEventDef* altAttackFunc; + const idEventDef* finishedAltAttackingFunc; + + bool noDamage; +}; + +#endif + diff --git a/src/Prey/game_vomiter.cpp b/src/Prey/game_vomiter.cpp new file mode 100644 index 0000000..bfc3d67 --- /dev/null +++ b/src/Prey/game_vomiter.cpp @@ -0,0 +1,333 @@ +// Game_Vomiter.cpp +// +// spews vomit chunks periodically +// triggering will cause an eruption + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_Erupt("erupt", "d"); +const idEventDef EV_UnErupt("unerupt", NULL); +const idEventDef EV_DamagePulse("damagepulse", NULL); +const idEventDef EV_MovablePulse(""); + +const idEventDef EV_Broadcast_AssignFxErupt( "", "e" ); + +CLASS_DECLARATION(hhAnimatedEntity, hhVomiter) + EVENT( EV_Activate, hhVomiter::Event_Trigger ) + EVENT( EV_Erupt, hhVomiter::Event_Erupt ) + EVENT( EV_UnErupt, hhVomiter::Event_UnErupt ) + EVENT( EV_PlayIdle, hhVomiter::Event_PlayIdle ) + EVENT( EV_DamagePulse, hhVomiter::Event_DamagePulse ) + EVENT( EV_MovablePulse, hhVomiter::Event_MovablePulse ) + EVENT( EV_Broadcast_AssignFxErupt, hhVomiter::Event_AssignFxErupt ) +END_CLASS + +hhVomiter::hhVomiter(void) { + // mdl: Moved this here to prevent uninitialized fxErupt from crashing the destructor + fxErupt = NULL; +} + +void hhVomiter::Spawn(void) { + + detectionTrigger = NULL; + + spawnArgs.GetFloat("minDelay", "3", mindelay); + spawnArgs.GetFloat("maxDelay", "7", maxdelay); + secBetweenDamage = spawnArgs.GetFloat("secBetweenDamage"); + secBetweenMovables = spawnArgs.GetFloat("secBetweenMovables"); + minMovableVel = spawnArgs.GetFloat("minMovableVel"); + maxMovableVel = spawnArgs.GetFloat("maxMovableVel"); + removeTime = spawnArgs.GetFloat("movableRemoveTime"); + numMovables = spawnArgs.GetInt("numMovables"); + goAwayChance = spawnArgs.GetFloat("GoAwayChance"); + + idleAnim = GetAnimator()->GetAnim("idle"); + painAnim = GetAnimator()->GetAnim("pain"); + spewAnim = GetAnimator()->GetAnim("spew"); + deathAnim = GetAnimator()->GetAnim("death"); + + bErupting = false; + bIdling = false; + + // setup the clipModel + GetPhysics()->SetContents( CONTENTS_SOLID ); + + // Spawn trigger used for waking it up + SpawnTrigger(); + + //TODO: Spawn this the first time it erupts instead ? + // Spawn FX system + hhFxInfo fxInfo; + fxInfo.SetStart( false ); + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone( false ); + fxInfo.Triggered( true ); + BroadcastFxInfo(spawnArgs.GetString("fx_vomit"), GetOrigin(), GetAxis(), &fxInfo, &EV_Broadcast_AssignFxErupt ); + + StartIdle(); + GoToSleep(); + fl.takedamage = true; +} + +hhVomiter::~hhVomiter() { + SAFE_REMOVE(fxErupt); +} + +void hhVomiter::Save(idSaveGame *savefile) const { + savefile->WriteBool(bErupting); + savefile->WriteBool(bIdling); + savefile->WriteBool(bAwake); + savefile->WriteFloat(mindelay); + savefile->WriteFloat(maxdelay); + + savefile->WriteFloat(secBetweenDamage); + savefile->WriteFloat(secBetweenMovables); + + savefile->WriteInt(idleAnim); + savefile->WriteInt(painAnim); + savefile->WriteInt(spewAnim); + savefile->WriteInt(deathAnim); + savefile->WriteFloat(minMovableVel); + savefile->WriteFloat(maxMovableVel); + savefile->WriteFloat(removeTime); + + savefile->WriteInt(numMovables); + savefile->WriteFloat(goAwayChance); + + savefile->WriteObject(fxErupt); + + detectionTrigger.Save(savefile); +} + +void hhVomiter::Restore( idRestoreGame *savefile ) { + savefile->ReadBool(bErupting); + savefile->ReadBool(bIdling); + savefile->ReadBool(bAwake); + savefile->ReadFloat(mindelay); + savefile->ReadFloat(maxdelay); + + savefile->ReadFloat(secBetweenDamage); + savefile->ReadFloat(secBetweenMovables); + + savefile->ReadInt(idleAnim); + savefile->ReadInt(painAnim); + savefile->ReadInt(spewAnim); + savefile->ReadInt(deathAnim); + savefile->ReadFloat(minMovableVel); + savefile->ReadFloat(maxMovableVel); + savefile->ReadFloat(removeTime); + + savefile->ReadInt(numMovables); + savefile->ReadFloat(goAwayChance); + + savefile->ReadObject( reinterpret_cast(fxErupt) ); + + detectionTrigger.Restore(savefile); +} + +void hhVomiter::SpawnTrigger() { + idDict Args; + + hhUtils::PassArgs( spawnArgs, Args, "triggerpass_" ); + + Args.Set( "target", name.c_str() ); + Args.Set( "bind", name.c_str() ); + + Args.SetVector( "origin", GetOrigin() + spawnArgs.GetVector("offset_trigger") ); + Args.SetMatrix( "rotation", GetAxis() ); + detectionTrigger = gameLocal.SpawnObject( spawnArgs.GetString("def_trigger"), &Args ); +} + +void hhVomiter::DormantBegin( void ) { +} + +void hhVomiter::DormantEnd( void ) { +} + +void hhVomiter::Erupt(int repeat) { + if (bErupting || health <= 0 || fl.isDormant) { + if (fl.isDormant) { + UnErupt(); // go to sleep, etc., so we can wake up again later + } + return; + } + + bErupting = true; + StartSound( "snd_erupt", SND_CHANNEL_ANY ); + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, spewAnim, gameLocal.time, 0); + + int startTime = GetAnimator()->CurrentAnim( ANIMCHANNEL_ALL )->Length(); + PostEventMS( &EV_PlayIdle, startTime ); + + PostEventMS( &EV_MovablePulse, 0 ); // Self repeating moveable spawn + PostEventMS( &EV_DamagePulse, 0 ); // Self repeating damage pulse + + // Activate FX + if (fxErupt) { + fxErupt->PostEventMS( &EV_Activate, 0, this ); + } + + // Stop erupting in a while + PostEventSec( &EV_UnErupt, spawnArgs.GetFloat("vomitTime") ); +} + +void hhVomiter::UnErupt() { + CancelEvents(&EV_Erupt); // Stop any pending events + CancelEvents(&EV_DamagePulse); + CancelEvents(&EV_MovablePulse); + + if (bErupting) { + bErupting = false; + // Deactivate FX + if (fxErupt) { + fxErupt->PostEventMS( &EV_Fx_KillFx, 0 ); + } + } + + GoToSleep(); +} + +void hhVomiter::GoToSleep() { + bAwake = false; +} + +void hhVomiter::WakeUp() { + if (!bAwake && !bErupting && health > 0) { + bAwake = true; + StartSound( "snd_wakeup", SND_CHANNEL_ANY ); + PostEventSec( &EV_Erupt, 0.1+mindelay + gameLocal.random.RandomFloat()*(maxdelay-mindelay), true); + } +} + +bool hhVomiter::Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + if (painAnim) { + StartSound( "snd_pain", SND_CHANNEL_ANY ); + GetAnimator()->ClearAllAnims( gameLocal.time, 200 ); + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, painAnim, gameLocal.time, 200); + + int startTime = GetAnimator()->CurrentAnim( ANIMCHANNEL_ALL )->Length(); + PostEventMS( &EV_PlayIdle, startTime ); + } + return( idEntity::Pain(inflictor, attacker, damage, dir, location) ); +} + +void hhVomiter::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + + if (health + damage > 0) { // Only do this the first death + // Clear Event Queue of all eruptions + CancelEvents(&EV_Erupt); + CancelEvents(&EV_DamagePulse); + if (bErupting) { + UnErupt(); + } + + StartSound( "snd_die", SND_CHANNEL_ANY ); + + // Spawn dead effect -- currently an explosion + hhFxInfo fxInfo; + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_death", GetOrigin(), -GetAxis(), &fxInfo ); + + ActivateTargets( attacker ); + + StopIdle(); + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, deathAnim, gameLocal.time, 0); + } + else { // Post-death pain +// GetAnimator()->ClearAllAnims( gameLocal.time, 200 ); +// GetAnimator()->PlayAnim(ANIMCHANNEL_ALL, painAnim, gameLocal.time, 200); + } +} + +void hhVomiter::StartIdle() { + hhFxInfo fxInfo; + + if (idleAnim && !bIdling && health > 0) { + PostEventMS(&EV_PlayIdle, 0); + StartSound( "snd_idle", SND_CHANNEL_IDLE ); + bIdling = true; + } +} + +void hhVomiter::StopIdle() { + CancelEvents(&EV_PlayIdle); + StopSound( SND_CHANNEL_IDLE ); + bIdling = false; +} + +// +// Events +// + +void hhVomiter::Event_AssignFxErupt( hhEntityFx* fx ) { + fxErupt = fx; +} + +void hhVomiter::Event_PlayIdle() { + GetAnimator()->CycleAnim(ANIMCHANNEL_ALL, idleAnim, gameLocal.time, 100); +} + +void hhVomiter::Event_Trigger( idEntity *activator ) { + WakeUp(); +} + +void hhVomiter::Event_Erupt(int repeat) { + Erupt(repeat); +} + +void hhVomiter::Event_UnErupt() { + UnErupt(); +} + +void hhVomiter::Event_DamagePulse() { + idEntity *touch[ MAX_GENTITIES ]; + int num; + // Damage all entities touching trigger's clipmodel + num = hhUtils::EntitiesTouchingClipmodel( detectionTrigger->GetPhysics()->GetClipModel(), touch, MAX_GENTITIES, MASK_SHOT_BOUNDINGBOX ); + for (int i = 0; i < num; i++ ) { + if( !touch[i] || touch[i] == this ) { + continue; + } + touch[i]->Damage(this, this, GetAxis()[2], spawnArgs.GetString("def_damage"), 1.0f, 0); + } + PostEventSec( &EV_DamagePulse, secBetweenDamage ); +} + +void hhVomiter::Event_MovablePulse() { + // Spawn movables + idDict args; + idEntity *ent; + idVec3 vel, dir; + + const char *movableName = spawnArgs.RandomPrefix("def_movable", gameLocal.random); + if (movableName && *movableName) { + args.Clear(); + args.SetVector("origin", GetOrigin()); + + if (gameLocal.random.RandomFloat() < goAwayChance) { + args.SetBool("removeOnCollision", true); + } + else { + args.SetFloat("removeTime", removeTime); + } + + for (int ix=0; ixGetPhysics()->SetLinearVelocity( vel ); + ent->fl.takedamage = false; // don't let the radius damage affect them + } + } + PostEventSec( &EV_MovablePulse, secBetweenMovables ); // Self repeating event +} diff --git a/src/Prey/game_vomiter.h b/src/Prey/game_vomiter.h new file mode 100644 index 0000000..64f91a8 --- /dev/null +++ b/src/Prey/game_vomiter.h @@ -0,0 +1,61 @@ + +#ifndef __GAME_VOMITER_H__ +#define __GAME_VOMITER_H__ + +class hhVomiter : public hhAnimatedEntity { +public: + CLASS_PROTOTYPE( hhVomiter ); + + hhVomiter(); + virtual ~hhVomiter(); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void Erupt(int repeat); + void UnErupt(void); + virtual bool Pain( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void DormantBegin( void ); + virtual void DormantEnd( void ); + +protected: + void StartIdle(); + void StopIdle(); + void SpawnTrigger(); + void WakeUp(); + void GoToSleep(); + +protected: + void Event_DamagePulse(void); + void Event_Trigger( idEntity *activator ); + void Event_Erupt(int repeat); + void Event_UnErupt(void); + void Event_PlayIdle( void ); + void Event_MovablePulse(); + void Event_AssignFxErupt( hhEntityFx* fx ); + +private: + bool bErupting; + bool bIdling; + bool bAwake; + float mindelay; // Delays until next vomit + float maxdelay; + float secBetweenDamage; + float secBetweenMovables; + int idleAnim; + int painAnim; + int spewAnim; + int deathAnim; + float minMovableVel; + float maxMovableVel; + float removeTime; + int numMovables; + float goAwayChance; + idEntityFx *fxErupt; + + idEntityPtr detectionTrigger; +}; + +#endif /* __GAME_VOMITER_H__ */ diff --git a/src/Prey/game_weaponHandState.cpp b/src/Prey/game_weaponHandState.cpp new file mode 100644 index 0000000..3c8750d --- /dev/null +++ b/src/Prey/game_weaponHandState.cpp @@ -0,0 +1,282 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/* +TERMS: + Transition: How to transition up and down + 0 - Abruptly. No up/down anims. Just disappears + 1 - With Anims. Play the up/down anims. +*/ + +CLASS_DECLARATION( idClass, hhWeaponHandState ) +END_CLASS + +/* +============ +hhWeaponHandState::hhWeaponHandState +============ +*/ +hhWeaponHandState::hhWeaponHandState( hhPlayer *aPlayer, + bool aTrackWeapon, int weaponTransition, + bool aTrackHand, int handTransition ){ + + player = aPlayer; + + oldHand = NULL; + oldWeaponNum = 0; + + oldHandUp = handTransition; + oldWeaponUp = weaponTransition; + + trackHand = aTrackHand; + trackWeapon = aTrackWeapon; +} + +void hhWeaponHandState::Save(idSaveGame *savefile) const { + player.Save(savefile); + oldHand.Save(savefile); + savefile->WriteInt(oldWeaponNum); + savefile->WriteInt(oldHandUp); + savefile->WriteInt(oldWeaponUp); + savefile->WriteBool(trackHand); + savefile->WriteBool(trackWeapon); +} + +void hhWeaponHandState::Restore( idRestoreGame *savefile ) { + player.Restore(savefile); + oldHand.Restore(savefile); + savefile->ReadInt(oldWeaponNum); + savefile->ReadInt(oldHandUp); + savefile->ReadInt(oldWeaponUp); + savefile->ReadBool(trackHand); + savefile->ReadBool(trackWeapon); +} + +void hhWeaponHandState::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(player.GetSpawnId(), 32); + msg.WriteBits(oldHand.GetSpawnId(), 32); + + msg.WriteBits(oldWeaponNum, 32); + msg.WriteBits(oldHandUp, 32); + msg.WriteBits(oldWeaponUp, 32); + msg.WriteBits(trackHand, 1); + msg.WriteBits(trackWeapon, 1); +} + +void hhWeaponHandState::ReadFromSnapshot( const idBitMsgDelta &msg ) { + player.SetSpawnId(msg.ReadBits(32)); + oldHand.SetSpawnId(msg.ReadBits(32)); + + oldWeaponNum = msg.ReadBits(32); + oldHandUp = msg.ReadBits(32); + oldWeaponUp = msg.ReadBits(32); + trackHand = !!msg.ReadBits(1); + trackWeapon = !!msg.ReadBits(1); +} + + +/* +============ +hhWeaponHandState::SetPlayer +============ +*/ +void hhWeaponHandState::SetPlayer( hhPlayer *aPlayer ) { + player = aPlayer; +} + +/* +============ +hhWeaponHandState::Save +============ +*/ +int hhWeaponHandState::Archive( const char *newWeaponDef, int weaponTransition, + const char *newHandDef, int handTransition ) { + + if ( !player.IsValid() ) { + gameLocal.Warning( "Error in hhWeaponHandState::Archive, no player set" ); + } + + // Do we care about the old hand? + if ( trackHand ) { + // Save the hand, break if no hand. Must save for the restore + oldHand = player->hand; + + if ( oldHand.IsValid() ) { + // If we are going to raise it later put down + if ( oldHandUp ) { + oldHand->PutAway(); + } + + // Make it disapear + oldHand->Hide(); + + // Make sure it doesn't come back (Ready makes it visible again) + oldHand->CancelEvents(&EV_Hand_Ready); + } + + // Clear the hand out + player->hand = NULL; + } + + // Do we care about the old weapon? + if ( trackWeapon ) { + // Save the weapon, break if none. Must save for the restore + oldWeaponNum = player->GetCurrentWeapon(); + if (oldWeaponNum == -1) + { + oldWeaponNum = player->GetIdealWeapon(); + assert(oldWeaponNum != -1); + } + //Calling destructor directly otherwise it doesn't get called until next frame + if( player->weapon.IsValid() ) { + player->weapon->DeconstructScriptObject(); + player->weapon->Hide(); + } + SAFE_REMOVE( player->weapon ); + } + + // See if we have a new weapon to put up + if ( trackWeapon && newWeaponDef && newWeaponDef[0] ) { + player->ForceWeapon( player->GetWeaponNum(newWeaponDef) ); + + // Weapon_Combat() normally takes care of the lowering/raising logic, so force the raise here + player->weapon->Raise(); + player->SetState( "RaiseWeapon" ); + + // Now that we have the weapon, how do we get it there? + if ( weaponTransition ) { + player->weapon->Raise(); + } + else { + player->weapon->ProcessEvent( &EV_Weapon_State, "Idle", 0 ); + } + } + + // Should we put up a new hand? + if ( trackHand && newHandDef && !player->IsDeathWalking() ) { + hhHand *newHand; //! Debug,remove later + + // Create the new hand + newHand = hhHand::AddHand( player.GetEntity(), newHandDef, true ); + + if ( !newHand ) { + gameLocal.Warning( "Error creating new hand for state change" ); + return( 0 ); + } + + // Make sure the weapon is pop'd into place + newHand->HandleWeapon( player.GetEntity(), newHand, 0, true ); + // Now that the hand is up, how do we get it there? + if ( handTransition ) { + player->hand->Raise(); + } + else { + player->hand->Ready(); + } + } + + return( 0 ); +} + + +/* +============ +hhWeaponHandState::RestoreFromArchive +============ +*/ +int hhWeaponHandState::RestoreFromArchive() { + bool setWeapon = false; + + if ( !player.IsValid() ) { + gameLocal.Warning( "Error in hhWeaponHandState::RestoreFromArchive, no player set" ); + return 0; + } + + if ( trackWeapon ) { + //Calling destructor directly otherwise it doesn't get called until next frame + if( player->weapon.IsValid() ) { + player->weapon->DeconstructScriptObject(); + player->weapon->Hide(); + } + SAFE_REMOVE( player->weapon ); + + // Put up the old weapon if there + if ( oldWeaponNum ) { + player->ForceWeapon( oldWeaponNum ); + + // Weapon_Combat() normally takes care of the lowering/raising logic, so force the raise here + player->SetState( "RaiseWeapon" ); + + if ( oldWeaponUp && player->health <= 0 ) { //HUMANHEAD bjk PCF (4-29-06) - fix weapon after shuttle death + // Make sure it is lowered + player->weapon->ProcessEvent( &EV_Weapon_State, "Raise", 0 ); + player->weapon->UpdateScript(); + + player->weapon->Raise(); + } + else { + player->weapon->ProcessEvent( &EV_Weapon_State, "Idle", 0 ); + } + } else { + player->ForceWeapon( -1 ); + } + setWeapon = true; + } + + + if ( trackHand ) { + // Remove the old hand - Need to set owner to NULL so it won't complain + if ( player->hand.IsValid() ) { + player->hand->ForceRemove(); + } + SAFE_REMOVE( player->hand ); + + hhHand *debugHand = oldHand.GetEntity(); + + // Find the first hand that should be there. Note: The 'if' could be made into a 'while' to do multiple levels + if ( oldHand.IsValid() && !oldHand->IsValidFor( player.GetEntity() ) ) { + idEntityPtr delHand; + + //gameLocal.Printf( "%d Removing weapon state hand\n", gameLocal.time ); + + delHand = oldHand; // Save the old hand for deletion + + // Move to the next hand + oldHand = oldHand->GetPreviousHand(); + debugHand = oldHand.GetEntity(); + + // delete the old old hand + delHand->ForceRemove(); + SAFE_REMOVE( delHand ); + } + + player->hand = oldHand; + + // If valid, put it up/have it handle the weapon + if ( oldHand.IsValid() ) { + player->hand->Show(); + + // Make sure the weapon is in the proper state + player->hand->HandleWeapon( player.GetEntity(), + player->hand.GetEntity(), 0, true ); + + // If they want it raised, raise it + if ( oldHandUp ) { + player->hand->Raise(); + } + // Else just pop it in there! + else { + player->hand->Ready(); + } + } + // If no hands in line, then set the weapon straight if we haven't set it already + else if ( !setWeapon ) { + player->weapon->ProcessEvent( &EV_Weapon_State, "Idle", 0 ); + } //. Got a real hand? + } //. We care about the hand + + return( 0 ); +} diff --git a/src/Prey/game_weaponHandState.h b/src/Prey/game_weaponHandState.h new file mode 100644 index 0000000..9d309ea --- /dev/null +++ b/src/Prey/game_weaponHandState.h @@ -0,0 +1,41 @@ + +#ifndef __PREY_GAME_WEAPONHANDSTATE_H__ +#define __PREY_GAME_WEAPONHANDSTATE_H__ + +class hhWeaponHandState : public idClass { + CLASS_PROTOTYPE( hhWeaponHandState ); + + public: + hhWeaponHandState( hhPlayer *player = NULL, + bool trackWeapon = true, int weaponTransition = 0, + bool trackHand = true, int handTransistion = 0 ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + int Archive( const char *newWeaponDef = NULL, int newWeaponTransition = 0, + const char *newHandDef = NULL, int newHandTransition = 0 ); + + int RestoreFromArchive(); + + void SetPlayer( hhPlayer *player ); + + void SetWeaponTransition( int trans ) { oldWeaponUp = trans; } + + protected: + + idEntityPtr player; + idEntityPtr oldHand; // Previous Hand. NULL if don't care + int oldWeaponNum; + int oldHandUp; + int oldWeaponUp; + + bool trackHand; + bool trackWeapon; +}; + +#endif /* __PREY_GAME_WEAPONHANDSTATE_H__ */ diff --git a/src/Prey/game_woundmanager.cpp b/src/Prey/game_woundmanager.cpp new file mode 100644 index 0000000..9541930 --- /dev/null +++ b/src/Prey/game_woundmanager.cpp @@ -0,0 +1,314 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// mdl: Only steam this often to keep performance hits when hitting several pipes +#define STEAM_DELAY 1000.0f + + +hhWoundManagerRenderEntity::hhWoundManagerRenderEntity( const idEntity* self ) { + this->self = self; +} + +hhWoundManagerRenderEntity::~hhWoundManagerRenderEntity() { +} + +void hhWoundManagerRenderEntity::DetermineWoundInfo( const trace_t& trace, const idVec3 velocity, jointHandle_t& jointNum, idVec3& origin, idVec3& normal, idVec3& dir ) { + jointNum = CLIPMODEL_ID_TO_JOINT_HANDLE( trace.c.id ); + + origin = trace.c.point; + normal = trace.c.normal; + dir = velocity; + dir.Normalize(); +} + +void hhWoundManagerRenderEntity::AddWounds( const idDeclEntityDef *def, surfTypes_t matterType, jointHandle_t jointNum, const idVec3& origin, const idVec3& normal, const idVec3& dir ) { + PROFILE_SCOPE("AddWounds", PROFMASK_COMBAT); + if( !def ) { + return; + } + + // Do a check so we don't spawn too many steam effects at once + if( matterType == SURFTYPE_PIPE ) { + if ( gameLocal.time > gameLocal.GetSteamTime() ) { + // Mark the current steam time + gameLocal.SetSteamTime( gameLocal.time + STEAM_DELAY ); + } else { + // Don't steam on this hit + matterType = SURFTYPE_METAL; + } + } + + ApplyWound( def->dict, gameLocal.MatterTypeToMatterKey("fx_wound", matterType), jointNum, origin, normal, dir ); //smoke_wound conflicted with smoke + + const char* impactMarkKey = gameLocal.MatterTypeToMatterKey("mtr", matterType); + ApplyMark( def->dict, impactMarkKey, origin, normal, dir ); + ApplySplatExit( def->dict, impactMarkKey, origin, normal, dir ); +} + +void hhWoundManagerRenderEntity::ApplyMark( const idDict& damageDict, const char* impactMarkKey, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ) { + PROFILE_SCOPE("ApplyMark", PROFMASK_COMBAT); + idList strList; + + const idDict* decalDict = gameLocal.FindEntityDefDict( damageDict.GetString("def_entranceMark_decal"), false ); + if( !decalDict || !self->spawnArgs.GetBool("accepts_decals", "1") ) { + return; + } + + if( !impactMarkKey || !impactMarkKey[0] ) { + return; + } + + idStr impactMark = decalDict->RandomPrefix( impactMarkKey, gameLocal.random ); + if( !impactMark.Length() ) { + return; + } + + if( self->IsType(idBrittleFracture::Type) ) { + static_cast( self.GetEntity() )->ProjectDecal( origin, normal, gameLocal.GetTime(), NULL ); + } else { + hhUtils::SplitString( impactMark, strList ); + + //bjk: uses normal now + float depth = 10.0f; + + for( int ix = strList.Num() - 1; ix >= 0; --ix ) { + gameLocal.ProjectDecal( origin, -normal, depth, true, hhMath::Lerp(decalDict->GetVec2("mark_size"), gameLocal.random.RandomFloat()), strList[ix] ); //HUMANHEAD bjk: using normal + } + } + + //rww - if we were running this code on the server, we could do this: + /* + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + idVec2 markSize; + + msg.Init(msgBuf, sizeof(msgBuf)); + msg.BeginWriting(); + msg.WriteFloat(origin[0]); + msg.WriteFloat(origin[1]); + msg.WriteFloat(origin[2]); + msg.WriteDir(normal, 24); + msg.WriteDir(dir, 24); + + //AGH. rwwFIXME: configstring type system? + msg.WriteString(impactMark); + + if (self->IsType(idBrittleFracture::Type)) + { + msg.WriteBool(true); + msg.WriteShort(self.GetEntity()->entityNumber); + } + else + { + msg.WriteBool(false); + markSize = damageDict->GetVec2("mark_size"); + msg.WriteFloat(markSize[0]); + msg.WriteFloat(markSize[1]); + } + + self.GetEntity()->BroadcastEventReroute(idEntity::EVENT_PROJECT_DECAL, &msg); + */ +} + +void hhWoundManagerRenderEntity::ApplyWound( const idDict& damageDict, const char* woundKey, jointHandle_t jointNum, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ) { + PROFILE_SCOPE("ApplyWound", PROFMASK_COMBAT); + + if ( !g_bloodEffects.GetBool() ) { + return; + } + + if (! (self->fl.acceptsWounds && woundKey && *woundKey)) { + return; + } + + const idDict* woundDict = gameLocal.FindEntityDefDict( damageDict.GetString("def_entranceWound"), false ); + if( !woundDict ) { + return; + } + + const char* woundName = woundDict->RandomPrefix( woundKey, gameLocal.random ); + if( !woundName || !woundName[0] ) { + return; + } + + // for steam sound + idStr sndKey = "snd_"; + sndKey += woundKey; + const char *sndName = woundDict->GetString(sndKey.c_str()); + bool bHasSound = idStr::Length(sndName) > 0; + + /* // Optimization: works but it could cause problems in some of our non-euclidean spaces if + // a portal destination is less than 1024 units away + if (!bHasSound && !gameLocal.isMultiplayer && gameLocal.GetLocalPlayer()) { + // Don't spawn purely visual wounds when behind player + idVec3 playerOrigin = gameLocal.GetLocalPlayer()->GetOrigin(); + idMat3 playerView = gameLocal.GetLocalPlayer()->GetAxis(); + idVec3 toSpot = origin - playerOrigin; + float dist = toSpot.Normalize(); + + // Only do this optimization when within some distance to avoid problems with the effects being on the + // other side of a portal + if (dist < 1024.0f) { + float dot = toSpot * playerView[0]; + + // If spot is behind me and not right on the border, throw it out + if (dot < 0.0f && dist > 64.0f) { + return; + } + } + } + */ + + hhFxInfo fxInfo; + fxInfo.SetNormal( normal ); + if( !self.GetEntity()->IsType(idStaticEntity::Type) ) { + fxInfo.SetEntity( self.GetEntity() ); + } + fxInfo.RemoveWhenDone( true ); + //correct for server: self->BroadcastFxInfo( woundName, origin, dir.ToMat3(), &fxInfo ); + //however this function should -only- be called on the client. + if (gameLocal.isServer && !gameLocal.GetLocalPlayer()) { + gameLocal.Error("hhWoundManagerRenderEntity::ApplyWound called on non-listen server!"); + } + hhEntityFx *fx = self.GetEntity()->SpawnFxLocal( woundName, origin, normal.ToMat3(), &fxInfo, true ); + if (fx) { + fx->fl.neverDormant = true; + + if (bHasSound) { + const idSoundShader *shader = declManager->FindSound( sndName ); + fx->StartSoundShader(shader, SND_CHANNEL_ANY, 0, true); + } + } +} + +void hhWoundManagerRenderEntity::ApplySplatExit( const idDict& damageDict, const char* impactMarkKey, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ) { + PROFILE_SCOPE("ApplySplatExit", PROFMASK_COMBAT); + + if ( !g_bloodEffects.GetBool() ) { + return; + } + + if( gameLocal.random.RandomFloat() > 0.8f ) { + return; + } + + const idDict *splatDict = gameLocal.FindEntityDefDict( damageDict.GetString("def_exitSplat"), false ); + if( !splatDict || !self->spawnArgs.GetBool("produces_splats") ) { + return; + } + + if( !impactMarkKey || !impactMarkKey[0] ) { + return; + } + + const char* decal = splatDict->RandomPrefix( impactMarkKey, gameLocal.random ); + if( !decal || !decal[0] ) { + return; + } + + trace_t trace; + if( gameLocal.clip.TracePoint(trace, origin, origin+dir*200.0f, MASK_SHOT_RENDERMODEL, self.GetEntity() ) ) { + + surfTypes_t matterType = gameLocal.GetMatterType( self.GetEntity(), trace.c.material, "ApplySplatExit" ); + + gameLocal.ProjectDecal( trace.endpos, + -trace.c.normal, + splatDict->GetFloat("splat_dist", "10"), + true, + hhMath::Lerp(splatDict->GetVec2("mark_size"), + gameLocal.random.RandomFloat()), decal ); + } +} + +hhWoundManagerAnimatedEntity::hhWoundManagerAnimatedEntity( const idEntity* self ) : hhWoundManagerRenderEntity(self) { +} + +hhWoundManagerAnimatedEntity::~hhWoundManagerAnimatedEntity() { +} + +void hhWoundManagerAnimatedEntity::ApplyMark( const idDict& damageDict, const char* impactMarkKey, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ) { + idList strList; + + const idDict* overlayDict = gameLocal.FindEntityDefDict( damageDict.GetString("def_entranceMark_overlay"), false ); + if( !overlayDict || !self->spawnArgs.GetBool("accepts_decals", "1") ) { + return; + } + + if( !impactMarkKey && !impactMarkKey[0] ) { + return; + } + + idStr impactMark = overlayDict->RandomPrefix( impactMarkKey, gameLocal.random ); + if( !impactMark.Length() ) { + return; + } + + hhUtils::SplitString( impactMark, strList ); + for( int ix = strList.Num() - 1; ix >= 0; --ix ) { + self->ProjectOverlay( origin, dir, overlayDict->GetFloat("mark_size"), strList[ix] ); + } +} + +void hhWoundManagerAnimatedEntity::ApplyWound( const idDict& damageDict, const char* woundKey, jointHandle_t jointNum, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ) { + + if (! (self->fl.acceptsWounds && woundKey && *woundKey && jointNum != INVALID_JOINT)) { + return; + } + + const idDict* woundDict = gameLocal.FindEntityDefDict( damageDict.GetString("def_entranceWound"), false ); + if( !woundDict ) { + return; + } + + const char* woundName = woundDict->RandomPrefix( woundKey, gameLocal.random ); + if( !woundName || !woundName[0] ) { + return; + } + +#if !GOLD + //correct for server: self->BroadcastFxInfo( woundName, origin, dir.ToMat3(), &fxInfo ); + //however this function should -only- be called on the client. + if (gameLocal.isServer && !gameLocal.GetLocalPlayer()) { + gameLocal.Error("hhWoundManagerRenderEntity::ApplyWound called on non-listen server!"); + } +#endif + +#if 0 + hhFxInfo fxInfo; + fxInfo.SetNormal( normal ); + fxInfo.SetEntity( self.GetEntity() ); + fxInfo.RemoveWhenDone( true ); + + hhEntityFx* fx = self.GetEntity()->SpawnFxLocal( woundName, origin, normal.ToMat3(), &fxInfo, true ); + if (fx) { + fx->fl.neverDormant = true; + fx->BindToJoint( self.GetEntity(), jointNum, true); + } +#else //rww - do this in some way that is less hideous and slow (specialcased here, cause this function gets a lot of use) + //(avoids binding twice and some other needless logic) + if ( !g_skipFX.GetBool() ) { + idDict fxArgs; + + fxArgs.Set( "fx", woundName ); + fxArgs.SetBool( "start", true ); + fxArgs.SetBool( "removeWhenDone", true ); + fxArgs.SetVector( "origin", origin ); + fxArgs.SetMatrix( "rotation", normal.ToMat3() ); + + hhEntityFx *fx = (hhEntityFx *)gameLocal.SpawnClientObject( "func_fx", &fxArgs ); + if (fx) { + hhFxInfo fxInfo; + fxInfo.SetNormal( normal ); + fxInfo.SetEntity( self.GetEntity() ); + fxInfo.RemoveWhenDone( true ); + fx->SetFxInfo( fxInfo ); + + fx->fl.neverDormant = true; + fx->BindToJoint( self.GetEntity(), jointNum, true ); + fx->Show(); + } + } +#endif +} diff --git a/src/Prey/game_woundmanager.h b/src/Prey/game_woundmanager.h new file mode 100644 index 0000000..e434979 --- /dev/null +++ b/src/Prey/game_woundmanager.h @@ -0,0 +1,34 @@ +#ifndef __HH_WOUND_MANAGER_RENDERENTITY_H +#define __HH_WOUND_MANAGER_RENDERENTITY_H + +class hhWoundManagerRenderEntity { + public: + hhWoundManagerRenderEntity( const idEntity* self ); + virtual ~hhWoundManagerRenderEntity(); + + //Main entry point + void AddWounds( const idDeclEntityDef *def, surfTypes_t matterType, jointHandle_t jointNum, const idVec3& origin, const idVec3& normal, const idVec3& dir ); + virtual void DetermineWoundInfo( const trace_t& trace, const idVec3 velocity, jointHandle_t& jointNum, idVec3& origin, idVec3& normal, idVec3& dir ); + + protected: + virtual void ApplyWound( const idDict& damageDict, const char* woundKey, jointHandle_t jointNum, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ); + virtual void ApplyMark( const idDict& damageDict, const char* impactMarkKey, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ); + virtual void ApplySplatExit( const idDict& damageDict, const char* impactMarkKey, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ); + + protected: + idEntityPtr self; +}; + +class hhWoundManagerAnimatedEntity : public hhWoundManagerRenderEntity { + public: + hhWoundManagerAnimatedEntity( const idEntity* self ); + virtual ~hhWoundManagerAnimatedEntity(); + + protected: + + virtual void ApplyMark( const idDict& damageDict, const char* impactMarkKey, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ); + virtual void ApplyWound( const idDict& damageDict, const char* woundKey, jointHandle_t jointNum, const idVec3 &origin, const idVec3 &normal, const idVec3 &dir ); + +}; + +#endif \ No newline at end of file diff --git a/src/Prey/game_wraith.cpp b/src/Prey/game_wraith.cpp new file mode 100644 index 0000000..05fa725 --- /dev/null +++ b/src/Prey/game_wraith.cpp @@ -0,0 +1,974 @@ +//***************************************************************************** +//** +//** GAME_WRAITH.CPP +//** +//** Game code for the Wraith creature +//** +//***************************************************************************** + +// HEADER FILES --------------------------------------------------------------- + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS --------------------------------------------------------------------- + +#define TWEEN_BANK 1000 + +// TYPES ---------------------------------------------------------------------- + +// CLASS DECLARATIONS --------------------------------------------------------- + +const idEventDef EV_Flee( "", NULL ); +const idEventDef EV_PlayAnimMoveEnd( "", NULL ); +extern const idEventDef EV_SpawnDeathWraith; +extern const idEventDef AI_FindEnemy; + +CLASS_DECLARATION( hhMonsterAI, hhWraith ) + EVENT( EV_Flee, hhWraith::Event_Flee ) + EVENT( EV_PlayAnimMoveEnd, hhWraith::Event_PlayAnimMoveEnd ) + EVENT( AI_FindEnemy, hhWraith::Event_FindEnemy ) + EVENT( EV_Activate, hhWraith::Event_Activate ) +END_CLASS + +//============================================================================= +// +// hhWraith::Spawn +// +//============================================================================= +void hhWraith::Spawn(void) { + // List animations + flyAnim = GetAnimator()->GetAnim( "fly" ); + possessAnim = GetAnimator()->GetAnim( "alert" ); + leftAnim = GetAnimator()->GetAnim( "bankLeft" ); + rightAnim = GetAnimator()->GetAnim( "bankRight" ); + fleeAnim = GetAnimator()->GetAnim( "flee" ); + fleeInAnim = GetAnimator()->GetAnim( "fleeIn" ); + + lastAnim = NULL; + + canPossess = spawnArgs.GetBool( "possess", "1" ); + + // Physics and collision information + fl.takedamage = true; + + minDamageDist = spawnArgs.GetFloat( "minDamageDist", "50" ); + + // Initialize flight logic + straightTicks = 0; + damageTicks = 0; + turnTicks = 0; + + isScaling = false; + + // Initially launch the wraith forward + velocity = GetPhysics()->GetAxis()[0] * 100; + + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, flyAnim, gameLocal.time, 0 ); + + BecomeActive( TH_THINK ); + fl.neverDormant = true; + + nextCheckTime = gameLocal.time + spawnArgs.GetInt("trace_check_time", "200"); + lastCheckOrigin = GetPhysics()->GetOrigin(); + lastDamageTime = spawnArgs.GetFloat( "damageDelay", "0.5" ); + + if ( spawnArgs.GetBool( "spawnFlyUp", "0" ) ) { + state = WS_SPAWN; + + // Orient the wraith pointing upwards + desiredAxis[0] = idVec3( 0, 0, 1 ); + desiredAxis[1] = idVec3( 1, 0, 0 ); + desiredAxis[2] = idVec3( 0, -1, 0 ); + + // Set the speed of this movement + desiredVelocity = spawnArgs.GetFloat( "initialVelocity", "10" ); // TODO: Make this random + + // Calculate how far the wraith should fly and how long that should take + // NOTE: If we make the velocity a random range, then we can make this spawn time constant + countDownTimer = spawnArgs.GetFloat( "spawnTime", "0.5" ) * 60; // Half a second to raise up + } else { + state = WS_FLY; + } + + // Initialize .def values + velocity_xy = spawnArgs.GetFloat( "velocity_xy", "7" ) * (60.0f * USERCMD_ONE_OVER_HZ); + velocity_z = spawnArgs.GetFloat( "velocity_z", "1" ) * (60.0f * USERCMD_ONE_OVER_HZ); + velocity_z_fast = spawnArgs.GetFloat( "velocity_z_fast", "5" ) * (60.0f * USERCMD_ONE_OVER_HZ); + dist_z_close = spawnArgs.GetFloat( "dist_z_close", "5" ); + dist_z_far = spawnArgs.GetFloat( "dist_z_far", "100" ); + + turn_threshold = DEG2RAD( spawnArgs.GetFloat( "turn_threshold", "5" ) ); // CJR PCF 5/17/06: Removed unnecessary 30Hz compensation + turn_radius_max = DEG2RAD( spawnArgs.GetFloat( "turn_radius_max", "130" ) ); // CJR PCF 5/17/06: Remove unnecessary 30Hz compensation + + straight_ticks = spawnArgs.GetInt( "straight_ticks", "30" ) * (USERCMD_HZ / 60.0f); + damage_ticks = spawnArgs.GetFloat( "damage_ticks", "8" ) * (USERCMD_HZ / 60.0f); + turn_ticks = spawnArgs.GetFloat( "turn_ticks", "200" ) * (USERCMD_HZ / 60.0f); + + flee_speed_z = spawnArgs.GetFloat( "flee_speed_z", "10" ) * (60.0f * USERCMD_ONE_OVER_HZ); + + target_z_threshold = spawnArgs.GetFloat( "target_z_threshold", "20" ); + + // JRM: are we waiting for a trigger? Then no sound. + if(!spawnArgs.GetBool("trigger","0")) { + StartSound( "snd_flyloop", SND_CHANNEL_BODY ); + StartSound( "snd_sight", SND_CHANNEL_VOICE ); + + // If this wraith should flee when spawned, then kill it immediately and get it fleeing + // This is used by the possessed kids to spawn a wraith that flees immediately + if ( spawnArgs.GetBool("flee_at_spawn", "0" ) ) { + // Fleeing + Killed( this, this, 0, vec3_origin, 0 ); + + isScaling = true; + scaleStart = spawnArgs.GetFloat( "scaleStart", "1" ); + scaleEnd = spawnArgs.GetFloat( "scaleEnd", "1" ); + scaleTime = spawnArgs.GetFloat( "scaleTime", "1" ); + + // Set data for dynamically scaling the wraith + SetShaderParm( SHADERPARM_ANY_DEFORM, DEFORMTYPE_SCALE ); // Scale deform + SetShaderParm( SHADERPARM_ANY_DEFORM_PARM1, scaleStart ); // Scale deform + lastScaleTime = MS2SEC( gameLocal.time ) + scaleTime; + } + } + + Event_SetMoveType(MOVETYPE_FLY); // JRM to get the wraiths to move again + + GetPhysics()->SetContents( CONTENTS_SHOOTABLE|CONTENTS_SHOOTABLEBYARROW ); + GetPhysics()->SetClipMask( 0 ); + + bFaceEnemy = false; + nextDrop = 0; + nextChatter = 0; + nextPossessTime = 0; + + minChatter = SEC2MS(spawnArgs.GetFloat( "min_chatter_time", "3" )); + maxChatter = SEC2MS(spawnArgs.GetFloat( "max_chatter_time", "6" )); + + if ( !IsHidden() ) { + Hide(); + PostEventMS( &EV_Activate, 0, this ); + } +} + +//============================================================================= +// +// hhWraith::~hhWraith +// +//============================================================================= +hhWraith::~hhWraith() { + StopSound( SND_CHANNEL_BODY ); +} + +//============================================================================= +// +// hhWraith::Save +// +//============================================================================= +void hhWraith::Save( idSaveGame *savefile ) const { + savefile->WriteInt( lastAnim ); + savefile->WriteBool( canPossess ); + savefile->WriteVec3( velocity ); + savefile->WriteInt( damageTicks ); + savefile->WriteInt( straightTicks ); + savefile->WriteInt( turnTicks ); + savefile->WriteVec3( lastCheckOrigin ); + savefile->WriteFloat( lastDamageTime ); + savefile->WriteInt( state ); + savefile->WriteFloat( velocity_xy ); + savefile->WriteFloat( velocity_z ); + savefile->WriteFloat( velocity_z_fast ); + savefile->WriteFloat( dist_z_close ); + savefile->WriteFloat( dist_z_far ); + savefile->WriteFloat( turn_threshold ); + savefile->WriteFloat( turn_radius_max ); + savefile->WriteInt( straight_ticks ); + savefile->WriteInt( damage_ticks ); + savefile->WriteInt( turn_ticks ); + savefile->WriteFloat( flee_speed_z ); + savefile->WriteFloat( target_z_threshold ); + savefile->WriteFloat( minDamageDist ); + savefile->WriteBool( isScaling ); + savefile->WriteFloat( scaleStart ); + savefile->WriteFloat( scaleEnd ); + savefile->WriteFloat( lastScaleTime ); + savefile->WriteInt( countDownTimer ); + savefile->WriteMat3( desiredAxis ); + savefile->WriteFloat( desiredVelocity ); + savefile->WriteBool( bFaceEnemy ); + fxFly.Save( savefile ); +} + +//============================================================================= +// +// hhWraith::Restore +// +//============================================================================= +void hhWraith::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( lastAnim ); + savefile->ReadBool( canPossess ); + savefile->ReadVec3( velocity ); + savefile->ReadInt( damageTicks ); + savefile->ReadInt( straightTicks ); + savefile->ReadInt( turnTicks ); + savefile->ReadVec3( lastCheckOrigin ); + savefile->ReadFloat( lastDamageTime ); + savefile->ReadInt( (int &)state ); + savefile->ReadFloat( velocity_xy ); + savefile->ReadFloat( velocity_z ); + savefile->ReadFloat( velocity_z_fast ); + savefile->ReadFloat( dist_z_close ); + savefile->ReadFloat( dist_z_far ); + savefile->ReadFloat( turn_threshold ); + savefile->ReadFloat( turn_radius_max ); + savefile->ReadInt( straight_ticks ); + savefile->ReadInt( damage_ticks ); + savefile->ReadInt( turn_ticks ); + savefile->ReadFloat( flee_speed_z ); + savefile->ReadFloat( target_z_threshold ); + savefile->ReadFloat( minDamageDist ); + savefile->ReadBool( isScaling ); + savefile->ReadFloat( scaleStart ); + savefile->ReadFloat( scaleEnd ); + savefile->ReadFloat( lastScaleTime ); + savefile->ReadInt( countDownTimer ); + savefile->ReadMat3( desiredAxis ); + savefile->ReadFloat( desiredVelocity ); + savefile->ReadBool( bFaceEnemy ); + + // List animations + flyAnim = GetAnimator()->GetAnim( "fly" ); + possessAnim = GetAnimator()->GetAnim( "alert" ); + leftAnim = GetAnimator()->GetAnim( "bankLeft" ); + rightAnim = GetAnimator()->GetAnim( "bankRight" ); + fleeAnim = GetAnimator()->GetAnim( "flee" ); + fleeInAnim = GetAnimator()->GetAnim( "fleeIn" ); + + canPossess = spawnArgs.GetBool( "possess", "1" ); + + minDamageDist = spawnArgs.GetFloat( "minDamageDist", "50" ); + + // Initialize .def values + velocity_xy = spawnArgs.GetFloat( "velocity_xy", "7" ); + velocity_z = spawnArgs.GetFloat( "velocity_z", "1" ); + velocity_z_fast = spawnArgs.GetFloat( "velocity_z_fast", "5" ); + dist_z_close = spawnArgs.GetFloat( "dist_z_close", "5" ); + dist_z_far = spawnArgs.GetFloat( "dist_z_far", "100" ); + + turn_threshold = DEG2RAD( spawnArgs.GetFloat( "turn_threshold", "5" ) ); + turn_radius_max = DEG2RAD( spawnArgs.GetFloat( "turn_radius_max", "130" ) ); + + straight_ticks = spawnArgs.GetInt( "straight_ticks", "30" ) * (60.0f * USERCMD_ONE_OVER_HZ); + damage_ticks = spawnArgs.GetFloat( "damage_ticks", "8" ) * (60.0f * USERCMD_ONE_OVER_HZ); + turn_ticks = spawnArgs.GetFloat( "turn_ticks", "200" ) * (60.0f * USERCMD_ONE_OVER_HZ); + + flee_speed_z = spawnArgs.GetFloat( "flee_speed_z", "10" ); + + target_z_threshold = spawnArgs.GetFloat( "target_z_threshold", "20" ); + + nextDrop = 0; + nextChatter = 0; + nextPossessTime = gameLocal.time + 500; // Don't possess for half a second after loading + minChatter = SEC2MS(spawnArgs.GetFloat( "min_chatter_time", "3" )); + maxChatter = SEC2MS(spawnArgs.GetFloat( "max_chatter_time", "6" )); + + fxFly.Restore( savefile ); + + nextCheckTime = gameLocal.time + spawnArgs.GetInt("trace_check_time", "200"); + scaleTime = spawnArgs.GetFloat( "scaleTime", "1" ); +} + +//============================================================================= +// +// hhWraith::Think +// +//============================================================================= +void hhWraith::Think(void) { + if(!IsHidden()) { // JRM - NO TRACES WHEN HIDDEN! + CheckCollisions(); + + if (gameLocal.time > nextDrop) { + hhMonsterAI::Damage(this, this, vec3_origin, "damage_wraithminion", 0.05f, INVALID_JOINT); + nextDrop = gameLocal.time + 1000; + } + + if (gameLocal.time > nextChatter) { + StartSound( "snd_chatter", SND_CHANNEL_VOICE ); + nextChatter = gameLocal.time + (minChatter + gameLocal.random.RandomFloat() * (maxChatter - minChatter)); + } + } + + // Scale the wraith + if ( isScaling ) { + float delta = lastScaleTime - MS2SEC( gameLocal.time ); + if ( delta > 0 ) { + SetShaderParm( SHADERPARM_ANY_DEFORM_PARM1, hhMath::Lerp( scaleEnd, scaleStart, delta / scaleTime ) ); + } + } + + hhMonsterAI::Think(); // JRM: Bypassing hhAI::Think() +} + +//============================================================================= +// +// hhWraith::Event_Activate +// +//============================================================================= +void hhWraith::Event_Activate(idEntity *activator) { + if ( IsHidden() ) { + TeleportIn(activator); + return; + } + if ( spawnArgs.GetBool("flee_at_spawn", "0" ) ) { + // Fleeing + Killed( this, this, 0, vec3_origin, 0 ); + + isScaling = true; + scaleStart = spawnArgs.GetFloat( "scaleStart", "1" ); + scaleEnd = spawnArgs.GetFloat( "scaleEnd", "1" ); + scaleTime = spawnArgs.GetFloat( "scaleTime", "1" ); + + // Set data for dynamically scaling the wraith + SetShaderParm( SHADERPARM_ANY_DEFORM, DEFORMTYPE_SCALE ); // Scale deform + SetShaderParm( SHADERPARM_ANY_DEFORM_PARM1, scaleStart ); // Scale deform + lastScaleTime = MS2SEC( gameLocal.time ) + scaleTime; + } + + hhMonsterAI::Event_Activate(activator); + + // we've been waiting for a trigger + if (spawnArgs.GetBool("trigger")) { + StartSound( "snd_flyloop", SND_CHANNEL_BODY ); + StartSound( "snd_sight", SND_CHANNEL_VOICE ); + } + + + const char *defName = spawnArgs.GetString("fx_fly"); + if (defName && defName[0]) { + hhFxInfo fxInfo; + + fxInfo.SetNormal( -GetAxis()[0] ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( false ); + fxFly = SpawnFxLocal( defName, GetOrigin(), GetAxis(), &fxInfo, gameLocal.isClient ); + if (fxFly.IsValid()) { + fxFly->fl.neverDormant = true; + + fxFly->fl.networkSync = false; + fxFly->fl.clientEvents = true; + } + } +} + +//============================================================================= +// +// hhWraith::FindEnemy +// +//============================================================================= +void hhWraith::Event_FindEnemy( int useFOV ) { + int i; + idEntity *ent; + idActor *closest; + idActor *actor; + float closestDist; + float distSquared; + + closest = NULL; + closestDist = 99999999.0f; + + if (team != 0) { // Search players + for ( i = 0; i < gameLocal.numClients ; i++ ) { + ent = gameLocal.entities[ i ]; + + if ( !ent || !ent->IsType( idActor::Type ) ) { + continue; + } + + if ( ent->IsType( hhPlayer::Type ) ) { // Check if the player is really dead or deathwalking + hhPlayer *player = static_cast< hhPlayer * >( ent ); + if ( player->IsDead() || player->IsDeathWalking() ) { + continue; + } + } + + // Find the closest enemy + distSquared = ( GetOrigin() - ent->GetOrigin() ).LengthSqr(); + if ( distSquared < closestDist ) { + closestDist = distSquared; + closest = static_cast( ent ); + } + } + } + else { // Search monsters + int numMonsters = hhMonsterAI::allSimpleMonsters.Num(); + for ( i = 0; i < numMonsters ; i++ ) { + actor = hhMonsterAI::allSimpleMonsters[ i ]; + + if ( !actor || actor==this ) { + continue; + } + + // Ignore dormant entities! + if (actor->IsHidden() || actor->fl.isDormant) { + continue; + } + + if ( (actor->health <= 0) || !(ReactionTo(actor) & ATTACK_ON_SIGHT) ) { + continue; + } + + distSquared = (actor->GetOrigin() - GetOrigin()).LengthSqr(); + if ( distSquared < closestDist ) { + closestDist = distSquared; + closest = actor; + } + } + } + + if (closest) { + SetEnemy( closest ); + } + else { + // FIXME: No enemies found, should we just go away or circle for a while? + Damage(this, this, vec3_origin, "damage_suicide", 1.0f, INVALID_JOINT); + } + + idThread::ReturnEntity( closest ); +} + +//============================================================================= +// +// hhWraith::UpdateEnemyPosition +// +// This is here to override the default UpdateEnemyPosition, which +// runs code that is unnecessary for the wraiths +//============================================================================= +void hhWraith::UpdateEnemyPosition( void ) { + // Do nothing. Wraith's don't need to check if the enemy is no longer visible, since they can go through walls +} + +//============================================================================= +// +// hhWraith::EnemyDead +// +//============================================================================= +void hhWraith::EnemyDead() { + if ( !enemy.IsValid() || !enemy->IsType( hhPlayer::Type ) ) { // Only consider the enemy dead if the enemy is not a player + hhMonsterAI::EnemyDead(); + } else if ( enemy.IsValid() && enemy->IsType( hhPlayer::Type ) ) { // If the player is deathwalking, then the enemy is dead + hhPlayer *player = static_cast< hhPlayer * >( enemy.GetEntity() ); + if ( player->IsDead() || player->IsDeathWalking() ) { + hhMonsterAI::EnemyDead(); + } + } +} + +//============================================================================= +// +// hhWraith::FlyMove +// +//============================================================================= +void hhWraith::FlyMove( void ) { + // The state of the creature determines how it will move + switch ( state ) { + case WS_SPAWN: + FlyUp(); // Flying up right after spawn + break; + case WS_FLY: + FlyToEnemy(); + break; + case WS_FLEE: // Flying away + FlyAway(); + break; +// case WS_POSSESS_CHARGE: +// WraithDamageEnemy(); +// break; + case WS_STILL: // Wraith is not moving at all (playing a specific anim, etc) + return; + } + + // run the physics for this frame + physicsObj.UseFlyMove( true ); + physicsObj.UseVelocityMove( false ); + physicsObj.SetDelta( vec3_zero ); + physicsObj.ForceDeltaMove( disableGravity ); + RunPhysics(); +} + +//============================================================================= +// +// hhWraith::FlyToEnemy +// +//============================================================================= +void hhWraith::FlyToEnemy( void ) { + float delta = 0; + bool dir = false; + int anim; + + if ( !enemy.IsValid() ) { + PostEventMS( &AI_FindEnemy, 1, 0 ); + return; + } + + if (canPossess) { + // Determine which direction the wraith needs to turn to the enemy (if the wraith should turn) + if ( --straightTicks <= 0 ) { + dir = GetFacePosAngle( enemy->GetOrigin(), delta ); + + turnTicks++; // Add in the number of ticks that the wraith has been turning + // If the wraith has been turning for a very long period of time (more than 4 seconds), then force the wraith to go straight + + // Vary turn_ticks by +/- 25 depending on the DDA value + if ( turnTicks > this->turn_ticks - (25 * ((gameLocal.GetDDAValue() - 0.5f) * 2))) { + dir = 0; + delta = 0; + turnTicks = 0; + deltaViewAngles.yaw = 0; + } + } else { + dir = 0; + delta = 0; + deltaViewAngles.yaw = 0; + turnTicks = 0; + } + } + + // Turn the wraith, and set the correct bank animation + anim = flyAnim; + if ( delta > this->turn_threshold ) { // Wraith should turn + if ( dir ) { // Turn to the left + deltaViewAngles.yaw = this->turn_radius_max; + anim = leftAnim; + } else { + deltaViewAngles.yaw = -this->turn_radius_max; + anim = rightAnim; + } + } else if ( straightTicks <= 0 ) { // Straight at the player + deltaViewAngles.yaw = 0; + // Vary straight_ticks by +/- 25 depending on the DDA value + straightTicks = this->straight_ticks - (25 * ((gameLocal.GetDDAValue() - 0.5f) * 2)); // Stay on this path for a short period of time + } + + // Actually set the animation + if ( anim != lastAnim ) { + GetAnimator()->ClearAllAnims( gameLocal.time, TWEEN_BANK ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, TWEEN_BANK ); + lastAnim = anim; + } + + velocity = viewAxis[0] * this->velocity_xy; + if ( damageTicks > 0 ) { // Fly slower if damaged + velocity *= 0.85f; + velocity += 10 * idVec3( gameLocal.random.CRandomFloat(), gameLocal.random.CRandomFloat(), gameLocal.random.CRandomFloat() ); + + damageTicks--; + } + + // Equalize the z-height of the wraith relative to its target + idVec3 enemyOrigin = enemy->GetEyePosition(); + idVec3 origin = GetPhysics()->GetOrigin(); + + // Factor in fly_offset + enemyOrigin.z += fly_offset; + + // Adjust z-velocity based upon the delta Z to target + velocity.z = 0; + if( ( origin.z > enemyOrigin.z + this->target_z_threshold ) || ( origin.z < enemyOrigin.z - this->target_z_threshold ) ) { + float newZ = enemyOrigin.z + target_z_threshold * 0.5f; + float deltaZ = newZ - origin.z; + if ( abs( deltaZ ) > this->dist_z_far ) { + velocity.z = this->velocity_z_fast * ( ( deltaZ > 0 ) ? 1 : -1 ); + } else if ( abs( deltaZ ) > this->dist_z_close ) { + velocity.z = this->velocity_z * ( ( deltaZ > 0 ) ? 1 : -1 ); + } + } + + // 1.5x the velocity if DDA is 1.0, half it for DDA = 0.0, leave it the same for normal (DDA = 0.5) + GetPhysics()->SetOrigin( GetPhysics()->GetOrigin() + (velocity * (gameLocal.GetDDAValue() + 0.5f))); + + UpdateVisuals(); +} + +//============================================================================= +// +// hhWraith::FlyAway +// +// Wraiths will automatically fly away from their enemy and up +// The Wraith will eventually be removed from the world in the CheckFleeRemove function +//============================================================================= +void hhWraith::FlyAway( void ) { + + velocity.x = 0; + velocity.y = 0; + velocity.z = this->flee_speed_z; + + GetPhysics()->SetOrigin( GetPhysics()->GetOrigin() + velocity ); + + UpdateVisuals(); + + // Check to see if the wraith has flown out of the world + CheckFleeRemove(); +} + +//============================================================================= +// +// hhWraith::FlyUp +// +// mdl: Wraiths will automatically fly up after being spawned if spawnFlyUp is set to 1 +// NOT related to flying away during death, that's FlyAway() +//============================================================================= +void hhWraith::FlyUp( void ) { + idVec3 velocity; + + // Turn towards desired angle + // TODO: Make this smooth + SetAxis( desiredAxis ); + + // Apply velocity + velocity = desiredAxis[0] * desiredVelocity; + SetOrigin( GetOrigin() + velocity ); + + UpdateVisuals(); + + // If the wraith is near the end of the spawn, then smoothly rotate the wraith to the horizontal + if ( --countDownTimer <= 0 ) { + state = WS_FLY; + } +} + +//============================================================================= +// +// hhWraith::TurnTowardEnemy +// +//============================================================================= +void hhWraith::TurnTowardEnemy() { + float delta; + bool dir; + + if ( !enemy.IsValid() ) { + return; + } + + // Face torward the enemy + dir = GetFacePosAngle( enemy->GetOrigin(), delta ); + + if ( delta > turn_threshold ) { + // Turn based upon delta angles + if ( dir ) { // Turn to the right + deltaViewAngles.yaw = turn_radius_max; + } else { + deltaViewAngles.yaw = -turn_radius_max; + } + } + + UpdateVisuals(); +} + +//============================================================================= +// +// hhWraith::CheckFleeRemove +// +//============================================================================= +void hhWraith::CheckFleeRemove( void ) { + // Check if the Wraith has flown out of the world + if( gameLocal.clip.Contents( GetPhysics()->GetOrigin() + idVec3(0, 0, 48), GetPhysics()->GetClipModel(), viewAxis, CONTENTS_SOLID, this ) ) { + Hide(); + PostEventSec( &EV_Remove, 2.0f ); // Keep the wraith around for a few seconds so the sounds can finish + state = WS_STILL; // No reason to keep moving the wraith + } +} + +//============================================================================= +// +// hhWraith::Damage +// +//============================================================================= +void hhWraith::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + hhMonsterAI::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); + damageTicks = this->damage_ticks; +} + +//============================================================================= +// +// hhWraith::Killed +// +//============================================================================= +void hhWraith::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + hhFxInfo fx; + //fx.SetEntity( this ); + fx.RemoveWhenDone( true ); + const char *deathFX; + + // Stop flyloop and chatter sounds, if any + StopSound( SND_CHANNEL_BODY ); + StopSound( SND_CHANNEL_VOICE ); + + if ( inflictor == this ) { + // If we timed out, don't drop a soul + spawnArgs.Delete( "def_dropSoul" ); + deathFX = spawnArgs.GetString( "fx_death" ); + } else { + // Use alternate death fx if we were killed by the player + deathFX = spawnArgs.GetString( "fx_death2" ); + } + + SpawnFxLocal( deathFX, GetOrigin(), GetAxis(), &fx ); + + SAFE_REMOVE(fxFly); + + hhMonsterAI::Killed( inflictor, attacker, damage, dir, location ); + + StartSound( "snd_death", SND_CHANNEL_VOICE ); + + Hide(); + + // activate targets + //ActivateTargets( this ); + + PostEventSec( &EV_Remove, 4 ); +} + +//============================================================================= +// +// hhWraith::CheckCollisions +// +// Check if the wraith is entering or exiting the world +//============================================================================= +void hhWraith::CheckCollisions( void ) { + + if (gameLocal.time < nextCheckTime) { + return; + } + + hhFxInfo fxInfo; + trace_t tr1; + trace_t tr2; + idVec3 jointPos; + idMat3 jointAxis; + + if (!GetJointWorldTransform( spawnArgs.GetString("joint_collision"), jointPos, jointAxis )) { + jointPos = GetOrigin(); + jointAxis = GetAxis(); + } + + // Trace to determine if the wraith just flew through a wall, or just emerged from a wall + // For speed, this only checks against the world + idVec3 dir, point; + gameLocal.clip.TracePoint( tr1, jointPos, lastCheckOrigin, MASK_SHOT_BOUNDINGBOX, this ); // Check if we entered something solid + gameLocal.clip.TracePoint( tr2, lastCheckOrigin, jointPos, MASK_SHOT_BOUNDINGBOX, this ); // Check if we exited something solid + + idVec3 playerOrigin = gameLocal.GetLocalPlayer()->GetOrigin(); + if ( tr1.fraction < 1.0f ) { + point = tr1.c.point; + dir = tr1.c.normal; + } + else if ( tr2.fraction < 1.0f) { + point = tr2.c.point; + dir = tr2.c.normal; + } + if ( tr1.fraction < 1.0f && tr2.fraction < 1.0f ) { + if ( (tr1.c.point - playerOrigin).LengthSqr() <= (tr2.c.point - playerOrigin).LengthSqr() ) { + point = tr1.c.point; + dir = tr1.c.normal; + } + else { + point = tr2.c.point; + dir = tr2.c.normal; + } + } + + if ( tr1.fraction < 1.0f || tr2.fraction < 1.0f ) { + // Spawn an FX at wall collision + //TODO: IMPORTANT: This currently spawns a system every check while passing through something. This creates enormous + // amounts of particles. Fix so there is a single system spawned on the way in and a single system spawned on the way out. + //TODO: This currently hits players so you get a big blob of particles right in your face and right behind you. + const char *hitWallFxName = spawnArgs.GetString("fx_hitwall"); + if (hitWallFxName && *hitWallFxName) { + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfo(hitWallFxName, point, mat3_identity, &fxInfo); + } + + + // Project a spooge decal on the wall here + const char *decalName = spawnArgs.GetString("mtr_decal", NULL); + if (decalName && *decalName) { + float depth = spawnArgs.GetFloat("decal_trace"); + float size = spawnArgs.GetFloat( "decal_size" ); + +// gameRenderWorld->DebugArrow(colorRed, point, point + dir*128, 5, 10000); + + gameLocal.ProjectDecal(tr1.c.point, -tr1.c.normal, depth, true, size, decalName); + gameLocal.ProjectDecal(tr2.c.point, -tr2.c.normal, depth, true, size, decalName); + } + } + + // Possession check + if ( canPossess && gameLocal.time > nextPossessTime ) { + gameLocal.clip.TraceBounds( tr1, lastCheckOrigin, jointPos, GetPhysics()->GetBounds(), MASK_SHOT_BOUNDINGBOX, this ); + if ( gameLocal.entities[ tr1.c.entityNum ] && gameLocal.entities[ tr1.c.entityNum ]->IsType( idActor::Type ) ) { + // If we hit a possessable actor, possess them + idActor *actor = reinterpret_cast ( gameLocal.entities[ tr1.c.entityNum ] ); + if ( actor != lastActor.GetEntity() ) { + // Play the attack sound + StartSound( "snd_attack", SND_CHANNEL_VOICE ); + if ( actor->IsType( hhPlayer::Type ) ) { + hhPlayer *player = reinterpret_cast ( gameLocal.entities[ tr1.c.entityNum ] ); + int power = player->GetSpiritPower(); + power -= 25; + + if ( power <= 0 ) { // Possess the player + power = 0; + // mdl: Wraiths no longer possess + //if ( player->CanBePossessed() ) { + // Possessable + //WraithPossess( player ); + //} else + if ( enemy.GetEntity() == player && player->IsSpiritWalking() && !player->IsPossessed() ) { + // Send the player back to his body + reinterpret_cast (enemy.GetEntity())->DisableSpiritWalk(5); + // Don't immediately possess if the players body happens to be right there. + nextPossessTime = gameLocal.time + 1000; + } + } else { // Gain health from the player's spirit power + health += 5; + if ( health > spawnHealth ) { + health = spawnHealth; + } + } + player->SetSpiritPower( power ); + + }// else if ( actor->CanBePossessed() ) { + // // Possess the actor + // WraithPossess( actor ); + //} + } + lastActor = actor; + } else { + lastActor = NULL; + } + } + + nextCheckTime = gameLocal.time + spawnArgs.GetInt("trace_check_time", "200"); + lastCheckOrigin = jointPos; +} + +//============================================================================= +// +// hhWraith::Event_Flee +// +// The wraith is about to flee and has already played the flee intro anim +//============================================================================= +void hhWraith::Event_Flee( ) { + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, fleeAnim, gameLocal.time, 0 ); + state = WS_FLEE; +} + +//============================================================================= +// +// hhWraith::PlayAnimMove +// +//============================================================================= +void hhWraith::PlayAnimMove( int anim, int blendTime ) { + GetAnimator()->ClearAllAnims( gameLocal.time, blendTime ); + GetAnimator()->PlayAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, blendTime ); + state = WS_STILL; + PostEventMS( &EV_PlayAnimMoveEnd, GetAnimator()->GetAnim( possessAnim )->Length() - blendTime ); +} + +//============================================================================= +// +// hhWraith::PlayAnimMoveEnd +// +//============================================================================= +void hhWraith::PlayAnimMoveEnd() { + idVec3 boneOrigin; + idMat3 boneAxis; + GetJointWorldTransform( "Head", boneOrigin, boneAxis ); + + GetPhysics()->SetOrigin( boneOrigin ); // Move the wraith to the end of the animation + + GetAnimator()->ClearAllAnims( gameLocal.time, 0 ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, flyAnim, gameLocal.time, 0 ); + state = WS_FLY; + + damageTicks = 0; +} + +//============================================================================= +// +// hhWraith::WraithPossess +// +// Actually possess an actor +//============================================================================= +void hhWraith::WraithPossess( idActor *actor ) { + hhFxInfo fxInfo; + + actor->Possess( this ); + + // Spawn in a flash + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_possessionFlash", GetOrigin(), GetAxis(), &fxInfo ); + + // activate targets + ActivateTargets( actor ); + + PostEventMS( &EV_Remove, 0 ); +} + + +//============================================================================= +// +// hhWraith::Event_PlayAnimMoveEnd +// +//============================================================================= +void hhWraith::Event_PlayAnimMoveEnd( ) { + PlayAnimMoveEnd(); +} + +void hhWraith::Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ) { + // Only switch to the proxy if the player has no spirit power + //if ( player->GetSpiritPower() == 0 ) { + // hhMonsterAI::Event_EnemyIsSpirit( player, proxy ); + //} +} + +void hhWraith::Event_EnemyIsPhysical( hhPlayer *player, hhSpiritProxy *proxy ) { + // We don't care whether we can see the enemy or not + enemy = player; +} + +void hhWraith::TeleportIn( idEntity *activator ) { + if ( spawnArgs.GetBool( "quickSpawn" ) ) { + PostEventMS( &EV_Show, 0 ); + PostEventMS( &EV_Activate, 50, activator ); + } + else { + hhFxInfo fx; + fx.RemoveWhenDone( true ); + fx.SetEntity( this ); + SpawnFxLocal( spawnArgs.GetString( "fx_spawn" ), GetOrigin(), GetAxis(), &fx ); + PostEventMS( &EV_Show, 1000 ); + PostEventMS( &EV_Activate, 1050, activator ); + } +} + +void hhWraith::Portalled(idEntity *portal) { + hhMonsterAI::Portalled( portal ); + if ( fxFly.IsValid() ) { + hhFxInfo fxInfo; + fxInfo.SetNormal( -GetAxis()[0] ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( false ); + + fxFly->SetFxInfo( fxInfo ); + + // Reset the fx system + fxFly->Stop(); + fxFly->Start( gameLocal.time ); + } +} + diff --git a/src/Prey/game_wraith.h b/src/Prey/game_wraith.h new file mode 100644 index 0000000..7a6f657 --- /dev/null +++ b/src/Prey/game_wraith.h @@ -0,0 +1,128 @@ + +#ifndef __GAME_WRAITH_H__ +#define __GAME_WRAITH_H__ + +typedef enum wraithState_s { + WS_SPAWN = 0, + WS_FLY, + WS_FLEE, + WS_POSSESS_CHARGE, + WS_DEATH_CHARGE, + WS_STILL +} wraithState_t; + +class hhWraith : public hhMonsterAI { + public: + CLASS_PROTOTYPE(hhWraith); + + void Spawn(void); + virtual ~hhWraith(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void UpdateEnemyPosition( void ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void Think( void ); + + virtual int HasAmmo( ammo_t type, int amount ) { return 0; } + virtual bool UseAmmo( ammo_t type, int amount ) { return false ; } + + protected: + virtual void EnemyDead(); + + // State flight functions + virtual void FlyUp( void ); + virtual void FlyMove( void ); + virtual void FlyToEnemy( void ); + virtual void FlyAway( void ); + + void CheckFleeRemove( void ); + void WraithPossess( idActor *actor ); + void CheckCollisions( void ); + + void PlayAnimMove( int anim, int blendTime ); + void PlayAnimMoveEnd(); + void TurnTowardEnemy(); + + void Event_FindEnemy( int useFOV ); + void Event_TurnTowardEnemy(); + void Event_Flee(); + void Event_PlayAnimMoveEnd( ); + virtual void Event_Activate(idEntity *activator); + + virtual void Event_EnemyIsSpirit( hhPlayer *player, hhSpiritProxy *proxy ); + virtual void Event_EnemyIsPhysical( hhPlayer *player, hhSpiritProxy *proxy ); + + virtual void TeleportIn( idEntity *activator ); + virtual void StartDisposeCountdown() { } // Doesn't apply to wraiths + + virtual void Portalled(idEntity *portal); + + protected: + // Animations + int flyAnim; + int possessAnim; + int leftAnim; + int rightAnim; + int fleeAnim; + int fleeInAnim; + + int lastAnim; // Used in turning flight logic + + // Variables + bool canPossess; + + idVec3 velocity; + + int damageTicks; // # of ticks to act damaged (fly slower, flash red) + int straightTicks; // # of ticks to fly straight + int turnTicks; // # if ticks when turning before the wraith will be forced to fly straight + + int nextCheckTime; + idVec3 lastCheckOrigin; + float lastDamageTime; // used to delay the damage check after applying damage + + wraithState_t state; + + // .def file variables + float velocity_xy; + float velocity_z; + float velocity_z_fast; + float dist_z_close; + float dist_z_far; + float turn_threshold; + float turn_radius_max; + int straight_ticks; // Number of ticks to delay before turning + int damage_ticks; + int turn_ticks; // Max time to spend turning before going straight + float flee_speed_z; + float target_z_threshold; + + float minDamageDist; // Damage distance check + + // Scale variables + bool isScaling; + float scaleStart; + float scaleEnd; + float scaleTime; + float lastScaleTime; + + int countDownTimer; + idMat3 desiredAxis; + float desiredVelocity; + + bool bFaceEnemy; + int nextDrop; + int nextChatter; + int minChatter; + int maxChatter; + + idEntityPtr lastActor; + + int nextPossessTime; + + idEntityPtr fxFly; +}; + +#endif /* __GAME_WRAITH_H__ */ diff --git a/src/Prey/game_zone.cpp b/src/Prey/game_zone.cpp new file mode 100644 index 0000000..7902135 --- /dev/null +++ b/src/Prey/game_zone.cpp @@ -0,0 +1,1182 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_SetGravityVector("setgravity", "v"); +const idEventDef EV_SetGravityFactor("setgravityfactor", "f"); +const idEventDef EV_DeactivateZone("", NULL); + +//----------------------------------------------------------------------- +// +// hhZone +// +// NOTE: You do not get a EntityLeaving() callback for entities that are +// removed. Could possibly use hhSafeEntitys to tell when they are removed +// but what's the point of calling EntityLeaving() with an invalid pointer. +//----------------------------------------------------------------------- + +ABSTRACT_DECLARATION(hhTrigger, hhZone) + EVENT( EV_DeactivateZone, hhZone::Event_TurnOff ) + EVENT( EV_Enable, hhZone::Event_Enable ) + EVENT( EV_Disable, hhZone::Event_Disable ) + EVENT( EV_Touch, hhZone::Event_Touch ) +END_CLASS + +//NOTE: If this works, this entity can cease inheriting from trigger, just need to take the +// tracemodel creation logic, isSimpleBox variable, make our own enable/disable functions +// touch goes away, triggeraction goes away, much simpler interface +#define ZONES_ALWAYS_ACTIVE 1 // testing: want to be able to use dormancy to turn off --pdm + +void hhZone::Spawn(void) { + slop = 0.0f; // Extra slop for bounds check +#if !ZONES_ALWAYS_ACTIVE + fl.neverDormant = true; +#endif + +#if ZONES_ALWAYS_ACTIVE + fl.neverDormant = false; + BecomeActive(TH_THINK); + bActive = true; + bEnabled = true; +#endif +} + +void hhZone::Save(idSaveGame *savefile) const { + savefile->WriteInt(zoneList.Num()); // idList + for (int i=0; iWriteInt(zoneList[i]); + } + + savefile->WriteFloat(slop); +} + +void hhZone::Restore( idRestoreGame *savefile ) { + int num; + + zoneList.Clear(); // idList + savefile->ReadInt(num); + zoneList.SetNum(num); + for (int i=0; iReadInt(zoneList[i]); + } + + savefile->ReadFloat(slop); +} + +bool hhZone::ValidEntity(idEntity *ent) { + return (ent && ent!=this && + ent->GetPhysics() && + !ent->GetPhysics()->IsType(idPhysics_Static::Type) && + ent->GetPhysics()->GetContents() != 0); +} + +void hhZone::Empty() { +} + +bool hhZone::ContainsEntityOfType(const idTypeInfo &t) { + idEntity *touch[ MAX_GENTITIES ]; + idBounds clipBounds; + + clipBounds.FromTransformedBounds( GetPhysics()->GetBounds(), GetOrigin(), GetAxis() ); + int num = gameLocal.clip.EntitiesTouchingBounds( clipBounds.Expand(slop), MASK_SHOT_BOUNDINGBOX, touch, MAX_GENTITIES ); + for (int i=0; iIsType(t)) { + gameLocal.Printf("Contains a %s\n", t.classname); + return true; + } + } + gameLocal.Printf("Doesn't contain a %s\n", t.classname); + return false; +} + +bool PointerInList(idEntity *target, idEntity **list, int num) { + for (int j=0; j < num; j++ ) { + if (list[j] == target) { + return true; + } + } + return false; +} + +void hhZone::ResetZoneList() { + // Call Leaving for anything previously entered + idEntity *previouslyInZone; + for (int i=0; i < zoneList.Num(); i++ ) { + previouslyInZone = gameLocal.entities[zoneList[i]]; + + if (previouslyInZone) { + EntityLeaving(previouslyInZone); + } + } + zoneList.Clear(); +} + +void hhZone::TriggerAction(idEntity *activator) { + CancelEvents(&EV_DeactivateZone); + // Turn on until all encroachers are gone + BecomeActive(TH_THINK); +} + +void hhZone::ApplyToEncroachers() { + idEntity *touch[ MAX_GENTITIES ]; + idEntity *previouslyInZone; + idEntity *encroacher; + int i, num; + + idBounds clipBounds; + clipBounds.FromTransformedBounds( GetPhysics()->GetBounds(), GetOrigin(), GetAxis() ); + + // Find all encroachers + if (isSimpleBox) { + num = gameLocal.clip.EntitiesTouchingBounds( clipBounds.Expand(slop), MASK_SHOT_BOUNDINGBOX | CONTENTS_PROJECTILE | CONTENTS_TRIGGER, touch, MAX_GENTITIES ); // CONTENTS_TRIGGER for walkthrough movables + } + else { + num = hhUtils::EntitiesTouchingClipmodel( GetPhysics()->GetClipModel(), touch, MAX_GENTITIES, MASK_SHOT_BOUNDINGBOX | CONTENTS_TRIGGER ); + } + + // for anything previously applied, but no longer encroaching, call EntityLeaving() + for (i=0; i < zoneList.Num(); i++ ) { + previouslyInZone = gameLocal.entities[zoneList[i]]; + + if (previouslyInZone) { + if (!ValidEntity(previouslyInZone) || !PointerInList(previouslyInZone, touch, num)) { + // We've applied before, but it's no longer encroaching + EntityLeaving(previouslyInZone); + + // NOTE: Rather than removing and dealing with the list shifting, we reconstruct the list + // from the touch list later + } + } + } + + // Check touch list for any newly entered encroachers + for (i = 0; i < num; i++ ) { + encroacher = touch[i]; + if (ValidEntity(encroacher)) { + if (zoneList.FindIndex(encroacher->entityNumber) == -1) { + EntityEntered(encroacher); + } + } + } + + // Call all encroachers and rebuild list + zoneList.Clear(); //fixme: could make a version of clear() that doesn't deallocate the memory + for (i = 0; i < num; i++ ) { + encroacher = touch[i]; + if (ValidEntity(encroacher)) { + zoneList.Append(encroacher->entityNumber); + EntityEncroaching(encroacher); + } + } + + // Deactivate if no encroachers left + if (!zoneList.Num()) { + Empty(); +#if !ZONES_ALWAYS_ACTIVE + PostEventMS(&EV_DeactivateZone, 0); +#endif + } +} + +void hhZone::Think() { + if (thinkFlags & TH_THINK) { + ApplyToEncroachers(); + } +} + +void hhZone::Event_TurnOff() { + BecomeInactive(TH_THINK); + bActive = false; +} + +void hhZone::Event_Enable( void ) { + hhTrigger::Event_Enable(); + TriggerAction(this); +} + +void hhZone::Event_Disable( void ) { + BecomeInactive(TH_THINK); + ResetZoneList(); + hhTrigger::Event_Disable(); +} + +void hhZone::Event_Touch( idEntity *other, trace_t *trace ) { + CancelEvents(&EV_DeactivateZone); + // Turn on until all encroachers are gone + BecomeActive(TH_THINK); + + bActive = true; +} + + +//----------------------------------------------------------------------- +// +// hhTriggerZone +// +// Zone used for precise trigger/untrigger mechanic. Fires trigger once +// upon a valid entity entering, and again when a valid entity leaves. Also, +// optionally calls a function for each entity in the volume each tick. +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhTriggerZone) +END_CLASS + +void hhTriggerZone::Spawn() { + funcRefInfo.ParseFunctionKeyValue( spawnArgs.GetString("inCallRef") ); +} + +void hhTriggerZone::Save(idSaveGame *savefile) const { + savefile->WriteStaticObject( funcRefInfo ); +} + +void hhTriggerZone::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject( funcRefInfo ); +} + +bool hhTriggerZone::ValidEntity(idEntity *ent) { + return (hhZone::ValidEntity(ent) && !IsType(hhProjectile::Type)); +} + +void hhTriggerZone::EntityEntered(idEntity *ent) { + ActivateTargets(ent); +} + +void hhTriggerZone::EntityLeaving(idEntity *ent) { + ActivateTargets(ent); +} + +void hhTriggerZone::EntityEncroaching( idEntity *ent ) { + if (funcRefInfo.GetFunction() != NULL) { + funcRefInfo.SetParm_Entity( ent, 0 ); + funcRefInfo.Verify(); + funcRefInfo.CallFunction( spawnArgs ); + } +} + +//----------------------------------------------------------------------- +// +// hhGravityZoneBase +// +//----------------------------------------------------------------------- + +ABSTRACT_DECLARATION(hhZone, hhGravityZoneBase) +END_CLASS + +void hhGravityZoneBase::Spawn(void) { + bReorient = spawnArgs.GetBool("reorient"); + bShowVector = spawnArgs.GetBool("showVector"); + bKillsMonsters = spawnArgs.GetBool("killmonsters"); + + //rww - avoid dictionary lookup post-spawn + gravityOriginOffset = vec3_origin; + if (spawnArgs.GetVector("override_origin", gravityOriginOffset.ToString(), gravityOriginOffset)) { + gravityOriginOffset -= GetOrigin(); + } + + //rww - sync over network + fl.networkSync = true; +} + +void hhGravityZoneBase::Save(idSaveGame *savefile) const { + savefile->WriteBool(bReorient); + savefile->WriteBool(bKillsMonsters); + savefile->WriteBool(bShowVector); + savefile->WriteVec3(gravityOriginOffset); +} + +void hhGravityZoneBase::Restore( idRestoreGame *savefile ) { + savefile->ReadBool(bReorient); + savefile->ReadBool(bKillsMonsters); + savefile->ReadBool(bShowVector); + savefile->ReadVec3(gravityOriginOffset); +} + +//rww - network code +void hhGravityZoneBase::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(bReorient, 1); + msg.WriteFloat(slop); +} + +void hhGravityZoneBase::ReadFromSnapshot( const idBitMsgDelta &msg ) { + bReorient = !!msg.ReadBits(1); + slop = msg.ReadFloat(); +} + +void hhGravityZoneBase::ClientPredictionThink( void ) { + Think(); +} +//rww - end network code + +const idVec3 hhGravityZoneBase::GetGravityOrigin() const { + return GetOrigin()+gravityOriginOffset; +} + +bool hhGravityZoneBase::ValidEntity(idEntity *ent) { + if (!ent) { + return false; + } + if (ent->fl.ignoreGravityZones) { + return false; + } + + if (ent->IsType(hhProjectile::Type) && ent->GetPhysics()->GetGravity() == vec3_origin) { + return false; // Projectiles with zero gravity + } + if (ent->IsType(hhPlayer::Type)) { + hhPlayer *pl = static_cast(ent); + if (pl->noclip) { + return false; // Noclipping players + } + else if (pl->spectating) { + return false; //spectating players + } + else if (gameLocal.isMultiplayer && ent->health <= 0) { + return false; //dead mp players (only the prox ragdoll needs gravity) + } + } + if (ent->IsType(hhVehicle::Type)) { + if (static_cast(ent)->IsNoClipping()) { + return false; // Noclipping vehicles + } + if (ent->IsType(hhShuttle::Type) && static_cast(ent)->IsConsole()) { + return false; // unpiloted shuttles + } + } + if (ent->IsType(hhPortal::Type) ) { + return true; // Portals are always valid entities in zones + } + + if (!hhZone::ValidEntity( ent )) { + return false; + } + + return true; +} + +bool hhGravityZoneBase::TouchingOtherZones(idEntity *ent, bool traceCheck, idVec3 &otherInfluence) { //rww + if (!ent->GetPhysics()) { + return false; + } + + bool hitAny = false; + + otherInfluence.Zero(); + + idBounds clipBounds; + + idEntity *touch[ MAX_GENTITIES ]; + clipBounds.FromTransformedBounds( ent->GetPhysics()->GetBounds(), ent->GetOrigin(), ent->GetAxis() ); + int num = gameLocal.clip.EntitiesTouchingBounds( clipBounds, GetPhysics()->GetContents(), touch, MAX_GENTITIES ); + for (int i = 0; i < num; i++) { + if (touch[i] && touch[i]->entityNumber != entityNumber && touch[i]->IsType(hhGravityZoneBase::Type)) { + //touching the object, isn't me, and seems to be another gravity zone + bool touchValid = false; + + if (traceCheck) { //let's perform a trace from the ent's origin to see which zone is hit first. (this is not an ideal solution, but it works) + trace_t tr; + const int checkContents = GetPhysics()->GetContents(); + const idVec3 &start = ent->GetOrigin(); + const float testLength = 512.0f; + idVec3 end; + + //first trace against the other + end = (touch[i]->GetPhysics()->GetBounds().GetCenter()-start).Normalize()*testLength; + gameLocal.clip.TracePoint(tr, start, end, checkContents, ent); + if (tr.c.entityNum == touch[i]->entityNumber) { //if the trace actually hit the other one + float otherFrac = tr.fraction; + + //now trace against me + end = (GetPhysics()->GetBounds().GetCenter()-start).Normalize()*testLength; + gameLocal.clip.TracePoint(tr, start, GetPhysics()->GetBounds().GetCenter(), checkContents, ent); + if (tr.c.entityNum != entityNumber || tr.fraction >= otherFrac) { //if the impact was further away (or same, don't want fighting), i lose. + touchValid = true; + } + } + } + else { + touchValid = true; + } + + if (touchValid) { + //accumulate force from other zones + hhGravityZoneBase *zone = static_cast(touch[i]); + if (zone->isSimpleBox || ent->GetPhysics()->ClipContents(zone->GetPhysics()->GetClipModel())) { //if not simple box perform a clip check + idVec3 grav = zone->GetCurrentGravity(ent->GetOrigin()); + hitAny = true; + + grav.Normalize(); + otherInfluence += grav; + + otherInfluence.Normalize(); + } + } + } + } + + return hitAny; +} + +void hhGravityZoneBase::EntityEntered( idEntity *ent ) { + if( ent->RespondsTo(EV_ShouldRemainAlignedToAxial) ) { + ent->ProcessEvent( &EV_ShouldRemainAlignedToAxial, (int)false ); + } + if( ent->RespondsTo(EV_OrientToGravity) ) { + ent->ProcessEvent( &EV_OrientToGravity, (int)bReorient ); + } +} + +void hhGravityZoneBase::EntityLeaving( idEntity *ent ) { + if( ent->RespondsTo(EV_ShouldRemainAlignedToAxial) ) { + ent->ProcessEvent( &EV_ShouldRemainAlignedToAxial, (int)true ); + } + + // Instead of reseting gravity here, post a message to do it, so if we are transitioning + // to another gravity zone or wallwalk, there won't be any discontinuities + if (gameLocal.isClient && !ent->fl.clientEvents && !ent->fl.clientEntity && ent->IsType(hhProjectile::Type)) { + ent->fl.clientEvents = true; //hackery to let normal projectiles reset their gravity for prediction + ent->PostEventMS( &EV_ResetGravity, 200 ); + ent->fl.clientEvents = false; + } + else { + ent->PostEventMS( &EV_ResetGravity, 200 ); + } +} + +void hhGravityZoneBase::EntityEncroaching( idEntity *ent ) { + // Cancel any pending gravity resets from other zones + ent->CancelEvents( &EV_ResetGravity ); + + idVec3 curGravity = GetCurrentGravity( ent->GetOrigin() ); + idVec3 otherGravity; + if (TouchingOtherZones(ent, false, otherGravity)) { //factor in gravity for all other zones being touched to avoid back-and-forth behaviour + float l = curGravity.Normalize(); + curGravity += otherGravity; + curGravity *= l*0.5f; + } + if (ent->GetPhysics()->IsAtRest() && ent->GetGravity() != curGravity) { + ent->SetGravity( curGravity ); + ent->GetPhysics()->Activate(); + } + else { + ent->SetGravity( curGravity ); + } + + if (ent->IsType( hhMonsterAI::Type )) { + if (bKillsMonsters && ent->health > 0 && + !static_cast(ent)->OverrideKilledByGravityZones() && + !ent->IsType(hhCrawler::Type) && + (idMath::Fabs(curGravity.x) > 0.01f || idMath::Fabs(curGravity.y) > 0.01f || curGravity.z >= 0.0f) && + static_cast(ent)->IsActive() ) { + + const char *monsterDamageType = spawnArgs.GetString("def_monsterdamage"); + ent->Damage(this, NULL, vec3_origin, monsterDamageType, 1.0f, 0); + } + } +} + + +//----------------------------------------------------------------------- +// +// hhGravityZone +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZoneBase, hhGravityZone) + EVENT( EV_SetGravityVector, hhGravityZone::Event_SetNewGravity ) +END_CLASS + +void hhGravityZone::Spawn(void) { + zeroGravOnChange = spawnArgs.GetBool("zeroGravOnChange"); + idVec3 startGravity( spawnArgs.GetVector("gravity") ); + interpolationTime = SEC2MS(spawnArgs.GetFloat("interpTime")); + gravityInterpolator.Init( gameLocal.time, 0, startGravity, startGravity ); + + if (startGravity != gameLocal.GetGravity()) { + if (!gameLocal.isMultiplayer) { //don't play sound in mp + StartSound("snd_gravity_loop_on", SND_CHANNEL_MISC1, 0, true); + } + } +} + +void hhGravityZone::Save(idSaveGame *savefile) const { + savefile->WriteFloat( gravityInterpolator.GetStartTime() ); // idInterpolate + savefile->WriteFloat( gravityInterpolator.GetDuration() ); + savefile->WriteVec3( gravityInterpolator.GetStartValue() ); + savefile->WriteVec3( gravityInterpolator.GetEndValue() ); + + savefile->WriteInt(interpolationTime); + savefile->WriteBool(zeroGravOnChange); +} + +void hhGravityZone::Restore( idRestoreGame *savefile ) { + float set; + idVec3 vec; + + savefile->ReadFloat( set ); // idInterpolate + gravityInterpolator.SetStartTime( set ); + savefile->ReadFloat( set ); + gravityInterpolator.SetDuration( set ); + savefile->ReadVec3( vec ); + gravityInterpolator.SetStartValue( vec ); + savefile->ReadVec3( vec ); + gravityInterpolator.SetEndValue( vec ); + + savefile->ReadInt(interpolationTime); + savefile->ReadBool(zeroGravOnChange); +} + +void hhGravityZone::Think() { + hhGravityZoneBase::Think(); + if (thinkFlags & TH_THINK) { + if (bShowVector) { + gameRenderWorld->DebugArrow(colorGreen, renderEntity.origin, renderEntity.origin + GetCurrentGravity(vec3_origin), 10); + } + } +} + +const idVec3 hhGravityZone::GetDestinationGravity() const { + return gravityInterpolator.GetEndValue(); +} + +const idVec3 hhGravityZone::GetCurrentGravity(const idVec3 &location) const { + return gravityInterpolator.GetCurrentValue( gameLocal.time ); +} + +void hhGravityZone::SetGravityOnZone( idVec3 &newGravity ) { + idVec3 startGrav; + + if (!gameLocal.isMultiplayer) { //don't play sound in mp + if ( newGravity.Compare(gameLocal.GetGravity(), VECTOR_EPSILON) ) { + StartSound("snd_gravity_off", SND_CHANNEL_ANY); + StopSound(SND_CHANNEL_MISC1, true); + StartSound("snd_gravity_loop_off", SND_CHANNEL_MISC1, 0, true); + } + else { + StartSound("snd_gravity_on", SND_CHANNEL_ANY); + StopSound(SND_CHANNEL_MISC1, true); + StartSound("snd_gravity_loop_on", SND_CHANNEL_MISC1, 0, true); + } + } + + if ( zeroGravOnChange ) { // nla + startGrav = vec3_origin; + } + else { + startGrav = GetCurrentGravity(vec3_origin); + } + // Interpolate to new gravity + gravityInterpolator.Init( gameLocal.time, interpolationTime, startGrav, newGravity ); +} + +//rww - network code +void hhGravityZone::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhGravityZoneBase::WriteToSnapshot(msg); + + msg.WriteFloat(gravityInterpolator.GetStartTime()); + msg.WriteFloat(gravityInterpolator.GetDuration()); + idVec3 vecStart = gravityInterpolator.GetStartValue(); + msg.WriteFloat(vecStart.x); + msg.WriteFloat(vecStart.y); + msg.WriteFloat(vecStart.z); + idVec3 vecEnd = gravityInterpolator.GetEndValue(); + msg.WriteDeltaFloat(vecStart.x, vecEnd.x); + msg.WriteDeltaFloat(vecStart.y, vecEnd.y); + msg.WriteDeltaFloat(vecStart.z, vecEnd.z); +} + +void hhGravityZone::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhGravityZoneBase::ReadFromSnapshot(msg); + gravityInterpolator.SetStartTime(msg.ReadFloat()); + gravityInterpolator.SetDuration(msg.ReadFloat()); + idVec3 vecStart; + vecStart.x = msg.ReadFloat(); + vecStart.y = msg.ReadFloat(); + vecStart.z = msg.ReadFloat(); + gravityInterpolator.SetStartValue(vecStart); + idVec3 vecEnd; + vecEnd.x = msg.ReadDeltaFloat(vecStart.x); + vecEnd.y = msg.ReadDeltaFloat(vecStart.y); + vecEnd.z = msg.ReadDeltaFloat(vecStart.z); + gravityInterpolator.SetEndValue(vecEnd); +} + +void hhGravityZone::ClientPredictionThink( void ) { + hhGravityZoneBase::ClientPredictionThink(); +} +//rww - end network code + +void hhGravityZone::Event_SetNewGravity( idVec3 &newGravity ) { + SetGravityOnZone( newGravity ); +} + + +//----------------------------------------------------------------------- +// +// hhGravityZoneInward +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZoneBase, hhGravityZoneInward) + EVENT( EV_SetGravityFactor, hhGravityZoneInward::Event_SetNewGravityFactor ) +END_CLASS + +void hhGravityZoneInward::Spawn(void) { + float startFactor = spawnArgs.GetFloat("factor", "50000"); + monsterGravityFactor = spawnArgs.GetFloat("monsterGravFactor", "1"); + interpolationTime = SEC2MS(spawnArgs.GetFloat("interpTime")); + factorInterpolator.Init( gameLocal.time, 0, startFactor, startFactor ); +} + +void hhGravityZoneInward::Save(idSaveGame *savefile) const { + savefile->WriteFloat( factorInterpolator.GetStartTime() ); // idInterpolate + savefile->WriteFloat( factorInterpolator.GetDuration() ); + savefile->WriteFloat( factorInterpolator.GetStartValue() ); + savefile->WriteFloat( factorInterpolator.GetEndValue() ); + + savefile->WriteInt(interpolationTime); + savefile->WriteFloat(monsterGravityFactor); +} + +void hhGravityZoneInward::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadFloat( set ); // idInterpolate + factorInterpolator.SetStartTime( set ); + savefile->ReadFloat( set ); + factorInterpolator.SetDuration( set ); + savefile->ReadFloat( set ); + factorInterpolator.SetStartValue(set); + savefile->ReadFloat( set ); + factorInterpolator.SetEndValue( set ); + + savefile->ReadInt(interpolationTime); + savefile->ReadFloat(monsterGravityFactor); +} + +void hhGravityZoneInward::EntityEntered(idEntity *ent) { + hhGravityZoneBase::EntityEntered(ent); + if ( ent && ent->IsType( hhMonsterAI::Type ) ) { + static_cast(ent)->GravClipModelAxis( true ); + } + // Disallow slope checking, it makes us stutter when walking on convex surfaces + + // aob - commented this because it allows the player to walk up vertical walls while in inward gravity zone + //Didn't see any studdering when thia was commented out. Do we still need it? + //if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + // static_cast(ent->GetPhysics())->SetSlopeCheck(false); + //} + //now done constantly while in a gravity zone + /* + if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + static_cast(ent->GetPhysics())->SetInwardGravity(1); + } + */ +} + +void hhGravityZoneInward::EntityLeaving(idEntity *ent) { + hhGravityZoneBase::EntityLeaving(ent); + if ( ent && ent->IsType( hhMonsterAI::Type ) ) { + static_cast(ent)->GravClipModelAxis( false ); + } + // Re-enable slope checking + + // aob - commented this because it allows the player to walk up vertical walls while in inward gravity zone + //Didn't see any studdering when thia was commented out. Do we still need it? + //if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + // static_cast(ent->GetPhysics())->SetSlopeCheck(true); + //} + if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + static_cast(ent->GetPhysics())->SetInwardGravity(0); + } +} + +// This is actually called each tick if for entities inside +void hhGravityZoneInward::EntityEncroaching( idEntity *ent ) { + + // Cancel any pending gravity resets from other zones + ent->CancelEvents( &EV_ResetGravity ); + + idVec3 curGravity = GetCurrentGravity( ent->GetOrigin() ); + idVec3 otherGravity; + if (TouchingOtherZones(ent, false, otherGravity)) { //factor in gravity for all other zones being touched to avoid back-and-forth behaviour + float l = curGravity.Normalize(); + curGravity += otherGravity; + curGravity *= l*0.5f; + } + if (ent->GetPhysics()->IsAtRest() && ent->GetGravity() != curGravity) { + ent->SetGravity( curGravity ); + ent->GetPhysics()->Activate(); + } + else { + ent->SetGravity( curGravity ); + } + if (ent->IsType( idAI::Type )) { + if (bKillsMonsters && ent->health > 0 && + !ent->IsType(hhCrawler::Type) && + (curGravity.x != 0.0f || curGravity.y != 0.0f || curGravity.z >= 0.0f) && + !static_cast(ent)->OverrideKilledByGravityZones() && + static_cast(ent)->IsActive() ) { + const char *monsterDamageType = spawnArgs.GetString("def_monsterdamage"); + ent->Damage(this, NULL, vec3_origin, monsterDamageType, 1.0f, 0); + } + } + //rww + else if (ent->IsType( hhPlayer::Type ) && ent->GetPhysics()->IsType(hhPhysics_Player::Type) ) { + static_cast(ent->GetPhysics())->SetInwardGravity(1); + } + + + if( ent->IsType(idAI::Type) && ent->health > 0 ) { + ent->GetPhysics()->SetGravity( ent->GetPhysics()->GetGravity() * monsterGravityFactor ); + ent->GetPhysics()->Activate(); + } + + if( bShowVector ) { + hhUtils::DebugCross( colorBlue, GetOrigin(), 100, 10 ); + idVec3 newGrav = GetCurrentGravity( ent->GetOrigin() ); + gameRenderWorld->DebugArrow( colorGreen, ent->GetRenderEntity()->origin, ent->GetRenderEntity()->origin + newGrav, 10 ); + } +} + +const idVec3 hhGravityZoneInward::GetCurrentGravity( const idVec3 &location ) const { + idVec3 grav; + idVec3 origin = GetGravityOrigin(); + float factor = factorInterpolator.GetCurrentValue( gameLocal.GetTime() ); + idVec3 inward = origin - location; + inward.Normalize(); + grav = inward * DEFAULT_GRAVITY * factor; + return grav; +} + +void hhGravityZoneInward::Event_SetNewGravityFactor( float newFactor ) { + // Interpolate to new gravity factor + float curFactor = factorInterpolator.GetCurrentValue( gameLocal.GetTime() ); + factorInterpolator.Init( gameLocal.GetTime(), interpolationTime, curFactor, newFactor ); +} + +//----------------------------------------------------------------------- +// +// hhAIWallwalkZone +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZone, hhAIWallwalkZone) +END_CLASS + +bool hhAIWallwalkZone::ValidEntity(idEntity *ent) { + // allow AI that isnt dead + return ent->IsType(idAI::Type) && ent->health > 0; +} + +void hhAIWallwalkZone::EntityEncroaching( idEntity *ent ) { + // Cancel any pending gravity resets from other zones + ent->CancelEvents( &EV_ResetGravity ); + + trace_t TraceInfo; + gameLocal.clip.TracePoint(TraceInfo, ent->GetOrigin(), ent->GetOrigin() + (idVec3(0,0,-300)*ent->GetRenderEntity()->axis), ent->GetPhysics()->GetClipMask(), ent); + if( TraceInfo.fraction < 1.0f ) { // && ent->health > 0 ) { + ent->SetGravity( -TraceInfo.c.normal ); + ent->GetPhysics()->Activate(); + } +} + +//----------------------------------------------------------------------- +// +// hhGravityZoneSinkhole +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhGravityZoneInward, hhGravityZoneSinkhole) + EVENT( EV_SetGravityFactor, hhGravityZoneSinkhole::Event_SetNewGravityFactor ) +END_CLASS + +void hhGravityZoneSinkhole::Spawn(void) { + bReorient = false; + maxMagnitude = spawnArgs.GetFloat("maxMagnitude", "10000"); + minMagnitude = spawnArgs.GetFloat("minMagnitude", "0"); +} + +void hhGravityZoneSinkhole::Save(idSaveGame *savefile) const { + savefile->WriteFloat(maxMagnitude); + savefile->WriteFloat(minMagnitude); +} + +void hhGravityZoneSinkhole::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat(maxMagnitude); + savefile->ReadFloat(minMagnitude); +} + +// Still have this in case we want to do something mass based +const idVec3 hhGravityZoneSinkhole::GetCurrentGravityEntity(const idEntity *ent) const { + idVec3 grav = vec3_origin; + if (ent) { + // precalc mass product / G as a constant and expose that as the fudge factor + idVec3 origin = GetGravityOrigin(); + float factor = factorInterpolator.GetCurrentValue( gameLocal.time ); + idVec3 inward = origin - ent->GetOrigin(); + float distance = inward.Normalize(); + float distanceSquared = distance*distance; + + // Some different gravitational fields + // float gravMag = (mass * ent->GetPhysics()->GetMass() * GRAVITATIONAL_CONSTANT) / distanceSquared; + // float gravMag = factor*factor / distanceSquared; // Inverse squared distance + // float gravMag = factor / sqrt(distance); // Inverse sqrt distance + // float gravMag = factor * sqrt(distance); // sqrt distance + float gravMag = factor*factor / 2 + 0.2f * distanceSquared; // Inverse squared distance + //gameLocal.Printf("factor=%.0f gravity magnitude=%.2f\n", factor, gravMag); + + gravMag = hhMath::ClampFloat(minMagnitude, maxMagnitude, gravMag); // This will cut off extremely large forces + grav = inward * gravMag; + } + return grav; +} + +const idVec3 hhGravityZoneSinkhole::GetCurrentGravity(const idVec3 &location) const { + idVec3 grav; + + // precalc mass product / G as a constant and expose that as the fudge factor + idVec3 origin = GetGravityOrigin(); + float factor = factorInterpolator.GetCurrentValue( gameLocal.time ); + idVec3 inward = origin - location; + float distance = inward.Normalize(); + float distanceSquared = distance*distance; + + // Some different gravitational fields + float gravMag = factor*factor / 2 + 0.2f * distanceSquared; // Inverse squared distance + gravMag = hhMath::ClampFloat(minMagnitude, maxMagnitude, gravMag); // This will cut off extremely large forces + grav = inward * gravMag; + return grav; +} + +void hhGravityZoneSinkhole::Event_SetNewGravityFactor( float newFactor ) { + // Interpolate to new gravity factor + float curFactor = factorInterpolator.GetCurrentValue(gameLocal.time); + factorInterpolator.Init( gameLocal.time, interpolationTime, curFactor, newFactor ); +} + + +//----------------------------------------------------------------------- +// +// hhVelocityZone +// +//----------------------------------------------------------------------- + +const idEventDef EV_SetVelocityVector("setvelocity", "v"); + +CLASS_DECLARATION(hhZone, hhVelocityZone) + EVENT( EV_SetVelocityVector, hhVelocityZone::Event_SetNewVelocity ) +END_CLASS + +void hhVelocityZone::Spawn(void) { + bReorient = spawnArgs.GetBool("reorient"); + interpolationTime = SEC2MS(spawnArgs.GetFloat("interpTime")); + idVec3 startVelocity = spawnArgs.GetVector("velocity"); + //slop = 25.0f; // we use a slightly larger bounds to catch things that are rotated by bReorient + bShowVector = spawnArgs.GetBool("showVector"); + bKillsMonsters = spawnArgs.GetBool("killmonsters"); + + velocityInterpolator.Init(gameLocal.time, 0, startVelocity, startVelocity); +} + +void hhVelocityZone::Save(idSaveGame *savefile) const { + savefile->WriteFloat( velocityInterpolator.GetStartTime() ); // idInterpolate + savefile->WriteFloat( velocityInterpolator.GetDuration() ); + savefile->WriteVec3( velocityInterpolator.GetStartValue() ); + savefile->WriteVec3( velocityInterpolator.GetEndValue() ); + + savefile->WriteBool(bKillsMonsters); + savefile->WriteBool(bReorient); + savefile->WriteBool(bShowVector); + savefile->WriteInt(interpolationTime); +} + +void hhVelocityZone::Restore( idRestoreGame *savefile ) { + float set; + idVec3 vec; + + savefile->ReadFloat( set ); // idInterpolate + velocityInterpolator.SetStartTime( set ); + savefile->ReadFloat( set ); + velocityInterpolator.SetDuration( set ); + savefile->ReadVec3( vec ); + velocityInterpolator.SetStartValue( vec ); + savefile->ReadVec3( vec ); + velocityInterpolator.SetEndValue( vec ); + + savefile->ReadBool(bKillsMonsters); + savefile->ReadBool(bReorient); + savefile->ReadBool(bShowVector); + savefile->ReadInt(interpolationTime); +} + +void hhVelocityZone::EntityLeaving(idEntity *ent) { + ent->GetPhysics()->SetLinearVelocity(idVec3(0, 0, 0)); + if( ent->RespondsTo(EV_OrientToGravity) ) { + ent->ProcessEvent( &EV_OrientToGravity, (int)bReorient ); + } +} + +void hhVelocityZone::EntityEncroaching(idEntity *ent) { + idVec3 baseVelocity = velocityInterpolator.GetCurrentValue(gameLocal.time); + idVec3 baseVelocityDirection = baseVelocity; + baseVelocityDirection.Normalize(); + + idVec3 curVelocity; + curVelocity = ent->GetPhysics()->GetLinearVelocity(); + curVelocity.ProjectOntoPlane(baseVelocityDirection); + + ent->GetPhysics()->SetLinearVelocity( curVelocity + baseVelocity ); + if( ent->RespondsTo(EV_OrientToGravity) ) { + ent->ProcessEvent( &EV_OrientToGravity, (int)bReorient ); + } + else if (ent->IsType( idAI::Type )) { + if (bKillsMonsters && ent->health > 0 && + !static_cast(ent)->IsFlying()) { + const char *monsterDamageType = spawnArgs.GetString("def_monsterdamage"); + ent->Damage(this, NULL, vec3_origin, monsterDamageType, 1.0f, 0); + } + } +} + +void hhVelocityZone::Think() { + hhZone::Think(); + if (thinkFlags & TH_THINK) { + if (bShowVector) { + idVec3 baseVelocity = velocityInterpolator.GetCurrentValue(gameLocal.time); + gameRenderWorld->DebugArrow(colorGreen, renderEntity.origin, renderEntity.origin+baseVelocity, 10); + } + } +} + +void hhVelocityZone::Event_SetNewVelocity( idVec3 &newVelocity ) { + // Interpolate to new velocity + idVec3 currentVelocity = velocityInterpolator.GetCurrentValue(gameLocal.time); + velocityInterpolator.Init(gameLocal.time, interpolationTime, currentVelocity, newVelocity); +} + + +//----------------------------------------------------------------------- +// +// hhShuttleRecharge +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhShuttleRecharge) +END_CLASS + +void hhShuttleRecharge::Spawn(void) { + amountHealth = spawnArgs.GetInt("amounthealth"); + amountPower = spawnArgs.GetInt("amountpower"); +} + +void hhShuttleRecharge::Save(idSaveGame *savefile) const { + savefile->WriteInt(amountHealth); + savefile->WriteInt(amountPower); +} + +void hhShuttleRecharge::Restore( idRestoreGame *savefile ) { + savefile->ReadInt(amountHealth); + savefile->ReadInt(amountPower); +} + +bool hhShuttleRecharge::ValidEntity(idEntity *ent) { + return ent && ent->IsType(hhShuttle::Type); +} + +void hhShuttleRecharge::EntityEntered(idEntity *ent) { + //static_cast(ent)->SetRecharging(true); +} + +void hhShuttleRecharge::EntityLeaving(idEntity *ent) { + //static_cast(ent)->SetRecharging(false); +} + +void hhShuttleRecharge::EntityEncroaching(idEntity *ent) { + if (ent->IsType(hhVehicle::Type)) { + hhVehicle *vehicle = static_cast(ent); + + //HUMANHEAD bjk PCF (4-27-06) - shuttle recharge was slow + if(USERCMD_HZ == 30) { + vehicle->GiveHealth(2*amountHealth); + vehicle->GivePower(2*amountPower); + } else { + vehicle->GiveHealth(amountHealth); + vehicle->GivePower(amountPower); + } + } +} + + +//----------------------------------------------------------------------- +// +// hhDockingZone +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhDockingZone) +END_CLASS + +void hhDockingZone::Spawn(void) { + dock = NULL; + triggerBehavior = TB_PLAYER_MONSTERS_FRIENDLIES; // Allow all actors to trigger it + + fl.networkSync = true; +} + +void hhDockingZone::Save(idSaveGame *savefile) const { + dock.Save(savefile); +} + +void hhDockingZone::Restore( idRestoreGame *savefile ) { + dock.Restore(savefile); +} + +void hhDockingZone::RegisterDock(hhDock *d) { + dock = d; +} + +bool hhDockingZone::ValidEntity(idEntity *ent) { + if (ent) { + if (dock.IsValid() && dock->ValidEntity(ent)) { + return true; + } + if (ent->IsType(idActor::Type)) { //FIXME: Is this causing the shuttleCount to go wrong + return hhShuttle::ValidPilot(static_cast(ent)); + } + } + return false; +} + +void hhDockingZone::EntityEncroaching(idEntity *ent) { + if (dock.IsValid()) { + dock->EntityEncroaching(ent); + } +} + +void hhDockingZone::EntityEntered(idEntity *ent) { + if (dock.IsValid()) { + dock->EntityEntered(ent); + } +} + +void hhDockingZone::EntityLeaving(idEntity *ent) { + if (dock.IsValid()) { + dock->EntityLeaving(ent); + } +} + +void hhDockingZone::WriteToSnapshot( idBitMsgDelta &msg ) const { + GetPhysics()->WriteToSnapshot(msg); + msg.WriteBits(dock.GetSpawnId(), 32); +} + +void hhDockingZone::ReadFromSnapshot( const idBitMsgDelta &msg ) { + GetPhysics()->ReadFromSnapshot(msg); + dock.SetSpawnId(msg.ReadBits(32)); +} + +void hhDockingZone::ClientPredictionThink( void ) { + if (!gameLocal.isNewFrame) { + return; + } + Think(); +} + +//----------------------------------------------------------------------- +// +// hhShuttleDisconnect +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhShuttleDisconnect) +END_CLASS + +void hhShuttleDisconnect::Spawn(void) { +} + +bool hhShuttleDisconnect::ValidEntity(idEntity *ent) { + return ent && ent->IsType(hhShuttle::Type); +} + +void hhShuttleDisconnect::EntityEntered(idEntity *ent) { + static_cast(ent)->AllowTractor(false); +} + +void hhShuttleDisconnect::EntityEncroaching(idEntity *ent) { +} + +void hhShuttleDisconnect::EntityLeaving(idEntity *ent) { + static_cast(ent)->AllowTractor(true); +} + + +//----------------------------------------------------------------------- +// +// hhShuttleSlingshot +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhShuttleSlingshot) +END_CLASS + +void hhShuttleSlingshot::Spawn(void) { +} + +bool hhShuttleSlingshot::ValidEntity(idEntity *ent) { + return ent && ent->IsType(hhShuttle::Type); +} + +void hhShuttleSlingshot::EntityEntered(idEntity *ent) { +} + +void hhShuttleSlingshot::EntityEncroaching(idEntity *ent) { + float factor = spawnArgs.GetFloat("BoostFactor"); + + hhShuttle *shuttle = static_cast(ent); + shuttle->ApplyBoost( 255.0f * factor); + + // CJR: Alter the player's view when zooming through a slingshot zone + shuttle->GetPilot()->PostEventMS( &EV_SetOverlayMaterial, 0, spawnArgs.GetString( "mtr_speedView" ), -1, false ); + +} + +void hhShuttleSlingshot::EntityLeaving(idEntity *ent) { + // CJR: Reset the player's view after zooming through a slingshot zone + hhShuttle *shuttle = static_cast(ent); + shuttle->GetPilot()->PostEventMS( &EV_SetOverlayMaterial, 0, "", -1, false ); +} + + + +//----------------------------------------------------------------------- +// +// hhRemovalVolume +// +//----------------------------------------------------------------------- + +CLASS_DECLARATION(hhZone, hhRemovalVolume) +END_CLASS + +void hhRemovalVolume::Spawn(void) { +} + +bool hhRemovalVolume::ValidEntity(idEntity *ent) { + return ent && ( + ent->IsType(idMoveable::Type) || + ent->IsType(idItem::Type) || + ent->IsType(hhAFEntity::Type) || + ent->IsType(hhAFEntity_WithAttachedHead::Type) || + (ent->IsType(hhMonsterAI::Type) && ent->health<=0 && !ent->fl.isTractored) ); +} + +void hhRemovalVolume::EntityEntered(idEntity *ent) { + ent->PostEventMS(&EV_Remove, 0); +} + +void hhRemovalVolume::EntityEncroaching(idEntity *ent) { +} + +void hhRemovalVolume::EntityLeaving(idEntity *ent) { +} diff --git a/src/Prey/game_zone.h b/src/Prey/game_zone.h new file mode 100644 index 0000000..aa2e7e3 --- /dev/null +++ b/src/Prey/game_zone.h @@ -0,0 +1,266 @@ + +#ifndef __GAME_GRAVITYZONE_H__ +#define __GAME_GRAVITYZONE_H__ + +class hhDock; + +class hhZone : public hhTrigger { +public: + ABSTRACT_PROTOTYPE( hhZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void Think( void ); + virtual void Present( void ) { } // HUMANHEAD mdl: Not used by zones + + // hhTrigger interface + void TriggerAction(idEntity *activator); + + // hhZone interface + void ResetZoneList(); + void ApplyToEncroachers(); + bool ContainsEntityOfType(const idTypeInfo &t); + virtual void EntityEntered(idEntity *ent) {} + virtual void EntityLeaving(idEntity *ent) {} + virtual void EntityEncroaching(idEntity *ent) {} + virtual bool ValidEntity(idEntity *ent); + virtual void Empty(); + +protected: + void Event_TurnOff(); + void Event_Enable( void ); + void Event_Disable( void ); + void Event_Touch( idEntity *other, trace_t *trace ); + +protected: + idList zoneList; // List of valid entities in zone last frame + float slop; +}; + +class hhTriggerZone : public hhZone { +public: + CLASS_PROTOTYPE( hhTriggerZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + + hhFuncParmAccessor funcRefInfo; +}; + +class hhGravityZoneBase : public hhZone { +public: + ABSTRACT_PROTOTYPE( hhGravityZoneBase ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual const idVec3 GetGravityOrigin() const; + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const = 0; + + virtual bool TouchingOtherZones(idEntity *ent, bool traceCheck, idVec3 &otherInfluence); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +protected: + bool bReorient; + bool bKillsMonsters; + bool bShowVector; + idVec3 gravityOriginOffset; //rww - avoid dictionary lookup +}; + +class hhGravityZone : public hhGravityZoneBase { +public: + CLASS_PROTOTYPE( hhGravityZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Think( void ); + virtual const idVec3 GetDestinationGravity() const; + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const; + virtual void SetGravityOnZone( idVec3 &newGravity ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +protected: + void Event_SetNewGravity( idVec3 &newgrav ); + +protected: + idInterpolate gravityInterpolator; + int interpolationTime; + bool zeroGravOnChange; +}; + +class hhAIWallwalkZone : public hhGravityZone { +public: + CLASS_PROTOTYPE( hhAIWallwalkZone ); + virtual void EntityEncroaching(idEntity *ent); + virtual bool ValidEntity(idEntity *ent); +}; + +class hhGravityZoneInward : public hhGravityZoneBase { +public: + CLASS_PROTOTYPE( hhGravityZoneInward ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void EntityEntered(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const; + +protected: + virtual void Event_SetNewGravityFactor( float newFactor ); + +protected: + idInterpolate factorInterpolator; + int interpolationTime; + float monsterGravityFactor; +}; + + +#define GRAVITATIONAL_CONSTANT 1.03416206832413664e-7f + +class hhGravityZoneSinkhole : public hhGravityZoneInward { +public: + CLASS_PROTOTYPE( hhGravityZoneSinkhole ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual const idVec3 GetCurrentGravity(const idVec3 &location) const; + const idVec3 GetCurrentGravityEntity(const idEntity *ent) const; + +protected: + void Event_SetNewGravityFactor( float newFactor ); + +protected: + float maxMagnitude; + float minMagnitude; +}; + + +class hhVelocityZone : public hhZone { +public: + CLASS_PROTOTYPE( hhVelocityZone ); + + void Spawn( void ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Think( void ); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: + void Event_SetNewVelocity( idVec3 &newvel ); + +protected: + idInterpolate velocityInterpolator; + bool bKillsMonsters; + bool bReorient; + bool bShowVector; + int interpolationTime; +}; + +class hhShuttleRecharge : public hhZone { +public: + CLASS_PROTOTYPE( hhShuttleRecharge ); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + +protected: + int amountHealth; + int amountPower; +}; + +class hhDockingZone : public hhZone { +public: + CLASS_PROTOTYPE( hhDockingZone ); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + void RegisterDock(hhDock *d); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +protected: + idEntityPtr dock; +}; + +class hhShuttleDisconnect : public hhZone { +public: + CLASS_PROTOTYPE( hhShuttleDisconnect ); + + void Spawn(void); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: +}; + +class hhShuttleSlingshot : public hhZone { +public: + CLASS_PROTOTYPE( hhShuttleSlingshot ); + + void Spawn(void); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: +}; + +class hhRemovalVolume : public hhZone { +public: + CLASS_PROTOTYPE( hhRemovalVolume ); + + void Spawn(void); + virtual bool ValidEntity(idEntity *ent); + virtual void EntityEntered(idEntity *ent); + virtual void EntityEncroaching(idEntity *ent); + virtual void EntityLeaving(idEntity *ent); + +protected: +}; + +#endif /* __GAME_GRAVITYZONE_H__ */ diff --git a/src/Prey/particles_particles.cpp b/src/Prey/particles_particles.cpp new file mode 100644 index 0000000..b699f36 --- /dev/null +++ b/src/Prey/particles_particles.cpp @@ -0,0 +1,368 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idVec3 hhSmokeParticles::defaultDir( 0.0f, 0.0f, 1.0f ); + +/* +================ +hhSmokeParticles::hhSmokeParticles +================ +*/ +hhSmokeParticles::hhSmokeParticles() { +} + +#if 0 +/* +================ +hhParticleSystem::RunTrailStage +================ +*/ +void hhParticleSystem::RunTrailStage( const int ev ) { + if (GetParticle()) { + idFXSmokeStage *stage = particleSystem->events[ev]; + float fraction; + //HUMANHEAD: aob + int numParticleToTrigger = 0; + idVec3 gravity; + //HUMANHEAD END + + for( int i=0; ihidden && current && current->state != SMOKE_DEAD ) { + // do we need to insert another particle? + if (current->nozzle && current->triggered != current->numToTrigger) { + //HUMANHEAD: aob - moved logic to helper function + ResetSmokeParticleTrail( ¤t->particles[current->triggered], current, stage ); + //HUMANHEAD END + current->triggered++; + } + // move them all, kill if we need to + bool triggerAlive = false; + for( int j=0; jtriggered; j++ ) { + if ( current->particles[j].age <= stage->timeToFade ) { + smokeParticle *sParticle = ¤t->particles[j]; + idVec3 speed = vec3_origin; // HUMANHEAD JRM - changed to vec3 + if (sParticle->age >= stage->timeToFinalSpeed) { + speed = stage->finalSpeed; + } else { + fraction = sParticle->age * stage->oneOverTimeToFinalSpeed; + speed = stage->initialSpeed + (stage->finalSpeed - stage->initialSpeed) * fraction; + } + //HUMANHEAD: aob + if (sParticle->age >= stage->timeToFinalGravity) { + gravity = stage->finalGravity; + } else { + fraction = sParticle->age * stage->oneOverTimeToFinalGravity; + gravity = stage->initialGravity + (stage->finalGravity - stage->initialGravity) * fraction; + } + //HUMANHEAD END + if (sParticle->age != 0) { + if (stage->slowSpawnSpeedOverLife) { + fraction = 1.0f - ((float)j / current->numToTrigger); + //HUMANHEAD: aob + sParticle->position += DetermineDeltaPos( speed * fraction, sParticle->xyzmove, sParticle->direction ); + //HUMANHEAD END + } else { + //HUMANHEAD: aob + sParticle->position += DetermineDeltaPos( speed, sParticle->xyzmove, sParticle->direction ); + //HUMANHEAD END + } + //HUMANHEAD: aob + sParticle->position += gravity; + //HUMANHEAD END + } + sParticle->age += gameLocal.msec; + triggerAlive = true; + if ( (sParticle->age+current->baseTime) >= stage->timeToFade ) { + if (current->nozzle && stage->continuous ) { + //HUMANHEAD: aob - moved logic to helper function + ResetSmokeParticleTrail( sParticle, current, stage ); + //HUMANHEAD END + } + } + } + } + if (!triggerAlive) { + current->state = SMOKE_DEAD; + } + } + } + } +} + +/* +================ +hhParticleSystem::ResetSmokeParticleTrail +================ +*/ +void hhParticleSystem::ResetSmokeParticleTrail( smokeParticle* particle, smokeGroup* group, idFXSmokeStage* smokeStage ) { + float ang = 0.0f; + float size = 0.0f; + + particle->position = group->groupOrigin; + particle->direction = group->groupDirection; + // HUMANHEAD JRM - need to make spawn direction spherical - NOT box + ang = gameLocal.random.RandomFloat() * hhMath::TWO_PI; + size = smokeStage->xymove; + if( !smokeStage->noXYMoveRandomness ) { + size *= gameLocal.random.RandomFloat(); + } + particle->xyzmove = idVec3( 0.0f, hhMath::Cos(ang) * size, hhMath::Sin(ang) * size ); + particle->xyzmove *= particle->direction.ToMat3(); + // HUMANHEAD JRM - end + particle->age = 0; +} + +/* +================ +hhParticleSystem::RunSmokeStage +================ +*/ +void hhParticleSystem::RunSmokeStage( const int ev ) { + if (GetParticle()) { + idFXSmokeStage *stage = particleSystem->events[ev]; + if (stage->trails) { + return RunTrailStage( ev ); + } + float fraction; + //HUMANHEAD: aob + int numParticleToTrigger = 0; + idVec3 gravity; + //HUMANHEAD END + for( int i=0; ihidden && current->state != SMOKE_DEAD ) { + // do we need to insert another particle? + // HUMANHEAD JRM - made a while and seperated if + if (current->nozzle && current->frameCount >= current->triggerEvery && current->triggered < current->numToTrigger) { + //HUMANHEAD: aob + numParticleToTrigger = current->minToTrigger + gameLocal.random.RandomInt( current->maxToTrigger - current->minToTrigger ); + //HUMANHEAD END + while (numParticleToTrigger && current->triggered < current->numToTrigger) {//HUMANHEAD: aob - changed numToTrigger to numParticlesToTrigger + //HUMANHEAD: aob - moved logic to helper function + ResetSmokeParticle( ¤t->particles[current->triggered], current, stage ); + current->triggered++; + //HUMANHEAD: aob + --numParticleToTrigger; + //HUMANHEAD END + } + current->triggerEvery = stage->triggerEvery - gameLocal.random.RandomInt( stage->triggerEvery - stage->minTriggerEvery ); + current->frameCount = 0; + } + current->frameCount++; + // move them all, kill if we need to + idVec3 mwpVector; + mwpVector.Zero(); + if (stage->moveWithParent && current->groupOrigin != current->oldGroupOrigin) { + mwpVector = current->groupOrigin - current->oldGroupOrigin; + current->oldGroupOrigin = current->groupOrigin; + } + bool triggerAlive = false; + for( int j=0; jtriggered; j++ ) { + if ( current->particles[j].age <= stage->timeToFade ) { + smokeParticle *sParticle = ¤t->particles[j]; + sParticle->age += gameLocal.msec; + sParticle->rotation += (sParticle->rotationSpeed * stage->rotationMul); + idVec3 speed; // HUMANHEAD JRM - changed to vec3 from float + if (sParticle->age >= stage->timeToFinalSpeed) { + speed = stage->finalSpeed; + } else { + fraction = sParticle->age * stage->oneOverTimeToFinalSpeed; + speed = stage->initialSpeed + (stage->finalSpeed - stage->initialSpeed) * fraction; + } + //HUMANHEAD: aob + if (sParticle->age >= stage->timeToFinalGravity) { + gravity = stage->finalGravity; + } else { + fraction = sParticle->age * stage->oneOverTimeToFinalGravity; + gravity = stage->initialGravity + (stage->finalGravity - stage->initialGravity) * fraction; + } + //HUMANHEAD END + if (sParticle->age != 0) { + if (stage->slowSpawnSpeedOverLife) { + fraction = 1.0f - ((float)j / current->numToTrigger); + //HUMANHEAD: aob + sParticle->position += DetermineDeltaPos( speed * fraction, sParticle->xyzmove, sParticle->direction ); + //HUMANHEAD END + } else { + //HUMANHEAD: aob + sParticle->position += DetermineDeltaPos( speed, sParticle->xyzmove, sParticle->direction ); + //HUMANHEAD END + } + //HUMANHEAD: aob + sParticle->position += gravity; + //HUMANHEAD END + } + sParticle->position += mwpVector; + triggerAlive = true; + if ( sParticle->age >= stage->timeToFade ) { + if( current->nozzle && stage->continuous ) { + //HUMANHEAD: aob - moved logic into helper function + ResetSmokeParticle( sParticle, current, stage ); + //HUMANHEAD END + } + } + } + } + if (!triggerAlive) { + current->state = SMOKE_DEAD; + } + } + } + } +} + +/* +================ +hhParticleSystem::ResetSmokeParticle +================ +*/ +void hhParticleSystem::ResetSmokeParticle( smokeParticle* particle, smokeGroup* group, idFXSmokeStage* smokeStage ) { + float ang = 0.0f; + float size = 0.0f; + + particle->position = (smokeStage->moveWithParent) ? group->oldGroupOrigin : group->groupOrigin; + particle->direction = group->groupDirection; + particle->rotation = gameLocal.random.RandomInt(360); + particle->rotationSpeed = gameLocal.random.CRandomFloat() * 3.0f; + // HUMANHEAD JRM - need to make spawn direction spherical - NOT box + ang = gameLocal.random.RandomFloat() * hhMath::TWO_PI; + size = smokeStage->xymove; + if( !smokeStage->noXYMoveRandomness ) { + size *= gameLocal.random.RandomFloat(); + } + particle->xyzmove = idVec3( 0.0f, hhMath::Cos(ang) * size, hhMath::Sin(ang) * size ); + particle->xyzmove *= particle->direction.ToMat3(); + // HUMANHEAD JRM - end + particle->age = 0; +} + +/* +================ +hhParticleSystem::DetermineDeltaPos +================ +*/ +idVec3 hhParticleSystem::DetermineDeltaPos( const idVec3& speed, const idVec3& xyzmove, const idVec3& dir ) { + idVec3 deltaPos; + //Convert to world coords + idVec3 velocity = (dir + xyzmove) * dir.ToMat3().Transpose(); + + for( int ix = 0; ix < 3; ++ix ) { + deltaPos[ix] = speed[ix] * velocity[ix]; + } + + //Convert back to local coords + return deltaPos * dir.ToMat3(); +} + +/* +================ +hhParticleSystem::ReTrigger +================ +*/ +const int hhParticleSystem::ReTrigger( const idVec3& newOrigin, const idVec3& newDirection ) { + + // all triggers need to be checked, but all triggers have the same Num() <-- optimize + for( int i = 0; i < triggers[0].Num(); i++ ) { + if ( StageFinished(i) ) { + ResetStage(i, newOrigin, newDirection ); + return (myIndex<<16)|i; + } + } + + smokeParticle initial; + initial.position = vec3_origin; + initial.rotation = 0.0f; + initial.rotationSpeed = 0.0f; + initial.direction = vec3_origin; + //HUMANHEAD: aob + initial.xyzmove.Zero(); + //HUMANHEAD END + + GetParticle(); + for( int i=0; ievents.Num(); i++ ) { + smokeGroup* newGroup = new smokeGroup; + + newGroup->state = SMOKE_ALIVE; + //HUMANHEAD: aob + newGroup->triggered = 0; + newGroup->minToTrigger = particleSystem->events[i]->minToTrigger; + newGroup->maxToTrigger = particleSystem->events[i]->maxToTrigger; + newGroup->numToTrigger = (particleSystem->events[i]->randomNumToTrigger) ? newGroup->minToTrigger + gameLocal.random.RandomInt(newGroup->maxToTrigger - newGroup->minToTrigger) : particleSystem->events[i]->numToTrigger; + newGroup->particles.AssureSize( newGroup->numToTrigger, initial ); + //HUMANHEAD END + newGroup->triggerEvery = particleSystem->events[i]->triggerEvery - gameLocal.random.RandomInt( particleSystem->events[i]->triggerEvery - particleSystem->events[i]->minTriggerEvery ); + newGroup->frameCount = newGroup->triggerEvery; + //HUMANHEAD: aob + newGroup->groupDirection = (newDirection == vec3_origin) ? defaultDir : newDirection; + //HUMANHEAD END + newGroup->groupOrigin = newOrigin; + newGroup->oldGroupOrigin = newOrigin; + newGroup->nozzle = true; + newGroup->hidden = false; + newGroup->baseTime = 0; + triggers[i].Append( newGroup ); + } + return (myIndex<<16)|(triggers[0].Num()-1); +} + +/* +================ +hhParticleSystem::ResetStage +================ +*/ +void hhParticleSystem::ResetStage( const int st, const idVec3 &start, const idVec3& direction ) { + GetParticle(); + + //HUMANHEAD: aob + smokeParticle initial; + initial.position = vec3_origin; + initial.rotation = 0.0f; + initial.rotationSpeed = 0.0f; + initial.direction = vec3_origin; + initial.xyzmove.Zero(); + //HUMANHEAD END + + for( int i=0; ievents.Num(); i++ ) { + smokeGroup* current = triggers[i][st]; + current->state = SMOKE_ALIVE; + current->triggered = 0; + //HUMANHEAD: aob + current->minToTrigger = particleSystem->events[i]->minToTrigger; + current->maxToTrigger = particleSystem->events[i]->maxToTrigger; + current->numToTrigger = (particleSystem->events[i]->randomNumToTrigger) ? current->minToTrigger + gameLocal.random.RandomInt(current->maxToTrigger - current->minToTrigger) : particleSystem->events[i]->numToTrigger; + current->particles.AssureSize( current->numToTrigger, initial ); + //HUMANHEAD END + current->triggerEvery = particleSystem->events[i]->triggerEvery - gameLocal.random.RandomInt( particleSystem->events[i]->triggerEvery - particleSystem->events[i]->minTriggerEvery ); + current->frameCount = current->triggerEvery; + current->groupOrigin = start; + current->oldGroupOrigin = start; + //HUMANHEAD: aob + current->groupDirection = (direction == vec3_origin) ? defaultDir : direction; + //HUMANHEAD END + current->nozzle = true; + current->hidden = false; + current->baseTime = 0; + } +} + +/* +================ +hhParticleSystem::SetNozzleOrigin +================ +*/ +void hhParticleSystem::SetNozzleDirection( const int handle, const idVec3 &vec ) { + int st = TriggerForHandle( handle ); + GetParticle(); + for( int i=0; ievents.Num(); i++ ) { + smokeGroup* current = triggers[i][st]; + //HUMANHEAD: aob + current->groupDirection = (vec == vec3_origin) ? defaultDir : vec; + //HUMANHEAD END + } +} +#endif \ No newline at end of file diff --git a/src/Prey/particles_particles.h b/src/Prey/particles_particles.h new file mode 100644 index 0000000..fbf100d --- /dev/null +++ b/src/Prey/particles_particles.h @@ -0,0 +1,13 @@ +// COMMENTED OUT +#ifndef __PREY_PARTICLES_PARTICLES__ +#define __PREY_PARTICLES_PARTICLES__ + +class hhSmokeParticles : public idSmokeParticles { + public: + hhSmokeParticles(); + + protected: + static const idVec3 defaultDir; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/physics_delta.cpp b/src/Prey/physics_delta.cpp new file mode 100644 index 0000000..f6e5a89 --- /dev/null +++ b/src/Prey/physics_delta.cpp @@ -0,0 +1,155 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idPhysics_Actor, hhPhysics_Delta ) +END_CLASS + +//============ +// hhPhysics_Delta::hhPhysics_Delta +//============ +hhPhysics_Delta::hhPhysics_Delta() { + + delta.Zero(); +} //. hhPhysics_Delta::hhPhysics_Delta() + + +//============ +// hhPhysics_Delta::Evaluate +//============ +bool hhPhysics_Delta::Evaluate( int timeStepMSec, int endTimeMSec ) { + trace_t trace; + idVec3 dest; + idRotation rotation; + float timeStep; + idVec3 velocity; + + + timeStep = MS2SEC( timeStepMSec ); + velocity = delta / timeStep; + + if ( delta == vec3_zero ) { + Rest(); + return( false ); + } + + clipModel->Unlink(); // Taken from monster + + dest = GetOrigin() + delta; + rotation.SetOrigin( GetOrigin() ); + rotation.SetAngle( 0 ); + + //? Maybe model after the idPhysics_Monster::Slide move, where we + // slide along scholng + + // If there was a collision, adjust the dest + if ( gameLocal.clip.Motion( trace, GetOrigin(), dest, rotation, clipModel, + GetAxis(), clipMask, self ) ) { + if ( self->AllowCollision( trace ) ) { + //gameLocal.Printf( "Hit something %.2f\n", trace.fraction ); + + dest = trace.endpos; + + //? Move this elsewhere? + self->Collide( trace, velocity ); + } //. We run into what we hit = ) + + //? Maybe we apply an impulse? + } //. We hit something + + //gameLocal.Printf( "Moving from %s to %s (%s)\n", GetOrigin().ToString(), + // dest.ToString(), delta.ToString() ); + + + //! What about origin and axis? + clipModel->Link( gameLocal.clip, self, 0, dest, GetAxis() ); // Taken from monster + + // We are done with delta, so zero it out + delta.Zero(); + + return( true ); +} //. hhPhysics_Delta::Evaluate( int, int ); + + +//============ +// hhPhysics_Delta::SetDelta +//============ +void hhPhysics_Delta::SetDelta( const idVec3 &d ){ + + delta = d; + + if ( delta != vec3_origin ) { + Activate(); + } +} //. hhPhysics_Delta::SetDelta( const idVec3 & ) + + +//============ +// hhPhysics_Delta::Activate +//============ +void hhPhysics_Delta::Activate(){ + + self->BecomeActive( TH_PHYSICS ); + +} //. hhPhysics_Delta::Activate( const idVec3 & ) + + +//============ +// hhPhysics_Delta::Rest +//============ +void hhPhysics_Delta::Rest(){ + + self->BecomeInactive( TH_PHYSICS ); + +} //. hhPhysics_Delta::Rest( const idVec3 & ) + + + +//================ +//hhPhysics_Delta::SetAxis +//================ +void hhPhysics_Delta::SetAxis( const idMat3 &newAxis, int id ) { + // Ripped from idPhysics_Monster + clipModel->Link( gameLocal.clip, self, 0, clipModel->GetOrigin(), newAxis ); + //? Activate(); +} + + +//================ +//hhPhysics_Delta::SetOrigin +//================ +void hhPhysics_Delta::SetOrigin( const idVec3 &newOrigin, int id ) { + // Ripped from idPhysics_Monster + /* + idVec3 masterOrigin; + idMat3 masterAxis; + + current.localOrigin = newOrigin; + if ( masterEntity ) { + self->GetMasterPosition( masterOrigin, masterAxis ); + current.origin = masterOrigin + newOrigin * masterAxis; + } + else { + current.origin = newOrigin; + } + */ + clipModel->Link( gameLocal.clip, self, 0, newOrigin, clipModel->GetAxis() ); + //? Activate(); +} + +//================ +//hhPhysics_Delta::Save +//================ +void hhPhysics_Delta::Save( idSaveGame *savefile ) const { + savefile->WriteVec3( delta ); +} + +//================ +//hhPhysics_Delta::Restore +//================ +void hhPhysics_Delta::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3( delta ); +} + diff --git a/src/Prey/physics_delta.h b/src/Prey/physics_delta.h new file mode 100644 index 0000000..76d1505 --- /dev/null +++ b/src/Prey/physics_delta.h @@ -0,0 +1,34 @@ +#ifndef __PREY_PHYSICS_DELTA_H__ +#define __PREY_PHYSICS_DELTA_H__ + + + +class hhPhysics_Delta : public idPhysics_Actor { + + public: + CLASS_PROTOTYPE( hhPhysics_Delta ); + + hhPhysics_Delta(); + + void SetOrigin( const idVec3 &newOrigin, int id = -1 ); + void SetAxis( const idMat3 &newAxis, int id = -1 ); + + + bool Evaluate( int timeStepMSec, int endTimeMSec ); + + + void SetDelta( const idVec3 &d ); + + void Activate(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + void Rest(); + + idVec3 delta; + +}; + +#endif /* __PREY_PHYSICS_DELTA_H__ */ diff --git a/src/Prey/physics_preyai.cpp b/src/Prey/physics_preyai.cpp new file mode 100644 index 0000000..71546f8 --- /dev/null +++ b/src/Prey/physics_preyai.cpp @@ -0,0 +1,573 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// HUMANHEAD nla - Class constants +const int hhPhysics_AI::NO_FLY_DIRECTION = 1000; + +CLASS_DECLARATION( idPhysics_Monster, hhPhysics_AI ) +END_CLASS + +/* +===================== +hhPhysics_AI::hhPhysics_AI +===================== +*/ +hhPhysics_AI::hhPhysics_AI() { + // HUMANHEAD nla + flyStepDirection = NO_FLY_DIRECTION; + //HUMANHEAD END + lastMoveTouch = NULL; + useGravity = TRUE; + bGravClipModelAxis = false; +} + +/* +===================== +hhPhysics_AI::SlideMove +===================== +// HUMANHEAD nla - if touched is NULL, the touched entities will be added +// directly. Otherwise they will be added to the list +*/ +monsterMoveResult_t hhPhysics_AI::SlideMove( idVec3 &start, idVec3 &velocity, const idVec3 &delta, idList *touched ) { + trace_t tr; + idVec3 move; + int i; + + blockingEntity = NULL; + move = delta; + for( i = 0; i < 3; i++ ) { + gameLocal.clip.Translation( tr, start, start + move, clipModel, clipModel->GetAxis(), clipMask, self ); + + if ( tr.c.entityNum != ENTITYNUM_WORLD && tr.c.entityNum != ENTITYNUM_NONE && gameLocal.entities[ tr.c.entityNum ] ) + lastMoveTouch = gameLocal.entities[ tr.c.entityNum ]; + + + start = tr.endpos; + + if ( tr.fraction == 1.0f ) { + if ( i > 0 ) { + return MM_SLIDING; + } + return MM_OK; + } + + if ( tr.c.entityNum != ENTITYNUM_NONE ) { + blockingEntity = gameLocal.entities[ tr.c.entityNum ]; + } + + /* + // clip the movement delta and velocity + move.ProjectOntoPlane( tr.c.normal, OVERCLIP ); + velocity.ProjectOntoPlane( tr.c.normal, OVERCLIP ); + */ + + // HUMANHEAD nla - Added logic to allow monsters to push + // if we can push other entities and not blocked by the world. Added PushCosine + if ( self->Pushes() && ( tr.c.entityNum != ENTITYNUM_WORLD ) ) { + // Early out if we aren't facing enough towards the object + idVec3 normVelocity( velocity ); + + normVelocity.NormalizeFast(); + /* + gameLocal.Printf( "%d Trying %s * %s = %.2f vs %.2f\n", gameLocal.time, + normVelocity.ToString(), tr.c.normal.ToString(), + idMath::Fabs( normVelocity * tr.c.normal ), self->PushCosine() ); + */ + if ( idMath::Fabs( normVelocity * tr.c.normal ) >= self->PushCosine() ) { + trace_t trPush; + int pushFlags; + float totalMass; + + clipModel->SetPosition( start, clipModel->GetAxis() ); + + // clip movement, only push idMoveables, don't push entities the player is standing on + // apply impact to pushed objects + pushFlags = PUSHFL_CLIP|PUSHFL_ONLYMOVEABLE|PUSHFL_NOGROUNDENTITIES; + + // clip & push + totalMass = gameLocal.push.ClipTranslationalPush( trPush, self, pushFlags, start + move, move ); + + if ( totalMass > 0.0f ) { + // decrease velocity based on the total mass of the objects being pushed ? + /*? Put back in later? + if ( velocity.LengthSqr() > Square( crouchSpeed * 0.8f ) ) { + velocity *= crouchSpeed * 0.8f / velocity.Length(); + } + pushed = true; + */ + } + + // Added logic to prevent pushing + tr = trPush; + start = tr.endpos; + // time_left -= time_left * trace.fraction; + + // if moved the entire distance + // Changed from orig. + /* + if ( trace.fraction >= 1.0f ) { + break; + } + */ + // Changed to this! = ) + if ( tr.fraction == 1.0f ) { + if ( i > 0 ) { + return MM_SLIDING; + } + return MM_OK; + } + } + } + // HUMANHEAD END + + // clip the movement delta and velocity + move.ProjectOntoPlane( tr.c.normal, OVERCLIP ); + velocity.ProjectOntoPlane( tr.c.normal, OVERCLIP ); + } + +/* NOTE: Underlying code changed some, fit this back in if needed. + //HUMANHEAD nla + if (touched == NULL) { + if (tr.fraction < 1.0f) { + AddTouchEnt(tr.c.entityNum); + } + } + else { + touched->Append(tr.c.entityNum); + } +*/ + return MM_BLOCKED; +} + +// HUMANHEAD nla +/* +===================== +hhPhysics_AI::FlyMove + + move start into the delta direction + the velocity is clipped conform any collisions +===================== +*/ +#define NUM_DIRECTIONS 2 +monsterMoveResult_t hhPhysics_AI::FlyMove( idVec3 &start, idVec3 &velocity, const idVec3 &delta ) { + trace_t tr; + idVec3 up, down, noStepPos, noStepVel, stepPos, stepVel, dirGrav; + monsterMoveResult_t result1, result2; + idVec3 originalStart, originalVelocity, bestStart, bestVelocity; + float noStepDistSq, distSq, bestDistSq; + int bestFlyStepDirection; + monsterMoveResult_t bestReturn; + idList noStepEntities, stepEntities, bestEntities; + + + if ( delta == vec3_origin ) { + return MM_OK; + } + + // Initialize + originalStart = start; + originalVelocity = velocity; + bestFlyStepDirection = NO_FLY_DIRECTION; + + // try to move without stepping up + noStepPos = start; + noStepVel = velocity; + result1 = SlideMove( noStepPos, noStepVel, delta, &noStepEntities ); + if ( result1 == MM_OK ) { + AddTouchEntList( noStepEntities ); + start = noStepPos; + velocity = noStepVel; + return MM_OK; + } + + // Assume no stepping is the best + bestStart = noStepPos; + bestVelocity = noStepVel; + bestReturn = result1; + bestEntities = noStepEntities; + // Add small fudge factor to take into account float issues + noStepDistSq = ((noStepPos - originalStart) * 1.001f).LengthSqr(); + bestDistSq = noStepDistSq; + + // Try to move around the obstacle + for ( int direction = 0; direction < NUM_DIRECTIONS ; direction++ ) { + stepEntities.Clear(); + + //! Maybe make this relative to the clip model? + if (direction == 0) { // Set the direction. Assumes 2x + dirGrav.Set(0, 0, -1); + } + else { + dirGrav *= -1; + } + + // try to step "up" + up = start - dirGrav * maxStepHeight; + gameLocal.clip.Translation( tr, start, up, clipModel, clipModel->GetAxis(), clipMask, self ); + if ( tr.fraction == 0.0f ) { + start = originalStart; // Reset + velocity = originalVelocity; + continue; + } + + // Add the entity touched + if ( tr.fraction < 1.0f ) { + stepEntities.Append( tr.c.entityNum ); + } + + // try to move at the stepped up position + stepPos = tr.endpos; + stepVel = velocity; + result2 = SlideMove( stepPos, stepVel, delta, &stepEntities ); + if ( result2 == MM_BLOCKED ) { // Couldn't move all the way at stepped pos + start = originalStart; // Reset + velocity = originalVelocity; + distSq = (stepPos - tr.endpos).LengthSqr(); // See if we moved further up there. Ignore the step up + if ((distSq > bestDistSq)) { + bestStart = stepPos; + bestVelocity = stepVel; + bestReturn = result2; + bestEntities = stepEntities; + bestDistSq = distSq; + bestFlyStepDirection = direction; + } + continue; + } + + // Stepping is the best move + start = originalStart; // Reset + velocity = originalVelocity; + distSq = ( stepPos - originalStart ).LengthSqr(); // Include the distance up we traveled + if ( ( direction == flyStepDirection ) || // Going the same direction as last time + ( ( ( direction < flyStepDirection ) || // Haven't tested the last direction + ( bestFlyStepDirection != flyStepDirection ) ) && // Have tested the last, but coundn't go that way + ( distSq > bestDistSq ) ) // Closer than the best + ) { + bestStart = stepPos; // Save the no step version + bestVelocity = stepVel; + bestReturn = MM_STEPPED; + bestEntities = stepEntities; + bestDistSq = distSq; + bestFlyStepDirection = direction; + } + + } //. Direction loop + + // Didn't work, use the no step return + start = bestStart; + velocity = bestVelocity; + flyStepDirection = bestFlyStepDirection; + AddTouchEntList( bestEntities ); + + return( bestReturn ); +} + +/* +================ +hhPhysics_AI::Evaluate +================ +*/ +bool hhPhysics_AI::Evaluate( int timeStepMSec, int endTimeMSec ) { + idVec3 masterOrigin, oldOrigin; + idMat3 masterAxis; + float timeStep; + float oldMasterYaw, oldMasterDeltaYaw; // HUMANHEAD pdm + + timeStep = MS2SEC( timeStepMSec ); + + moveResult = MM_OK; + blockingEntity = NULL; + oldOrigin = current.origin; + oldMasterYaw = masterYaw; // HUMANHEAD pdm + oldMasterDeltaYaw = masterDeltaYaw; // HUMANHEAD pdm + + //HUMANHEAD: aob + HadGroundContacts( HasGroundContacts() ); + //HUMANHEAD END + + // if bound to a master + if ( masterEntity ) { + self->GetMasterPosition( masterOrigin, masterAxis ); + current.origin = masterOrigin + current.localOrigin * masterAxis; + clipModel->Link( gameLocal.clip, self, 0, current.origin, clipModel->GetAxis() ); + //HUMANHEAD rww + if (!timeStep) { + current.velocity = vec3_origin; + } + else { + //HUMANHEAD END + current.velocity = ( current.origin - oldOrigin ) / timeStep; + } + masterDeltaYaw = masterYaw; + masterYaw = masterAxis[0].ToYaw(); + masterDeltaYaw = masterYaw - masterDeltaYaw; + + // HUMANHEAD pdm: If we haven't moved and master is at rest, put me to rest + if (current.origin == oldOrigin && masterYaw == oldMasterYaw && masterDeltaYaw == oldMasterDeltaYaw) { + if (masterEntity->IsAtRest()) { + Rest(); + } + return false; + } + // HUMANHEAD END + return true; + } + + // if the monster is at rest + if ( current.atRest >= 0 ) { + return false; + } + + assert(timeStep != 0.0f); //HUMANHEAD rww + + ActivateContactEntities(); + + // move the monster velocity into the frame of a pusher + current.velocity -= current.pushVelocity; + + clipModel->Unlink(); + + // check if on the ground + //HUMANHEAD: aob - moved ground check to after movement + + // if not on the ground or moving upwards + float upspeed; + if ( gravityNormal != vec3_zero ) { + upspeed = -( current.velocity * gravityNormal ); + } else { + upspeed = current.velocity.z; + } + if ( fly || self->fl.isTractored || ( !forceDeltaMove && ( !current.onGround || upspeed > 1.0f ) ) ) { + if ( upspeed < 0.0f ) { + moveResult = MM_FALLING; + } + else { + current.onGround = false; + moveResult = MM_OK; + } + delta = current.velocity * timeStep; + if ( delta != vec3_origin ) { + //HUMANHEAD: aob - removed scope hardcode + moveResult = SlideMove( current.origin, current.velocity, delta ); + delta.Zero(); + } + + if ( !fly && !self->fl.isTractored && IsGravityEnabled()) { // HUMANHEAD JRM: Gravity toggle + current.velocity += gravityVector * timeStep; + } + } else { + if ( useVelocityMove ) { + delta = current.velocity * timeStep; + } else { + current.velocity = delta / timeStep; + } + + current.velocity -= ( current.velocity * gravityNormal ) * gravityNormal; + + if ( delta == vec3_origin ) { + Rest(); + } else { + // try moving into the desired direction + //HUMANHEAD: aob + current.velocity = delta / timeStep; + //HUMANHEAD END + + //HUMANHEAD: aob - removed scope hardcode + //moveResult = idPhysics_Monster::StepMove( current.origin, current.velocity, delta ); + if( IsGravityEnabled() ) { + moveResult = StepMove( current.origin, current.velocity, delta ); + } else { + moveResult = SlideMove( current.origin, current.velocity, delta ); + } + + delta.Zero(); + } + } + + //HUMANHEAD: aob - for changing gravity zones + idVec3 rotationCheckOrigin = GetOrigin() + GetAxis()[2] * GetBounds()[1].z; + IterativeRotateMove( GetAxis()[2], -GetGravityNormal(), GetOrigin(), rotationCheckOrigin, p_iterRotMoveNumIterations.GetInteger() ); + //HUMANHEAD END + + clipModel->Link( gameLocal.clip, self, 0, current.origin, clipModel->GetAxis() ); + + // check if on the ground + //HUMANHEAD: aob - removed scope hardcode + CheckGround( current ); + + // get all the ground contacts + EvaluateContacts(); + + // move the monster velocity back into the world frame + current.velocity += current.pushVelocity; + current.pushVelocity.Zero(); + + if ( IsOutsideWorld() ) { + // HUMANHEAD pdm: Allow some things to go outside world without warning + if (!self->IsType(hhWraith::Type) && !self->IsType( hhTalon::Type ) ) { + gameLocal.Warning( "clip model outside world bounds for entity '%s' at (%s)", self->name.c_str(), current.origin.ToString(0) ); + } + Rest(); + } + + return ( current.origin != oldOrigin ); +} + +/* +================== +hhPhysics_AI::ApplyFriction + +Handles both ground friction and water friction + +//HUMANHEAD: aob +================== +*/ +idVec3 hhPhysics_AI::ApplyFriction( const idVec3& vel, const float deltaTime ) { + float speed = 0.0f; + float newSpeed = 0.0f; + float control = 0.0f; + float drop = 0.0f; + idVec3 velocity = vel; + + speed = velocity.Length(); + if( speed <= VECTOR_EPSILON ) { + return vec3_origin; + } + + if( fly || self->fl.isTractored ) { + drop += speed * PM_FLYFRICTION * deltaTime; + } + else if( !current.onGround ) { + drop += speed * PM_AIRFRICTION * deltaTime; + } + else if( current.onGround ) { + if( !(GetGroundSurfaceFlags() & SURF_SLICK) ) { + drop += speed * PM_FRICTION * deltaTime; + } + } + + // scale the velocity + newSpeed = speed - drop; + if( newSpeed < 0.0f ) { + newSpeed = 0.0f; + } + + return vel * ( newSpeed / speed ); +} + +/* +================ +hhPhysics_AI::AddForce + HUMANHEAD pdm: added so that springs will work on monsters +================ +*/ +void hhPhysics_AI::AddForce( const int id, const idVec3 &point, const idVec3 &force ) { + // HUMANHEAD pdm: so we can use forces on them + current.velocity += (force / GetMass()) * USERCMD_ONE_OVER_HZ; + + Activate(); +} + +/* +================ +hhPhysics_AI::SetLinearVelocity +================ +*/ +void hhPhysics_AI::SetLinearVelocity( const idVec3 &newLinearVelocity, int id ) { + current.velocity = newLinearVelocity; + + Activate(); +} + +/* +================ +hhPhysics_AI::GetLinearVelocity +================ +*/ +const idVec3& hhPhysics_AI::GetLinearVelocity( int id ) const { + return current.velocity; +} + +/* +================ +hhPhysics_AI::ApplyImpulse +================ +*/ +void hhPhysics_AI::ApplyImpulse( const int id, const idVec3 &point, const idVec3 &impulse ) { + // HUMANHEAD: aob - so we can use forces on them (crane) + current.velocity += impulse / GetMass(); + // HUMANHEAD END + + Activate(); +} + +/* +================ +hhPhysics_AI::SetMaster +overridden to set localAxis +================ +*/ +void hhPhysics_AI::SetMaster( idEntity *master, const bool orientated ) { + idVec3 masterOrigin; + idMat3 masterAxis; + + if ( master ) { + if ( !masterEntity ) { + // transform from world space to master space + self->GetMasterPosition( masterOrigin, masterAxis ); + current.localOrigin = ( current.origin - masterOrigin ) * masterAxis.Transpose(); + masterEntity = master; + masterYaw = masterAxis[0].ToYaw(); + idAI *entAI = static_cast(self); + if ( entAI ) { + idAngles angles( 0, entAI->spawnArgs.GetFloat( "angle" ), 0 ); + localAxis = angles.ToMat3() * masterAxis.Transpose(); + } + } + ClearContacts(); + } + else { + if ( masterEntity ) { + masterEntity = NULL; + Activate(); + } + } +} + +/* +================ +hhPhysics_AI::SetGravity +overridden to call SetClipModelAxis for asteroid gravity +================ +*/ +void hhPhysics_AI::SetGravity( const idVec3 &newGravity ) { + idPhysics_Monster::SetGravity( newGravity ); + if ( bGravClipModelAxis ) { + SetClipModelAxis(); + } +} + +//================ +//hhPhysics_AI::Save +//================ +void hhPhysics_AI::Save( idSaveGame *savefile ) const { + savefile->WriteInt( flyStepDirection ); + lastMoveTouch.Save( savefile ); + savefile->WriteBool( useGravity ); + savefile->WriteMat3( localAxis ); + savefile->WriteBool( bGravClipModelAxis ); +} + +//================ +//hhPhysics_AI::Restore +//================ +void hhPhysics_AI::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( flyStepDirection ); + lastMoveTouch.Restore( savefile ); + savefile->ReadBool( useGravity ); + savefile->ReadMat3( localAxis ); + savefile->ReadBool( bGravClipModelAxis ); +} + diff --git a/src/Prey/physics_preyai.h b/src/Prey/physics_preyai.h new file mode 100644 index 0000000..1c2f5c7 --- /dev/null +++ b/src/Prey/physics_preyai.h @@ -0,0 +1,45 @@ +#ifndef __HH_PHYSICS_AI +#define __HH_PHYSICS_AI + +class hhPhysics_AI : public idPhysics_Monster { + CLASS_PROTOTYPE( hhPhysics_AI ); + + public: + hhPhysics_AI(); + + bool Evaluate( int timeStepMSec, int endTimeMSec ); + const idVec3& GetLinearVelocity( int id = 0 ) const; + virtual void SetLinearVelocity( const idVec3 &newLinearVelocity, int id = 0 ); + void AddForce( const int id, const idVec3 &point, const idVec3 &force ); + void ApplyImpulse( const int id, const idVec3 &point, const idVec3 &impulse ); + idVec3 GetLocalOrigin( void ) { return current.localOrigin; } + + idEntity* GetLastMoveTouch(void) {return lastMoveTouch.GetEntity();} + + void EnableGravity(bool tf = true) {useGravity = tf;} + bool IsGravityEnabled(void) const {return useGravity;} + void SetMaster( idEntity *master, const bool orientated ); + void SetGravity( const idVec3 &newGravity ); + void SetGravClipModelAxis( bool enable ) { bGravClipModelAxis = enable; } + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + idVec3 ApplyFriction( const idVec3& vel, const float deltaTime ); + virtual monsterMoveResult_t FlyMove( idVec3 &start, idVec3 &velocity, const idVec3 &delta ); + virtual monsterMoveResult_t SlideMove( idVec3 &start, idVec3 &velocity, const idVec3 &delta, idList *touched = NULL ); + + // HUMANHEAD nla - Constants + public: + static const int NO_FLY_DIRECTION; + idMat3 localAxis; + + protected: + int flyStepDirection; // Direction we were last stepping in + idEntityPtr lastMoveTouch; // Entity we touched on last slidemove + bool useGravity; + bool bGravClipModelAxis; // Set clipmodel axis for gravity changes +}; + +#endif \ No newline at end of file diff --git a/src/Prey/physics_preyparametric.cpp b/src/Prey/physics_preyparametric.cpp new file mode 100644 index 0000000..4808a3b --- /dev/null +++ b/src/Prey/physics_preyparametric.cpp @@ -0,0 +1,302 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idPhysics_Static, hhPhysics_StaticWeapon ) +END_CLASS + +/* +============ +hhPhysics_StaticWeapon::hhPhysics_StaticWeapon +============ +*/ +hhPhysics_StaticWeapon::hhPhysics_StaticWeapon() { + castSelf = NULL; + selfOwner = NULL; +} + +/* +============ +hhPhysics_StaticWeapon::SetSelfOwner +============ +*/ +void hhPhysics_StaticWeapon::SetSelfOwner( idActor* a ) { + if( self && self->IsType(hhWeapon::Type) ) { + castSelf = static_cast( self ); + } + + if( a && a->IsType(hhPlayer::Type) ) { + selfOwner = static_cast( a ); + } +} + +/* +============ +hhPhysics_StaticWeapon::SetLocalAxis +============ +*/ +void hhPhysics_StaticWeapon::SetLocalAxis( const idMat3& newLocalAxis ) { + current.localAxis = newLocalAxis; +} + +/* +============ +hhPhysics_StaticWeapon::SetLocalOrigin +============ +*/ +void hhPhysics_StaticWeapon::SetLocalOrigin( const idVec3& newLocalOrigin ) { + current.localOrigin = newLocalOrigin; +} + +/* +============ +hhPhysics_StaticWeapon::Evaluate +============ +*/ +bool hhPhysics_StaticWeapon::Evaluate( int timeStepMSec, int endTimeMSec ) { + if( !selfOwner ) { + return false; + } + + idMat3 localAxis; + idVec3 localOrigin( 0.0f, 0.0f, selfOwner->EyeHeight() ); + idAngles pitchAngles( selfOwner->GetUntransformedViewAngles().pitch, 0.0f, 0.0f ); + + if( selfOwner->InVehicle() ) { + localAxis = pitchAngles.ToMat3(); + } else { + localAxis = ( pitchAngles + selfOwner->GunTurningOffset() ).ToMat3(); + localOrigin += selfOwner->GunAcceleratingOffset(); + if ( castSelf ) { + castSelf->MuzzleRise( localOrigin, localAxis ); + } + } + + SetLocalAxis( localAxis ); + SetLocalOrigin( localOrigin ); + + return idPhysics_Static::Evaluate( timeStepMSec, endTimeMSec ); +} + +//================ +//hhPhysics_StaticWeapon::Save +//================ +void hhPhysics_StaticWeapon::Save( idSaveGame *savefile ) const { + savefile->WriteObject( castSelf ); + savefile->WriteObject( selfOwner ); +} + +//================ +//hhPhysics_StaticWeapon::Restore +//================ +void hhPhysics_StaticWeapon::Restore( idRestoreGame *savefile ) { + savefile->ReadObject( reinterpret_cast( castSelf ) ); + savefile->ReadObject( reinterpret_cast( selfOwner ) ); +} + +CLASS_DECLARATION( idPhysics_Static, hhPhysics_StaticForceField ) +END_CLASS + +/* +================ +hhPhysics_StaticForceField::hhPhysics_StaticForceField +================ +*/ +hhPhysics_StaticForceField::hhPhysics_StaticForceField() { +} + +/* +================ +hhPhysics_StaticForceField::Evaluate +================ +*/ +bool hhPhysics_StaticForceField::Evaluate( int timeStepMSec, int endTimeMSec ) { + bool moved = idPhysics_Static::Evaluate( timeStepMSec, endTimeMSec ); + + EvaluateContacts(); + + return moved; +} + +/* +================ +hhPhysics_StaticForceField::AddContactEntitiesForContacts +================ +*/ +void hhPhysics_StaticForceField::AddContactEntitiesForContacts( void ) { + int i; + idEntity *ent; + + for ( i = 0; i < contacts.Num(); i++ ) { + ent = gameLocal.entities[ contacts[i].entityNum ]; + if ( ent && ent != self ) { + ent->AddContactEntity( self ); + } + } +} + +/* +================ +hhPhysics_StaticForceField::ActivateContactEntities +================ +*/ +void hhPhysics_StaticForceField::ActivateContactEntities( void ) { + int i; + idEntity *ent; + + for ( i = 0; i < contactEntities.Num(); i++ ) { + ent = contactEntities[i].GetEntity(); + if ( ent ) { + ent->ActivatePhysics( self ); + } else { + contactEntities.RemoveIndex( i-- ); + } + } +} + +/* +================ +hhPhysics_StaticForceField::EvaluateContacts +================ +*/ +bool hhPhysics_StaticForceField::EvaluateContacts( void ) { + idVec6 dir; + int num; + + ClearContacts(); + + contacts.SetNum( 10, false ); + + dir.SubVec3(0).Zero(); + dir.SubVec3(1).Zero(); + //dir.SubVec3(0).Normalize(); + //dir.SubVec3(1).Normalize(); + num = gameLocal.clip.Contacts( &contacts[0], 10, clipModel->GetOrigin(), + dir, CONTACT_EPSILON, clipModel, clipModel->GetAxis(), MASK_SOLID, self ); + contacts.SetNum( num, false ); + + AddContactEntitiesForContacts(); + + return ( contacts.Num() != 0 ); +} + +/* +================ +hhPhysics_StaticForceField::GetNumContacts +================ +*/ +int hhPhysics_StaticForceField::GetNumContacts( void ) const { + return contacts.Num(); +} + +/* +================ +hhPhysics_StaticForceField::GetContact +================ +*/ +const contactInfo_t &hhPhysics_StaticForceField::GetContact( int num ) const { + return contacts[num]; +} + +/* +================ +hhPhysics_StaticForceField::ClearContacts +================ +*/ +void hhPhysics_StaticForceField::ClearContacts( void ) { + int i; + idEntity *ent; + + for ( i = 0; i < contacts.Num(); i++ ) { + ent = gameLocal.entities[ contacts[i].entityNum ]; + if ( ent ) { + ent->RemoveContactEntity( self ); + } + } + contacts.SetNum( 0, false ); +} + +/* +================ +hhPhysics_StaticForceField::AddContactEntity +================ +*/ +void hhPhysics_StaticForceField::AddContactEntity( idEntity *e ) { + int i; + idEntity *ent; + + for ( i = 0; i < contactEntities.Num(); i++ ) { + ent = contactEntities[i].GetEntity(); + if ( !ent ) { + contactEntities.RemoveIndex( i-- ); + continue; + } + if ( ent == e ) { + return; + } + } + contactEntities.Alloc() = e; +} + +/* +================ +hhPhysics_StaticForceField::RemoveContactEntity +================ +*/ +void hhPhysics_StaticForceField::RemoveContactEntity( idEntity *e ) { + int i; + idEntity *ent; + + for ( i = 0; i < contactEntities.Num(); i++ ) { + ent = contactEntities[i].GetEntity(); + if ( !ent ) { + contactEntities.RemoveIndex( i-- ); + continue; + } + if ( ent == e ) { + contactEntities.RemoveIndex( i-- ); + return; + } + } +} + +//================ +//hhPhysics_StaticForceField::Save +//================ +void hhPhysics_StaticForceField::Save( idSaveGame *savefile ) const { + int i; + savefile->WriteInt( contacts.Num() ); + for ( i = 0; i < contacts.Num(); i++ ) { + savefile->WriteContactInfo( contacts[i] ); + } + + savefile->WriteInt( contactEntities.Num() ); + for ( i = 0; i < contactEntities.Num(); i++ ) { + //HUMANHEAD PCF mdl 04/26/06 - Changed '->' to a '.', as it should have been + contactEntities[i].Save( savefile ); + } +} + +//================ +//hhPhysics_StaticForceField::Restore +//================ +void hhPhysics_StaticForceField::Restore( idRestoreGame *savefile ) { + int i; + int num; + + savefile->ReadInt( num ); + contacts.SetNum( num ); + for ( i = 0; i < num; i++ ) { + savefile->ReadContactInfo( contacts[i] ); + } + + savefile->ReadInt( num ); + contactEntities.SetNum( num ); + for ( i = 0; i < num; i++ ) { + //HUMANHEAD PCF mdl 04/26/06 - Changed '->' to a '.', as it should have been + contactEntities[i].Restore( savefile ); + } +} + diff --git a/src/Prey/physics_preyparametric.h b/src/Prey/physics_preyparametric.h new file mode 100644 index 0000000..6169304 --- /dev/null +++ b/src/Prey/physics_preyparametric.h @@ -0,0 +1,54 @@ + +#ifndef __PREY_PHYSICS_PARAMETRIC_H__ +#define __PREY_PHYSICS_PARAMETRIC_H__ + +class hhWeapon; +class hhPlayer; + +class hhPhysics_StaticWeapon : public idPhysics_Static { + CLASS_PROTOTYPE( hhPhysics_StaticWeapon ); + + public: + hhPhysics_StaticWeapon(); + + virtual void SetSelfOwner( idActor* a ); + virtual bool Evaluate( int timeStepMSec, int endTimeMSec ); + + void SetLocalAxis( const idMat3& newLocalAxis ); + void SetLocalOrigin( const idVec3& newLocalOrigin ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + hhWeapon* castSelf; + hhPlayer* selfOwner; +}; + +class hhPhysics_StaticForceField : public idPhysics_Static { + CLASS_PROTOTYPE( hhPhysics_StaticForceField ); + + public: + hhPhysics_StaticForceField(); + + virtual bool Evaluate( int timeStepMSec, int endTimeMSec ); + + virtual bool EvaluateContacts( void ); + virtual int GetNumContacts( void ) const; + virtual const contactInfo_t & GetContact( int num ) const; + virtual void ClearContacts( void ); + virtual void AddContactEntitiesForContacts( void ); + virtual void AddContactEntity( idEntity *e ); + virtual void RemoveContactEntity( idEntity *e ); + + virtual void ActivateContactEntities( void ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + idList contacts; // contacts + idList contactEntities; +}; + +#endif /* __PREY_PHYSICS_PARAMETRIC_H__ */ diff --git a/src/Prey/physics_simple.cpp b/src/Prey/physics_simple.cpp new file mode 100644 index 0000000..428013b --- /dev/null +++ b/src/Prey/physics_simple.cpp @@ -0,0 +1,50 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idPhysics_RigidBody, hhPhysics_RigidBodySimple ) +END_CLASS + +/* +================ +SimpleRigidBodyDerivatives +================ +*/ +void SimpleRigidBodyDerivatives( const float t, const void *clientData, const float *state, float *derivatives ) { + const hhPhysics_RigidBodySimple *p = (hhPhysics_RigidBodySimple *) clientData; + rigidBodyIState_t *s = (rigidBodyIState_t *) state; + // NOTE: this struct should be build conform rigidBodyIState_t + struct rigidBodyDerivatives_s { + idVec3 linearVelocity; + idMat3 angularMatrix; + idVec3 force; + idVec3 torque; + } *d = (struct rigidBodyDerivatives_s *) derivatives; + + // derivatives + d->linearVelocity = p->inverseMass * s->linearMomentum; + d->angularMatrix.Zero(); + //d->angularMatrix = SkewSymmetric( vec3_zero ) * s->orientation; + d->force = - p->linearFriction * s->linearMomentum + p->current.externalForce; + d->torque.Zero(); +} + +/* +================ +hhPhysics_RigidBodySimple::hhPhysics_RigidBodySimple +================ +*/ +hhPhysics_RigidBodySimple::hhPhysics_RigidBodySimple() { + SAFE_DELETE_PTR( integrator ); + integrator = new idODE_Euler( sizeof(rigidBodyIState_t) / sizeof(float), SimpleRigidBodyDerivatives, this ); +} + +/* +================ +hhPhysics_RigidBodySimple::Integrate +================ +*/ +void hhPhysics_RigidBodySimple::Integrate( const float deltaTime, rigidBodyPState_t &next ) { + idPhysics_RigidBody::Integrate( deltaTime, next ); +} \ No newline at end of file diff --git a/src/Prey/physics_simple.h b/src/Prey/physics_simple.h new file mode 100644 index 0000000..ea26372 --- /dev/null +++ b/src/Prey/physics_simple.h @@ -0,0 +1,15 @@ +#ifndef __HH_PHYSICS_SIMPLE_H__ +#define __HH_PHYSICS_SIMPLE_H__ + +class hhPhysics_RigidBodySimple : public idPhysics_RigidBody { + CLASS_PROTOTYPE( hhPhysics_RigidBodySimple ); + public: + hhPhysics_RigidBodySimple(); + + virtual void Integrate( const float deltaTime, rigidBodyPState_t &next ); + + protected: + friend void SimpleRigidBodyDerivatives( const float t, const void *clientData, const float *state, float *derivatives ); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/physics_vehicle.cpp b/src/Prey/physics_vehicle.cpp new file mode 100644 index 0000000..ccb9b1f --- /dev/null +++ b/src/Prey/physics_vehicle.cpp @@ -0,0 +1,234 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +//#define DEBUG_VEHICLE_PHYSICS 1 +#define VEHICLE_DEBUG if(g_vehicleDebug.GetInteger()) gameLocal.Printf +#if DEBUG_VEHICLE_PHYSICS + #define CheckSolid(a, b, c, d, e, f) DoCheckSolid(a, b, c, d, e, f) +#else + #define CheckSolid(a, b, c, d, e, f) 0 +#endif + +//------------------------------------------------------------- +// +// CheckSolid: utility function for checking for collision errors +// +//------------------------------------------------------------- +bool DoCheckSolid(const char *text, idVec3 &pos, idClipModel *clipModel, int clipMask, idMat3 &axis, idEntity *pass) { + int myContents = gameLocal.clip.Contents(pos, clipModel, axis, clipMask, pass); + if( myContents ) { + VEHICLE_DEBUG("%s\n", text); + VEHICLE_DEBUG(" position: %s\n", pos.ToString()); + VEHICLE_DEBUG(" orientation: %s\n", axis.ToAngles().ToString()); + VEHICLE_DEBUG(" contents: %d\n", myContents); + return true; + } + return false; +} + + +CLASS_DECLARATION( hhPhysics_RigidBodySimple, hhPhysics_Vehicle ) +END_CLASS + +//------------------------------------------------------------- +// +// hhPhysics_Vehicle::VehicleMotion +// +//------------------------------------------------------------- +bool hhPhysics_Vehicle::VehicleMotion(trace_t &collision, const idVec3 &start, const idVec3 &end, const idMat3 &axis) { + trace_t translationalTrace; + idVec3 move = end - start; + idVec3 curPosition = start; + idMat3 curAxis = axis; + + memset(&collision, 0, sizeof(collision)); // sanity + + collision.fraction = 1.0f; // Assume success until proven otherwise + + // + // try to slide move + // + for(int i = 0; i < 3; i++ ) { + + gameLocal.clip.Translation( translationalTrace, curPosition, curPosition + move, clipModel, curAxis, clipMask, self ); + curPosition = translationalTrace.endpos; + + CheckSolid(va("SlideMoved (%d) into something solid during slidemove!", i), curPosition, clipModel, clipMask, curAxis, self); + + // Keep the shortest frac + if (translationalTrace.fraction < collision.fraction) { + collision.fraction = translationalTrace.fraction; + collision.c = translationalTrace.c; + } + + if ( translationalTrace.fraction == 1.0f ) { + break; + } + + // Shorten the move by the amount moved + move *= (1.0f - translationalTrace.fraction); + + // project movement and momentum onto the sliding surface + move.ProjectOntoPlane( translationalTrace.c.normal, OVERCLIP ); + } + + collision.endAxis = curAxis; + collision.endpos = curPosition; + + return collision.fraction < 1.0f; +} + +//------------------------------------------------------------- +// +// hhPhysics_Vehicle::CheckForCollisions +// +// Evaluate the impulse based rigid body physics. +// in the event of a collision, tries to do a slide move +//------------------------------------------------------------- +bool hhPhysics_Vehicle::CheckForCollisions( const float deltaTime, rigidBodyPState_t &next, trace_t &collision ) { + bool collided = false; + + CheckSolid("Before VehicleMotion: in something solid!", current.i.position, clipModel, clipMask, current.i.orientation, self); + + collided = VehicleMotion( collision, current.i.position, next.i.position, current.i.orientation ); + if( collided ) { + // set the next state to the state at the moment of impact + next.i.position = collision.endpos; + next.i.orientation = collision.endAxis; + collided = true; + } + + CheckSolid("After VehicleMotion: in something solid!", next.i.position, clipModel, clipMask, next.i.orientation, self); + + return collided; +} + +/* +================ +hhPhysics_Vehicle::SetFriction +================ +*/ +void hhPhysics_Vehicle::SetFriction( const float linear, const float angular, const float contact ) { + //don't cap the friction for the vehicle + linearFriction = linear; + angularFriction = angular; + contactFriction = contact; +} + +const float VEH_VELOCITY_MAX = 16000; +const int VEH_VELOCITY_TOTAL_BITS = 16; +const int VEH_VELOCITY_EXPONENT_BITS = idMath::BitsForInteger( idMath::BitsForFloat( VEH_VELOCITY_MAX ) ) + 1; +const int VEH_VELOCITY_MANTISSA_BITS = VEH_VELOCITY_TOTAL_BITS - 1 - VEH_VELOCITY_EXPONENT_BITS; +const float VEH_MOMENTUM_MAX = 1e20f; +const int VEH_MOMENTUM_TOTAL_BITS = 16; +const int VEH_MOMENTUM_EXPONENT_BITS = idMath::BitsForInteger( idMath::BitsForFloat( VEH_MOMENTUM_MAX ) ) + 1; +const int VEH_MOMENTUM_MANTISSA_BITS = VEH_MOMENTUM_TOTAL_BITS - 1 - VEH_MOMENTUM_EXPONENT_BITS; +const float VEH_FORCE_MAX = 1e20f; +const int VEH_FORCE_TOTAL_BITS = 16; +const int VEH_FORCE_EXPONENT_BITS = idMath::BitsForInteger( idMath::BitsForFloat( VEH_FORCE_MAX ) ) + 1; +const int VEH_FORCE_MANTISSA_BITS = VEH_FORCE_TOTAL_BITS - 1 - VEH_FORCE_EXPONENT_BITS; + +/* +================ +hhPhysics_Vehicle::WriteToSnapshot +================ +*/ +void hhPhysics_Vehicle::WriteToSnapshot( idBitMsgDelta &msg ) const { + idCQuat quat, localQuat; + + quat = current.i.orientation.ToCQuat(); + localQuat = current.localAxis.ToCQuat(); + + msg.WriteFloat( gravityVector.x ); + msg.WriteFloat( gravityVector.y ); + msg.WriteFloat( gravityVector.z ); + + msg.WriteBits(dropToFloor, 1); + + msg.WriteLong( current.atRest ); + msg.WriteFloat( current.i.position[0] ); + msg.WriteFloat( current.i.position[1] ); + msg.WriteFloat( current.i.position[2] ); + msg.WriteFloat( quat.x ); + msg.WriteFloat( quat.y ); + msg.WriteFloat( quat.z ); + msg.WriteFloat( current.i.linearMomentum[0], VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + msg.WriteFloat( current.i.linearMomentum[1], VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + msg.WriteFloat( current.i.linearMomentum[2], VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + msg.WriteFloat( current.i.angularMomentum[0], VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + msg.WriteFloat( current.i.angularMomentum[1], VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + msg.WriteFloat( current.i.angularMomentum[2], VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + msg.WriteDeltaFloat( current.i.position[0], current.localOrigin[0] ); + msg.WriteDeltaFloat( current.i.position[1], current.localOrigin[1] ); + msg.WriteDeltaFloat( current.i.position[2], current.localOrigin[2] ); + msg.WriteDeltaFloat( quat.x, localQuat.x ); + msg.WriteDeltaFloat( quat.y, localQuat.y ); + msg.WriteDeltaFloat( quat.z, localQuat.z ); + msg.WriteDeltaFloat( 0.0f, current.pushVelocity[0], VEH_VELOCITY_EXPONENT_BITS, VEH_VELOCITY_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.pushVelocity[1], VEH_VELOCITY_EXPONENT_BITS, VEH_VELOCITY_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.pushVelocity[2], VEH_VELOCITY_EXPONENT_BITS, VEH_VELOCITY_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.externalForce[0], VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.externalForce[1], VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.externalForce[2], VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.externalTorque[0], VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.externalTorque[1], VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + msg.WriteDeltaFloat( 0.0f, current.externalTorque[2], VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); +} + +/* +================ +hhPhysics_Vehicle::ReadFromSnapshot +================ +*/ +void hhPhysics_Vehicle::ReadFromSnapshot( const idBitMsgDelta &msg ) { + idCQuat quat, localQuat; + + idVec3 newGrav; + newGrav.x = msg.ReadFloat(); + newGrav.y = msg.ReadFloat(); + newGrav.z = msg.ReadFloat(); + + SetGravity(newGrav); + + dropToFloor = !!msg.ReadBits(1); + + current.atRest = msg.ReadLong(); + current.i.position[0] = msg.ReadFloat(); + current.i.position[1] = msg.ReadFloat(); + current.i.position[2] = msg.ReadFloat(); + quat.x = msg.ReadFloat(); + quat.y = msg.ReadFloat(); + quat.z = msg.ReadFloat(); + current.i.linearMomentum[0] = msg.ReadFloat( VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + current.i.linearMomentum[1] = msg.ReadFloat( VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + current.i.linearMomentum[2] = msg.ReadFloat( VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + current.i.angularMomentum[0] = msg.ReadFloat( VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + current.i.angularMomentum[1] = msg.ReadFloat( VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + current.i.angularMomentum[2] = msg.ReadFloat( VEH_MOMENTUM_EXPONENT_BITS, VEH_MOMENTUM_MANTISSA_BITS ); + current.localOrigin[0] = msg.ReadDeltaFloat( current.i.position[0] ); + current.localOrigin[1] = msg.ReadDeltaFloat( current.i.position[1] ); + current.localOrigin[2] = msg.ReadDeltaFloat( current.i.position[2] ); + localQuat.x = msg.ReadDeltaFloat( quat.x ); + localQuat.y = msg.ReadDeltaFloat( quat.y ); + localQuat.z = msg.ReadDeltaFloat( quat.z ); + current.pushVelocity[0] = msg.ReadDeltaFloat( 0.0f, VEH_VELOCITY_EXPONENT_BITS, VEH_VELOCITY_MANTISSA_BITS ); + current.pushVelocity[1] = msg.ReadDeltaFloat( 0.0f, VEH_VELOCITY_EXPONENT_BITS, VEH_VELOCITY_MANTISSA_BITS ); + current.pushVelocity[2] = msg.ReadDeltaFloat( 0.0f, VEH_VELOCITY_EXPONENT_BITS, VEH_VELOCITY_MANTISSA_BITS ); + current.externalForce[0] = msg.ReadDeltaFloat( 0.0f, VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + current.externalForce[1] = msg.ReadDeltaFloat( 0.0f, VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + current.externalForce[2] = msg.ReadDeltaFloat( 0.0f, VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + current.externalTorque[0] = msg.ReadDeltaFloat( 0.0f, VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + current.externalTorque[1] = msg.ReadDeltaFloat( 0.0f, VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + current.externalTorque[2] = msg.ReadDeltaFloat( 0.0f, VEH_FORCE_EXPONENT_BITS, VEH_FORCE_MANTISSA_BITS ); + + current.i.orientation = quat.ToMat3(); + current.localAxis = localQuat.ToMat3(); + + if ( clipModel ) { + clipModel->Link( gameLocal.clip, self, clipModel->GetId(), current.i.position, current.i.orientation ); + } +} diff --git a/src/Prey/physics_vehicle.h b/src/Prey/physics_vehicle.h new file mode 100644 index 0000000..b473386 --- /dev/null +++ b/src/Prey/physics_vehicle.h @@ -0,0 +1,37 @@ + +#ifndef __HH_PHYSICS_VEHICLE_H__ +#define __HH_PHYSICS_VEHICLE_H__ + +/* +=================================================================================== + + hhPhysics_Vehicle + + Simulates the motion of a vehicle through the environment. The orientation of + the vehicle is controlled directly by the player, so the physics move the + vehicle so that it will fit at it's current orientation. + +=================================================================================== +*/ + +class hhPhysics_Vehicle : public hhPhysics_RigidBodySimple { + +public: + CLASS_PROTOTYPE( hhPhysics_Vehicle ); + + virtual void SetFriction( const float linear, const float angular, const float contact ); + + const idVec3& GetLinearMomentum() const { return current.i.linearMomentum; } + const idVec3& GetAngularMomentum() const { return current.i.angularMomentum; } + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + +protected: + virtual bool CheckForCollisions( const float deltaTime, rigidBodyPState_t &next, trace_t &collision ); + + bool VehicleMotion( trace_t &collision, const idVec3 &start, const idVec3 &end, const idMat3 &axis ); +}; + + +#endif \ No newline at end of file diff --git a/src/Prey/prey_animator.cpp b/src/Prey/prey_animator.cpp new file mode 100644 index 0000000..b32d452 --- /dev/null +++ b/src/Prey/prey_animator.cpp @@ -0,0 +1,607 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +/* +============ +hhAnimator::hhAnimator +============ +*/ + +hhAnimator::hhAnimator() { + + lastCycleRotate = 0; + +} //. hhAnimator::hhAnimator() + + +/* +===================== +hhAnimator::CycleAnimRandom +===================== +*/ +void hhAnimator::CycleAnim( int channelNum, int anim, int currentTime, int blendTime, const idEventDef *pEvent ) { + + //! NLANOTE - Add ability to skip cycling + idAnimator::CycleAnim( channelNum, anim, currentTime, blendTime ); + + if ( !anim ) { return; } + + + hhAnim *animPtr = (hhAnim *)GetAnim( anim ); + //HUMANHEAD rww - crash here, can't repro. putting extra logic in for non-gold builds to check. +#if !GOLD + if (!animPtr) { + assert(0); + common->Warning("hhAnimator::CycleAnim anim index %i returned NULL for GetAnim.", anim); + return; + } +#endif +//HUMANHEAD END + + // If the anim was an exact match to the requested name, if so, just return + if ( animPtr->exactMatch ) { + return; + } + + // Determine if there are multiple anims to play. If not, then just return. + int numAnimVariants = GetNumAnimVariants( anim ); + + if ( numAnimVariants < 2 ) { + return; + } + + // The CycleAnim could have started at a random spot in the anim, take this into account, and add only the time left in the anim + int lengthLeft; + lengthLeft = animPtr->Length() - channels[ channelNum ][0].AnimTime( currentTime ); + + channels[ channelNum ][0].rotateTime = currentTime + lengthLeft; + channels[ channelNum ][0].rotateEvent = pEvent; + + entity->PostEventMS( &EV_CheckCycleRotate, lengthLeft ); + +} //. BlendCycleRandom( int, idAnim *, float, int, int ) + + +/* +============== +hhAnimator::GetNumAnimVariants +============== +*/ +int hhAnimator::GetNumAnimVariants( int anim ) { + const char *name = GetAnim( anim )->Name(); + int len, numAnims, maxAnims; + + + // nla - Copied from idDeclModelDef::GetAnim( const char * ) + len = strlen( name ); + if ( len && idStr::CharIsNumeric( name[ len - 1 ] ) ) { + // find a specific animation + return( 1 ); + } + + // find all animations with same name + numAnims = 0; + maxAnims = NumAnims(); + + for( int i = 1; i < maxAnims; i++ ) { + if ( !strcmp( GetAnim( i )->Name(), name ) ) { + numAnims++; + } + } + + return( numAnims ); +} //. hhAnimator::GetNumAnimVariants( int ) + + + +/* +===================== +hhAnimator::CheckCycleRotate + Check the blend animations to see if we need to switch any animations +===================== +*/ +void hhAnimator::CheckCycleRotate() { + hhAnimBlend *blend; + int anim; + int i; + const idEventDef *event; + + + if ( gameLocal.time <= lastCycleRotate ) { + return; + } + lastCycleRotate = gameLocal.time; + + // Cycle through the channels + for( i = 0; i < ANIM_NumAnimChannels; i++ ) { + // Only cycle out the first/main anim of the channel. The rest ar being phased out and don't matter + blend = channels[ i ]; + if ( !blend->IsDone( gameLocal.time ) ) { + if ( blend->Anim() && ( blend->rotateTime > 0 ) && + ( blend->rotateTime <= gameLocal.time ) ) { // HUMANHEAD JRM + + anim = GetAnim( blend->Anim()->Name() ); + event = blend->rotateEvent; + + // gameLocal.Printf( "%s Cycling Anim %s\n", entity->name.c_str(), GetAnim( anim )->FullName() ); + + // blendWeight.Init( currentTime, blendTime, weight, newweight ); + int remainingBlend = ( blend->blendStartTime + blend->blendDuration ) - + gameLocal.time; + if ( remainingBlend < 0 ) { remainingBlend = 0; } + blend->CycleAnim( modelDef, anim, gameLocal.time, remainingBlend ); + entity->BecomeActive( TH_ANIMATE ); + + blend->rotateTime = gameLocal.time + GetAnim( anim )->Length(); + blend->rotateEvent = event; + + entity->PostEventMS( &EV_CheckCycleRotate, GetAnim( anim )->Length() ); + + // Process any change events + if ( event ) { + entity->ProcessEvent( event ); + } + } + } + } +} //. CheckCycleRotate + + +/* +==================== +hhAnimator::CheckThaw +HUMANHEAD nla +==================== +*/ +void hhAnimator::CheckThaw( ) { + hhAnimBlend *blend; + int i, j; + + + // Cycle through the channels + blend = channels[0]; + for( i = 0; i < ANIM_NumAnimChannels; i++ ) { + for( j = 0; j < ANIM_MaxAnimsPerChannel; j++, blend++ ) { + if ( blend->Anim() ) { + blend->ThawIfTime( gameLocal.time ); + } + } + } + +} //. CheckThaw( ) + +/* +===================== +hhAnimator::CheckTween + Check if the current animation should be tweened to from the previous anim + Inputs: channel - The channel to check the current anim with + Outputs: The time in ms for the tween. 0 if non given. +HUMANHEAD nla +===================== +*/ +int hhAnimator::CheckTween( int channelNum ) { + idStr toAnimName; + idStr fromAnimName = ""; + int timeMS = 0; + int tempInt; + + + // Ensure we have a valid channel number + if ( ( channelNum < 0 ) || ( channelNum >= ANIM_NumAnimChannels ) ) { + gameLocal.Error( "idAnimator::CheckTween : channel out of range" ); + } + + + // If no valid anim playing, then nothing to tween. :) + if ( channels[ channelNum ][ 0 ].Anim() == NULL ) { + return( 0 ); + } + else { // Set the to anim name + toAnimName = channels[ channelNum ][ 0 ].AnimName(); + } + + // If a valid anim was playing, get its name + if ( channels[ channelNum ][ 1 ].Anim() != NULL ) { + fromAnimName = channels[ channelNum ][ 1 ].AnimName(); + } + + // We aren't really tweening, head out. = ) + if ( toAnimName == fromAnimName ) { + // gameLocal.Printf("Earlying out!"); + return( 0 ); + } + + // See if a default anim tween time is given + if ( entity->spawnArgs.GetInt( va( "tween2%s", + (const char *) toAnimName ), + "0", tempInt ) ) { + timeMS = tempInt; + // JRM - removed + //gameLocal.Printf( "Got tween2%s of %d\n", (const char *) toAnimName, + // timeMS ); + } + + // See if a anim to anim tween time is given + if ( fromAnimName.Length() && + entity->spawnArgs.GetInt( va( "tween%s2%s", + (const char *) fromAnimName, + (const char *) toAnimName ), + "0", tempInt ) ) { + //! Check that they aren't the same anims + + timeMS = tempInt; + // JRM - removed + //gameLocal.Printf( "Got tween%s2%s of %d\n", (const char *) fromAnimName, + // (const char *) toAnimName, + // timeMS ); + } + + // If a valid tween time is valid, use it + if ( timeMS > 0 ) { + // Freeze the current anim + channels[ channelNum ][ 0 ].Freeze( gameLocal.time, GetEntity(), timeMS ); + + // Setup the new blend times + // NLAMERGE 5 - This really used anymore? + /* + channels[ channelNum ][ 0 ].blendWeight.SetDuration( timeMS ); + channels[ channelNum ][ 1 ].blendWeight.SetDuration( timeMS ); + */ + } + + return( timeMS ); +} //. CheckTween( int ) + + +/* +===================== +hhAnimator::IsAnimPlaying +HUMANHEAD nla +===================== +*/ +bool hhAnimator::IsAnimPlaying( int channelNum, const idAnim *anim ) { + idAnimBlend * playingAnim; + + + playingAnim = FindAnim( channelNum, anim ); + if ( playingAnim && ! playingAnim->IsDone( gameLocal.time ) ) { + return( true ); + } + + return( false ); +} //. IsAnimPlaying( idAnim * ) + +/* +===================== +ggAnimator::IsAnimPlaying +HUMANHEAD nla +===================== +*/ +bool hhAnimator::IsAnimPlaying( int channelNum, const char* anim ) { + return IsAnimPlaying( channelNum, GetAnim( GetAnim( anim ) ) ); +} + + +/* +===================== +hhAnimator::IsAnimPlaying +HUMANHEAD nla +===================== +*/ +bool hhAnimator::IsAnimPlaying( const idAnim *anim ) { + + for( int i = 0; i < ANIM_NumAnimChannels; i++ ) { + if ( IsAnimPlaying( i, anim ) ) { + return( true ); + } + } + + return( false ); +} + + +/* +===================== +hhAnimator::IsAnimPlaying +HUMANHEAD nla +===================== +*/ +bool hhAnimator::IsAnimPlaying( const char* anim ) { + return IsAnimPlaying( GetAnim( GetAnim( anim ) ) ); +} + + +/* +===================== +hhAnimator::FindAnim +HUMANHEAD nla +===================== +*/ +idAnimBlend *hhAnimator::FindAnim( int channelNum, const idAnim *anim ) { + int i; + + if ( ( channelNum < 0 ) || ( channelNum >= ANIM_NumAnimChannels ) ) { + gameLocal.Error( "hhAnimator::FindAnim : channel out of range" ); + } + + for( i = 0; i < ANIM_MaxAnimsPerChannel; i++ ) { + if ( channels[ channelNum ][ i ].Anim() == anim ) { + return &( channels[ channelNum ][ i ] ); + } + } + + return NULL; +} + + +/* +===================== +hhAnimator::GetBlendAnim +HUMANHEAD nla +===================== +*/ +const idAnimBlend * hhAnimator::GetBlendAnim( int channelNum, int index ) const { + + if ( channelNum < 0 || channelNum >= ANIM_NumAnimChannels ) { + gameLocal.Error("Channel out of range"); + } + + return ( ( index < 0 ) || ( index >= ANIM_MaxAnimsPerChannel ) ) ? NULL : &(channels[channelNum][ index ]); +} + + +/* +===================== +hhAnimator::NumBlendAnims +// HUMANHEAD Copied logic from IsAnimating +HUMANHEAD nla +===================== +*/ +int hhAnimator::NumBlendAnims( int currentTime ) { + int i, j; + const idAnimBlend *blend; + // HUMANHEAD nla + int num; + + num = 0; + // HUMANHEAD END + + if ( !modelDef->ModelHandle() ) { + return false; + } + + // if animating with an articulated figure + if ( AFPoseJoints.Num() && currentTime <= AFPoseTime ) { + return true; + } + + blend = channels[ 0 ]; + for( i = 0; i < ANIM_NumAnimChannels; i++ ) { + for( j = 0; j < ANIM_MaxAnimsPerChannel; j++, blend++ ) { + if ( !blend->IsDone( currentTime ) ) { + // HUMANHEAD nla + num++; + // HUMANHEAD END + } + } + } + + return num; +} + + +/* +==================== +hhAnimator::CopyAnimations +HUMANHEAD nla +==================== +*/ +void hhAnimator::CopyAnimations( hhAnimator &source ) { + + + // Loop through each channel + for ( int chan = 0; chan < ANIM_NumAnimChannels; ++chan ) { + + // NLA - Simplifed to the double loop in the Nov 2003 "Big Merge" (c) + // Copy the channel anim info + for ( int anim = 0; anim < ANIM_MaxAnimsPerChannel; + ++anim ) { + + channels[ chan ][ anim ] = source.channels[ chan ][ anim ]; + + } + } + + +} + + +/* +==================== +hhAnimator::CopyPoses +HUMANHEAD nla +==================== +*/ +void hhAnimator::CopyPoses( hhAnimator &source ) { + int i, num; + + // Copy over the joint Mod info + jointMods.DeleteContents( true ); + num = source.jointMods.Num(); + jointMods.SetNum( num ); + for( i = 0; i < num; i++ ) { + jointMods[ i ] = new jointMod_t; + *jointMods[ i ] = *source.jointMods[ i ]; + } + + // Copy over the frameBounds + frameBounds = source.frameBounds; + + // Copy over AF info + AFPoseBlendWeight = source.AFPoseBlendWeight; + + num = source.AFPoseJoints.Num(); + AFPoseJoints.SetNum( num ); + for ( int i = 0; i < num; i++ ) { + AFPoseJoints[ i ] = source.AFPoseJoints[ i ]; + } + + num = source.AFPoseJointMods.Num(); + AFPoseJointMods.SetNum( num ); + for ( int i = 0; i < num; i++ ) { + AFPoseJointMods[ i ] = source.AFPoseJointMods[ i ]; + } + + num = source.AFPoseJointFrame.Num(); + AFPoseJointFrame.SetNum( num ); + for ( int i = 0; i < num; i++ ) { + AFPoseJointFrame[ i ] = source.AFPoseJointFrame[ i ]; + } + + AFPoseBounds = source.AFPoseBounds; + AFPoseTime = source.AFPoseTime; +} + + +/* +==================== +hhAnimator::Freeze +HUMANHEAD nla +==================== +*/ +bool hhAnimator::Freeze( ) { + hhAnimBlend *blend; + + + blend = &( channels[ 0 ][ 0 ] ); + + return( blend->Freeze( gameLocal.time, GetEntity() ) ); + +} + + +/* +==================== +hhAnimator::Thaw +HUMANHEAD nla +==================== +*/ +bool hhAnimator::Thaw( ) { + hhAnimBlend *blend; + + + blend = &( channels[ 0 ][ 0 ] ); + + return( blend->Thaw( gameLocal.time ) ); + +} + + +/* +===================== +hhAnimator::IsAnimating +===================== +*/ +bool hhAnimator::IsAnimating( int currentTime ) const { + int i, j; + const hhAnimBlend *blend; + // HUMANHEAD nla + int numFrozen = 0; + // HUMANHEAD END + + + if ( !modelDef || !modelDef->ModelHandle() ) { + return false; + } + + // if animating with an articulated figure + if ( AFPoseJoints.Num() && currentTime <= AFPoseTime ) { + return true; + } + + blend = channels[ 0 ]; + for( i = 0; i < ANIM_NumAnimChannels; i++ ) { + for( j = 0; j < ANIM_MaxAnimsPerChannel; j++, blend++ ) { + if ( !blend->IsDone( currentTime ) ) { + return true; + } + // HUMANHEAD nla + if ( blend->IsFrozen() ) { numFrozen++; } + // HUMANHEAD END + } + } + + // HUMANHEAD nla + if ( numFrozen > 1 ) { return( true ); } + // HUMANHEAD END + + return false; +} + +/* +===================== +hhAnimator::FrameHasChanged +===================== +*/ +bool hhAnimator::FrameHasChanged( int currentTime ) const { + int i, j; + const hhAnimBlend *blend; + // HUMANHEAD nla - Used to allow for tween (2 frozen) anims to work + int numFrozen = 0; + // HUMANHEAD END + + + if ( !modelDef || !modelDef->ModelHandle() ) { + return false; + } + + // if animating with an articulated figure + if ( AFPoseJoints.Num() && currentTime <= AFPoseTime ) { + return true; + } + + blend = channels[ 0 ]; + for( i = 0; i < ANIM_NumAnimChannels; i++ ) { + for( j = 0; j < ANIM_MaxAnimsPerChannel; j++, blend++ ) { + if ( blend->FrameHasChanged( currentTime ) ) { + return true; + } + // HUMANHEAD nla + if ( blend->IsFrozen() ) { numFrozen++; } + // HUMANHEAD END + } + } + + // HUMANHEAD nla + if ( numFrozen > 1 ) { return( true ); } + // HUMANHEAD END + + if ( forceUpdate && IsAnimating( currentTime ) ) { + return true; + } + + return false; +} + +//================ +//hhAnimator::Save +//================ +void hhAnimator::Save( idSaveGame *savefile ) const { + // Not a subclass of idClass, so this is necessary -mdl + idAnimator::Save( savefile ); + savefile->WriteInt( lastCycleRotate ); +} + +//================ +//hhAnimator::Restore +//================ +void hhAnimator::Restore( idRestoreGame *savefile ) { + // Not a subclass of idClass, so this is necessary -mdl + idAnimator::Restore( savefile ); + savefile->ReadInt( lastCycleRotate ); +} + diff --git a/src/Prey/prey_animator.h b/src/Prey/prey_animator.h new file mode 100644 index 0000000..6b5b2c7 --- /dev/null +++ b/src/Prey/prey_animator.h @@ -0,0 +1,50 @@ + + +#ifndef __PREY_GAME_ANIMATOR_H__ +#define __PREY_GAME_ANIMATOR_H__ + +class hhAnimator : public idAnimator { + public: + + hhAnimator(); + + // Overridden methods + bool FrameHasChanged( int animtime ) const; + bool IsAnimating( int currentTime ) const; + + // Unique methods + void CycleAnim( int channelNum, int anim, int currenttime, int blendtime, const idEventDef *pEvent = NULL ); + void CheckCycleRotate(); + void CheckThaw(); + int CheckTween( int channelNum ); + + bool IsAnimPlaying( int channelNum, const idAnim *anim ); + //bool IsAnimPlaying( int channelNum, int animNum ) { return( IsAnimPlaying( channelNum, GetAnim( animNum ) ) ); } + bool IsAnimPlaying( int channelNum, const char* anim ); + bool IsAnimPlaying( const idAnim *anim ); + //bool IsAnimPlaying( int animNum ) { return( IsAnimPlaying( GetAnim( animNum ) ) ); } + bool IsAnimPlaying( const char* anim ); + bool IsAnimatedModel() const { return modelDef != NULL; } + + int GetNumAnimVariants( int anim ); + + const idAnimBlend *GetBlendAnim( int channelNum, int index ) const; + // Backwards compat functions + idAnimBlend * FindAnim( int channelNum, const idAnim *anim ); // JRM: removed const - couldn't compile + const idAnimBlend *GetBlendAnim( int index ) const { return( GetBlendAnim( ANIMCHANNEL_ALL, index ) ); }; + int NumBlendAnims( int currentTime ); + + void CopyAnimations( hhAnimator &animator ); + void CopyPoses( hhAnimator &animator ); + bool Freeze(); + bool Thaw(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + + int lastCycleRotate; +}; + +#endif /* __PREY_GAME_ANIMATOR_H__ */ diff --git a/src/Prey/prey_baseweapons.cpp b/src/Prey/prey_baseweapons.cpp new file mode 100644 index 0000000..df22d35 --- /dev/null +++ b/src/Prey/prey_baseweapons.cpp @@ -0,0 +1,2525 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define WEAPON_DEBUG if(g_debugWeapon.GetBool()) gameLocal.Warning + +/*********************************************************************** + + hhWeapon + +***********************************************************************/ +const idEventDef EV_PlayAnimWhenReady( "playAnimWhenReady", "s" ); +const idEventDef EV_Weapon_Aside( "" ); +const idEventDef EV_Weapon_EjectAltBrass( "ejectAltBrass" ); + +const idEventDef EV_Weapon_HasAmmo( "hasAmmo", "", 'd' ); +const idEventDef EV_Weapon_HasAltAmmo( "hasAltAmmo", "", 'd' ); + +const idEventDef EV_Weapon_GetFireDelay( "getFireDelay", "", 'f' ); +const idEventDef EV_Weapon_GetAltFireDelay( "getAltFireDelay", "", 'f' ); +const idEventDef EV_Weapon_GetSpread( "getSpread", "", 'f' ); +const idEventDef EV_Weapon_GetAltSpread( "getAltSpread", "", 'f' ); +const idEventDef EV_Weapon_GetString( "getString", "s", 's' ); +const idEventDef EV_Weapon_GetAltString( "getAltString", "s", 's' ); + +const idEventDef EV_Weapon_AddToAltClip( "addToAltClip", "f" ); +const idEventDef EV_Weapon_AltAmmoInClip( "altAmmoInClip", "", 'f' ); +const idEventDef EV_Weapon_AltAmmoAvailable( "altAmmoAvailable", "", 'f' ); +const idEventDef EV_Weapon_AltClipSize( "altClipSize", "", 'f' ); + +const idEventDef EV_Weapon_FireAltProjectiles( "fireAltProjectiles" ); +const idEventDef EV_Weapon_FireProjectiles( "fireProjectiles" ); + +const idEventDef EV_Weapon_WeaponAside( "weaponAside" ); // nla +const idEventDef EV_Weapon_WeaponPuttingAside( "weaponPuttingAside" ); // nla +const idEventDef EV_Weapon_WeaponUprighting( "weaponUprighting" ); // nla + +const idEventDef EV_Weapon_IsAnimPlaying( "isAnimPlaying", "s", 'd' ); +const idEventDef EV_Weapon_Raise( "" ); // nla - For the hands to post an event to raise the weapons + +const idEventDef EV_Weapon_SetViewAnglesSensitivity( "setViewAnglesSensitivity", "f" ); +const idEventDef EV_Weapon_UseAltAmmo( "useAltAmmo", "d" ); + +const idEventDef EV_Weapon_Hide( "hideWeapon" ); +const idEventDef EV_Weapon_Show( "showWeapon" ); + +CLASS_DECLARATION( hhAnimatedEntity, hhWeapon ) + EVENT( EV_Weapon_FireAltProjectiles, hhWeapon::Event_FireAltProjectiles ) + EVENT( EV_Weapon_FireProjectiles, hhWeapon::Event_FireProjectiles ) + EVENT( EV_PlayAnimWhenReady, hhWeapon::Event_PlayAnimWhenReady ) + EVENT( EV_SpawnFxAlongBone, hhWeapon::Event_SpawnFXAlongBone ) + EVENT( EV_Weapon_EjectAltBrass, hhWeapon::Event_EjectAltBrass ) + EVENT( EV_Weapon_HasAmmo, hhWeapon::Event_HasAmmo ) + EVENT( EV_Weapon_HasAltAmmo, hhWeapon::Event_HasAltAmmo ) + EVENT( EV_Weapon_AddToAltClip, hhWeapon::Event_AddToAltClip ) + EVENT( EV_Weapon_AltAmmoInClip, hhWeapon::Event_AltAmmoInClip ) + EVENT( EV_Weapon_AltAmmoAvailable, hhWeapon::Event_AltAmmoAvailable ) + EVENT( EV_Weapon_AltClipSize, hhWeapon::Event_AltClipSize ) + EVENT( EV_Weapon_GetFireDelay, hhWeapon::Event_GetFireDelay ) + EVENT( EV_Weapon_GetAltFireDelay, hhWeapon::Event_GetAltFireDelay ) + EVENT( EV_Weapon_GetString, hhWeapon::Event_GetString ) + EVENT( EV_Weapon_GetAltString, hhWeapon::Event_GetAltString ) + EVENT( EV_Weapon_Raise, hhWeapon::Event_Raise ) + EVENT( EV_Weapon_WeaponAside, hhWeapon::Event_Weapon_Aside ) + EVENT( EV_Weapon_WeaponPuttingAside, hhWeapon::Event_Weapon_PuttingAside ) + EVENT( EV_Weapon_WeaponUprighting, hhWeapon::Event_Weapon_Uprighting ) + EVENT( EV_Weapon_IsAnimPlaying, hhWeapon::Event_IsAnimPlaying ) + EVENT( EV_Weapon_SetViewAnglesSensitivity, hhWeapon::Event_SetViewAnglesSensitivity ) + EVENT( EV_Weapon_Hide, hhWeapon::Event_HideWeapon ) + EVENT( EV_Weapon_Show, hhWeapon::Event_ShowWeapon ) + + //idWeapon + EVENT( EV_Weapon_State, hhWeapon::Event_WeaponState ) + EVENT( EV_Weapon_WeaponReady, hhWeapon::Event_WeaponReady ) + EVENT( EV_Weapon_WeaponOutOfAmmo, hhWeapon::Event_WeaponOutOfAmmo ) + EVENT( EV_Weapon_WeaponReloading, hhWeapon::Event_WeaponReloading ) + EVENT( EV_Weapon_WeaponHolstered, hhWeapon::Event_WeaponHolstered ) + EVENT( EV_Weapon_WeaponRising, hhWeapon::Event_WeaponRising ) + EVENT( EV_Weapon_WeaponLowering, hhWeapon::Event_WeaponLowering ) + EVENT( EV_Weapon_AddToClip, hhWeapon::Event_AddToClip ) + EVENT( EV_Weapon_AmmoInClip, hhWeapon::Event_AmmoInClip ) + EVENT( EV_Weapon_AmmoAvailable, hhWeapon::Event_AmmoAvailable ) + EVENT( EV_Weapon_ClipSize, hhWeapon::Event_ClipSize ) + EVENT( AI_PlayAnim, hhWeapon::Event_PlayAnim ) + EVENT( AI_PlayCycle, hhWeapon::Event_PlayCycle ) + EVENT( AI_AnimDone, hhWeapon::Event_AnimDone ) + EVENT( EV_Weapon_Next, hhWeapon::Event_Next ) + EVENT( EV_Weapon_Flashlight, hhWeapon::Event_Flashlight ) + EVENT( EV_Weapon_EjectBrass, hhWeapon::Event_EjectBrass ) + EVENT( EV_Weapon_GetOwner, hhWeapon::Event_GetOwner ) + EVENT( EV_Weapon_UseAmmo, hhWeapon::Event_UseAmmo ) + EVENT( EV_Weapon_UseAltAmmo, hhWeapon::Event_UseAltAmmo ) +END_CLASS + +/* +================ +hhWeapon::hhWeapon +================ +*/ +hhWeapon::hhWeapon() { + owner = NULL; + worldModel = NULL; + + thread = NULL; + + //HUMANHEAD: aob + fireController = NULL; + altFireController = NULL; + fl.networkSync = true; + cameraShakeOffset.Zero(); //rww - had to move here to avoid den/nan/blah in spawn + //HUMANHEAD END + + Clear(); +} + +/* +================ +hhWeapon::Spawn +================ +*/ +void hhWeapon::Spawn() { + //idWeapon + if ( !gameLocal.isClient ) + { + worldModel = static_cast< hhAnimatedEntity * >( gameLocal.SpawnEntityType( hhAnimatedEntity::Type, NULL ) ); + worldModel.GetEntity()->fl.networkSync = true; + } + + thread = new idThread(); + thread->ManualDelete(); + thread->ManualControl(); + //idWeapon End + + physicsObj.SetSelf( this ); + physicsObj.SetClipModel( NULL, 1.0f ); + physicsObj.SetOrigin( GetPhysics()->GetOrigin() ); + physicsObj.SetAxis( GetPhysics()->GetAxis() ); + SetPhysics( &physicsObj ); + + fl.solidForTeam = true; + + // HUMANHEAD: aob + memset( &eyeTraceInfo, 0, sizeof(trace_t) ); + eyeTraceInfo.fraction = 1.0f; + BecomeActive( TH_TICKER ); + handedness = hhMath::hhMax( 1, spawnArgs.GetInt("handedness", "1") ); + fl.neverDormant = true; + // HUMANHEAD END +} + +/* +================ +hhWeapon::~hhWeapon() +================ +*/ +hhWeapon::~hhWeapon() { + SAFE_REMOVE( worldModel ); + SAFE_REMOVE( thread ); + + SAFE_DELETE_PTR( fireController ); + SAFE_DELETE_PTR( altFireController ); + + if ( nozzleFx && nozzleGlowHandle != -1 ) { + gameRenderWorld->FreeLightDef( nozzleGlowHandle ); + } +} + +/* +================ +hhWeapon::SetOwner +================ +*/ +void hhWeapon::SetOwner( idPlayer *_owner ) { + if( !_owner || !_owner->IsType(hhPlayer::Type) ) { + owner = NULL; + return; + } + + owner = static_cast( _owner ); + + if( GetPhysics() && GetPhysics()->IsType(hhPhysics_StaticWeapon::Type) ) { + static_cast(GetPhysics())->SetSelfOwner( owner.GetEntity() ); + } +} + +/* +================ +hhWeapon::Save +================ +*/ +void hhWeapon::Save( idSaveGame *savefile ) const { + savefile->WriteInt( status ); + savefile->WriteObject( thread ); + savefile->WriteString( state ); + savefile->WriteString( idealState ); + savefile->WriteInt( animBlendFrames ); + savefile->WriteInt( animDoneTime ); + + owner.Save( savefile ); + worldModel.Save( savefile ); + + savefile->WriteStaticObject( physicsObj ); + + savefile->WriteObject( fireController ); + savefile->WriteObject( altFireController ); + + savefile->WriteTrace( eyeTraceInfo ); + savefile->WriteVec3( cameraShakeOffset ); + savefile->WriteInt( handedness ); + + savefile->WriteVec3( pushVelocity ); + + savefile->WriteString( weaponDef->GetName() ); + + savefile->WriteString( icon ); + + savefile->WriteInt( kick_endtime ); + + savefile->WriteBool( lightOn ); + + savefile->WriteInt( zoomFov ); + + savefile->WriteInt( weaponAngleOffsetAverages ); + savefile->WriteFloat( weaponAngleOffsetScale ); + savefile->WriteFloat( weaponAngleOffsetMax ); + savefile->WriteFloat( weaponOffsetTime ); + savefile->WriteFloat( weaponOffsetScale ); + + savefile->WriteBool( nozzleFx ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->WriteInt( nozzleGlowHandle ); + savefile->WriteJoint( nozzleJointHandle.view ); + savefile->WriteJoint( nozzleJointHandle.world ); + savefile->WriteRenderLight( nozzleGlow ); + savefile->WriteVec3( nozzleGlowColor ); + savefile->WriteMaterial( nozzleGlowShader ); + savefile->WriteFloat( nozzleGlowRadius ); + savefile->WriteVec3( nozzleGlowOffset ); +} + +/* +================ +hhWeapon::Restore +================ +*/ +void hhWeapon::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( (int &)status ); + savefile->ReadObject( reinterpret_cast(thread) ); + savefile->ReadString( state ); + savefile->ReadString( idealState ); + savefile->ReadInt( animBlendFrames ); + savefile->ReadInt( animDoneTime ); + + //Re-link script fields + WEAPON_ATTACK.LinkTo( scriptObject, "WEAPON_ATTACK" ); + WEAPON_RELOAD.LinkTo( scriptObject, "WEAPON_RELOAD" ); + WEAPON_RAISEWEAPON.LinkTo( scriptObject, "WEAPON_RAISEWEAPON" ); + WEAPON_LOWERWEAPON.LinkTo( scriptObject, "WEAPON_LOWERWEAPON" ); + WEAPON_NEXTATTACK.LinkTo( scriptObject, "nextAttack" ); //HUMANHEAD rww + + //HUMANHEAD: aob + WEAPON_ALTATTACK.LinkTo( scriptObject, "WEAPON_ALTATTACK" ); + WEAPON_ASIDEWEAPON.LinkTo( scriptObject, "WEAPON_ASIDEWEAPON" ); + WEAPON_ALTMODE.LinkTo( scriptObject, "WEAPON_ALTMODE" ); + //HUMANHEAD END + + owner.Restore( savefile ); + worldModel.Restore( savefile ); + + savefile->ReadStaticObject( physicsObj ); + RestorePhysics( &physicsObj ); + + savefile->ReadObject( reinterpret_cast< idClass *&> ( fireController ) ); + savefile->ReadObject( reinterpret_cast< idClass *&> ( altFireController ) ); + + savefile->ReadTrace( eyeTraceInfo ); + savefile->ReadVec3( cameraShakeOffset ); + savefile->ReadInt( handedness ); + + savefile->ReadVec3( pushVelocity ); + + idStr objectname; + savefile->ReadString( objectname ); + weaponDef = gameLocal.FindEntityDef( objectname, false ); + if (!weaponDef) { + gameLocal.Error( "Unknown weaponDef: %s\n", objectname ); + } + dict = &(weaponDef->dict); + + savefile->ReadString( icon ); + + savefile->ReadInt( kick_endtime ); + + savefile->ReadBool( lightOn ); + + savefile->ReadInt( zoomFov ); + + savefile->ReadInt( weaponAngleOffsetAverages ); + savefile->ReadFloat( weaponAngleOffsetScale ); + savefile->ReadFloat( weaponAngleOffsetMax ); + savefile->ReadFloat( weaponOffsetTime ); + savefile->ReadFloat( weaponOffsetScale ); + + savefile->ReadBool( nozzleFx ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->ReadInt( nozzleGlowHandle ); + savefile->ReadJoint( nozzleJointHandle.view ); + savefile->ReadJoint( nozzleJointHandle.world ); + savefile->ReadRenderLight( nozzleGlow ); + savefile->ReadVec3( nozzleGlowColor ); + savefile->ReadMaterial( nozzleGlowShader ); + savefile->ReadFloat( nozzleGlowRadius ); + savefile->ReadVec3( nozzleGlowOffset ); +} + +/* +================ +hhWeapon::Clear +================ +*/ +void hhWeapon::Clear( void ) { + DeconstructScriptObject(); + scriptObject.Free(); + + WEAPON_ATTACK.Unlink(); + WEAPON_RELOAD.Unlink(); + WEAPON_RAISEWEAPON.Unlink(); + WEAPON_LOWERWEAPON.Unlink(); + WEAPON_NEXTATTACK.Unlink(); //HUMANHEAD rww + + //HUMANHEAD: aob + WEAPON_ALTATTACK.Unlink(); + WEAPON_ASIDEWEAPON.Unlink(); + WEAPON_ALTMODE.Unlink(); + + SAFE_DELETE_PTR( fireController ); + SAFE_DELETE_PTR( altFireController ); + //HUMANEAD END + + //memset( &renderEntity, 0, sizeof( renderEntity ) ); + renderEntity.entityNum = entityNumber; + + renderEntity.noShadow = true; + renderEntity.noSelfShadow = true; + renderEntity.customSkin = NULL; + + // set default shader parms + renderEntity.shaderParms[ SHADERPARM_RED ] = 1.0f; + renderEntity.shaderParms[ SHADERPARM_GREEN ]= 1.0f; + renderEntity.shaderParms[ SHADERPARM_BLUE ] = 1.0f; + renderEntity.shaderParms[3] = 1.0f; + renderEntity.shaderParms[ SHADERPARM_TIMEOFFSET ] = 0.0f; + renderEntity.shaderParms[5] = 0.0f; + renderEntity.shaderParms[6] = 0.0f; + renderEntity.shaderParms[7] = 0.0f; + + // nozzle fx + nozzleFx = false; + memset( &nozzleGlow, 0, sizeof( nozzleGlow ) ); + nozzleGlowHandle = -1; + nozzleJointHandle.Clear(); + + memset( &refSound, 0, sizeof( refSound_t ) ); + if ( owner.IsValid() ) { + // don't spatialize the weapon sounds + refSound.listenerId = owner->GetListenerId(); + } + + // clear out the sounds from our spawnargs since we'll copy them from the weapon def + const idKeyValue *kv = spawnArgs.MatchPrefix( "snd_" ); + while( kv ) { + spawnArgs.Delete( kv->GetKey() ); + kv = spawnArgs.MatchPrefix( "snd_" ); + } + + weaponDef = NULL; + dict = NULL; + + kick_endtime = 0; + + icon = ""; + + pushVelocity.Zero(); + + status = WP_HOLSTERED; + state = ""; + idealState = ""; + animBlendFrames = 0; + animDoneTime = 0; + + lightOn = false; + + zoomFov = 90; + + weaponAngleOffsetAverages = 10; //initialize this to default number to prevent infinite loops + + FreeModelDef(); +} + +/* +================ +hhWeapon::InitWorldModel +================ +*/ +void hhWeapon::InitWorldModel( const idDict *dict ) { + idEntity *ent; + + ent = worldModel.GetEntity(); + if ( !ent || !dict ) { + return; + } + + const char *model = dict->GetString( "model_world" ); + const char *attach = dict->GetString( "joint_attach" ); + + if (gameLocal.isMultiplayer) { //HUMANHEAD rww - shadow default based on whether the player shadows + ent->GetRenderEntity()->noShadow = MP_PLAYERNOSHADOW_DEFAULT; + } + + if ( model[0] && attach[0] ) { + ent->Show(); + ent->SetModel( model ); + ent->GetPhysics()->SetContents( 0 ); + ent->GetPhysics()->SetClipModel( NULL, 1.0f ); + ent->BindToJoint( owner.GetEntity(), attach, true ); + ent->GetPhysics()->SetOrigin( vec3_origin ); + ent->GetPhysics()->SetAxis( mat3_identity ); + + // supress model in player views, but allow it in mirrors and remote views + renderEntity_t *worldModelRenderEntity = ent->GetRenderEntity(); + if ( worldModelRenderEntity ) { + worldModelRenderEntity->suppressSurfaceInViewID = owner->entityNumber+1; + worldModelRenderEntity->suppressShadowInViewID = owner->entityNumber+1; + worldModelRenderEntity->suppressShadowInLightID = LIGHTID_VIEW_MUZZLE_FLASH + owner->entityNumber; + } + } else { + ent->SetModel( "" ); + ent->Hide(); + } +} + +/* +================ +hhWeapon::GetWeaponDef + +HUMANHEAD: aob - this should only be called after the weapon has just been created +================ +*/ +void hhWeapon::GetWeaponDef( const char *objectname ) { + //HUMANHEAD: aob - put logic in helper function + ParseDef( objectname ); + //HUMANHEAD END + + if( owner->inventory.weaponRaised[owner->GetWeaponNum(objectname)] || gameLocal.isMultiplayer ) + ProcessEvent( &EV_Weapon_State, "Raise", 0 ); + else { + ProcessEvent( &EV_Weapon_State, "NewRaise", 0 ); + owner->inventory.weaponRaised[owner->GetWeaponNum(objectname)] = true; + } +} + +/* +================ +hhWeapon::ParseDef + +HUMANHEAD: aob +================ +*/ +void hhWeapon::ParseDef( const char* objectname ) { + if( !objectname || !objectname[ 0 ] ) { + return; + } + + if( !owner.IsValid() ) { + gameLocal.Error( "hhWeapon::ParseDef: no owner" ); + } + + weaponDef = gameLocal.FindEntityDef( objectname, false ); + if (!weaponDef) { + gameLocal.Error( "Unknown weaponDef: %s\n", objectname ); + } + dict = &(weaponDef->dict); + + // setup the world model + InitWorldModel( dict ); + + if (!owner.IsValid() || !owner.GetEntity()) { + gameLocal.Error("NULL owner in hhWeapon::ParseDef!"); + } + + //HUMANHEAD: aob + const idDict* infoDict = gameLocal.FindEntityDefDict( dict->GetString("def_fireInfo"), false ); + if( infoDict ) { + fireController = CreateFireController(); + if( fireController ) { + fireController->Init( infoDict, this, owner.GetEntity() ); + } + } + + infoDict = gameLocal.FindEntityDefDict( dict->GetString("def_altFireInfo"), false ); + if( infoDict ) { + altFireController = CreateAltFireController(); + if( altFireController ) { + altFireController->Init( infoDict, this, owner.GetEntity() ); + } + } + //HUMANHEAD END + + icon = dict->GetString( "inv_icon" ); + + // copy the sounds from the weapon view model def into out spawnargs + const idKeyValue *kv = dict->MatchPrefix( "snd_" ); + while( kv ) { + spawnArgs.Set( kv->GetKey(), kv->GetValue() ); + kv = dict->MatchPrefix( "snd_", kv ); + } + + weaponAngleOffsetAverages = dict->GetInt( "weaponAngleOffsetAverages", "10" ); + weaponAngleOffsetScale = dict->GetFloat( "weaponAngleOffsetScale", "0.1" ); + weaponAngleOffsetMax = dict->GetFloat( "weaponAngleOffsetMax", "10" ); + + weaponOffsetTime = dict->GetFloat( "weaponOffsetTime", "400" ); + weaponOffsetScale = dict->GetFloat( "weaponOffsetScale", "0.002" ); + + zoomFov = dict->GetInt( "zoomFov", "70" ); + + InitScriptObject( dict->GetString("scriptobject") ); + + nozzleFx = weaponDef->dict.GetBool( "nozzleFx", "0" ); + nozzleGlowColor = weaponDef->dict.GetVector("nozzleGlowColor", "1 1 1"); + nozzleGlowRadius = weaponDef->dict.GetFloat("nozzleGlowRadius", "10"); + nozzleGlowOffset = weaponDef->dict.GetVector( "nozzleGlowOffset", "0 0 0" ); + nozzleGlowShader = declManager->FindMaterial( weaponDef->dict.GetString( "mtr_nozzleGlowShader", "" ), false ); + GetJointHandle( weaponDef->dict.GetString( "nozzleJoint", "" ), nozzleJointHandle ); + + //HUMANHEAD bjk + int clipAmmo = owner->inventory.clip[owner->GetWeaponNum(objectname)]; + if ( ( clipAmmo < 0 ) || ( clipAmmo > fireController->ClipSize() ) ) { + // first time using this weapon so have it fully loaded to start + clipAmmo = fireController->ClipSize(); + if ( clipAmmo > fireController->AmmoAvailable() ) { + clipAmmo = fireController->AmmoAvailable(); + } + } + fireController->AddToClip(clipAmmo); + + WEAPON_ALTMODE = owner->inventory.altMode[owner->GetWeaponNum(objectname)]; + //HUMANHEAD END + + Show(); +} + +/* +================ +idWeapon::ShouldConstructScriptObjectAtSpawn + +Called during idEntity::Spawn to see if it should construct the script object or not. +Overridden by subclasses that need to spawn the script object themselves. +================ +*/ +bool hhWeapon::ShouldConstructScriptObjectAtSpawn( void ) const { + return false; +} + +/* +================ +hhWeapon::InitScriptObject +================ +*/ +void hhWeapon::InitScriptObject( const char* objectType ) { + if( !objectType || !objectType[0] ) { + gameLocal.Error( "No scriptobject set on '%s'.", dict->GetString("classname") ); + } + + if( !idStr::Icmp(scriptObject.GetTypeName(), objectType) ) { + //Same script object, don't reload it + return; + } + + // setup script object + if( !scriptObject.SetType(objectType) ) { + gameLocal.Error( "Script object '%s' not found on weapon '%s'.", objectType, dict->GetString("classname") ); + } + + WEAPON_ATTACK.LinkTo( scriptObject, "WEAPON_ATTACK" ); + WEAPON_RELOAD.LinkTo( scriptObject, "WEAPON_RELOAD" ); + WEAPON_RAISEWEAPON.LinkTo( scriptObject, "WEAPON_RAISEWEAPON" ); + WEAPON_LOWERWEAPON.LinkTo( scriptObject, "WEAPON_LOWERWEAPON" ); + WEAPON_NEXTATTACK.LinkTo( scriptObject, "nextAttack" ); //HUMANHEAD rww + + //HUMANHEAD: aob + WEAPON_ALTATTACK.LinkTo( scriptObject, "WEAPON_ALTATTACK" ); + WEAPON_ASIDEWEAPON.LinkTo( scriptObject, "WEAPON_ASIDEWEAPON" ); + WEAPON_ALTMODE.LinkTo( scriptObject, "WEAPON_ALTMODE" ); + //HUMANHEAD END + + // call script object's constructor + ConstructScriptObject(); +} + +/*********************************************************************** + + GUIs + +***********************************************************************/ + +/* +================ +hhWeapon::Icon +================ +*/ +const char *hhWeapon::Icon( void ) const { + return icon; +} + +/* +================ +hhWeapon::UpdateGUI +================ +*/ +void hhWeapon::UpdateGUI() { + if ( !renderEntity.gui[0] ) { + return; + } + + if ( status == WP_HOLSTERED ) { + return; + } + + int ammoamount = AmmoAvailable(); + int altammoamount = AltAmmoAvailable(); + + // show remaining ammo + renderEntity.gui[ 0 ]->SetStateInt( "ammoamount", ammoamount ); + renderEntity.gui[ 0 ]->SetStateInt( "altammoamount", altammoamount ); + renderEntity.gui[ 0 ]->SetStateBool( "ammolow", ammoamount > 0 && ammoamount <= LowAmmo() ); + renderEntity.gui[ 0 ]->SetStateBool( "altammolow", altammoamount > 0 && altammoamount <= LowAltAmmo() ); + renderEntity.gui[ 0 ]->SetStateBool( "ammoempty", ( ammoamount == 0 ) ); + renderEntity.gui[ 0 ]->SetStateBool( "altammoempty", ( altammoamount == 0 ) ); + renderEntity.gui[ 0 ]->SetStateInt( "clipammoAmount", AmmoInClip() ); + renderEntity.gui[ 0 ]->SetStateInt( "clipaltammoAmount", AltAmmoInClip() ); + renderEntity.gui[ 0 ]->StateChanged(gameLocal.time); +} + +/*********************************************************************** + + Model and muzzleflash + +***********************************************************************/ +/* +================ +hhWeapon::GetGlobalJointTransform + +This returns the offset and axis of a weapon bone in world space, suitable for attaching models or lights +================ +*/ +bool hhWeapon::GetJointWorldTransform( const char* jointName, idVec3 &offset, idMat3 &axis ) { + weaponJointHandle_t handle; + + GetJointHandle( jointName, handle ); + return GetJointWorldTransform( handle, offset, axis ); +} + +/* +================ +hhWeapon::GetGlobalJointTransform + +This returns the offset and axis of a weapon bone in world space, suitable for attaching models or lights +================ +*/ +bool hhWeapon::GetJointWorldTransform( const weaponJointHandle_t& handle, idVec3 &offset, idMat3 &axis, bool muzzleOnly ) { + //FIXME: this seems to work but may need revisiting + //FIXME: totally forgot about mirrors and portals. This may not work. + if( (!pm_thirdPerson.GetBool() && owner == gameLocal.GetLocalPlayer()) || (gameLocal.GetLocalPlayer() && gameLocal.GetLocalPlayer()->spectating && gameLocal.GetLocalPlayer()->spectator == owner->entityNumber) || (gameLocal.isServer && !muzzleOnly)) { //rww - mp server should always create projectiles from the viewmodel + // view model + if ( hhAnimatedEntity::GetJointWorldTransform(handle.view, offset, axis) ) { + return true; + } + } else { + // world model + if ( worldModel.IsValid() && worldModel.GetEntity()->GetJointWorldTransform(handle.world, offset, axis) ) { + return true; + } + } + offset = GetOrigin(); + axis = GetAxis(); + return false; +} + +/* +================ +hhWeapon::GetJointHandle + +HUMANHEAD: aob +================ +*/ +void hhWeapon::GetJointHandle( const char* jointName, weaponJointHandle_t& handle ) { + if( dict ) { + handle.view = GetAnimator()->GetJointHandle( jointName ); + } + + if( worldModel.IsValid() ) { + handle.world = worldModel->GetAnimator()->GetJointHandle( jointName ); + } +} + +/* +================ +hhWeapon::SetPushVelocity +================ +*/ +void hhWeapon::SetPushVelocity( const idVec3 &pushVelocity ) { + this->pushVelocity = pushVelocity; +} + + +/*********************************************************************** + + State control/player interface + +***********************************************************************/ + +/* +================ +hhWeapon::Raise +================ +*/ +void hhWeapon::Raise( void ) { + WEAPON_RAISEWEAPON = true; + WEAPON_ASIDEWEAPON = false; +} + +/* +================ +hhWeapon::PutAway +================ +*/ +void hhWeapon::PutAway( void ) { + //hasBloodSplat = false; + WEAPON_LOWERWEAPON = true; + WEAPON_RAISEWEAPON = false; //HUMANHEAD bjk PCF 5-4-06 : fix for weapons being up after death + WEAPON_ASIDEWEAPON = false; + WEAPON_ATTACK = false; + WEAPON_ALTATTACK = false; +} + + +/* +================ +hhWeapon::PutAside +================ +*/ +void hhWeapon::PutAside( void ) { + //HUMANHEAD bjk PCF (4-27-06) - no setting unnecessary weapon state + //WEAPON_LOWERWEAPON = false; + //WEAPON_RAISEWEAPON = false; + WEAPON_ASIDEWEAPON = true; + WEAPON_ATTACK = false; + WEAPON_ALTATTACK = false; +} + +/* +================ +hhWeapon::PutUpright +================ +*/ +void hhWeapon::PutUpright( void ) { + WEAPON_ASIDEWEAPON = false; +} + +/* +================ +hhWeapon::SnapDown + +HUMANHEAD: aob +================ +*/ +void hhWeapon::SnapDown() { + ProcessEvent( &EV_Weapon_State, "Down", 0 ); +} + +/* +================ +hhWeapon::SnapUp + +HUMANHEAD: aob +================ +*/ +void hhWeapon::SnapUp() { + ProcessEvent( &EV_Weapon_State, "Up", 0 ); +} + +/* +================ +hhWeapon::Reload +================ +*/ +void hhWeapon::Reload( void ) { + WEAPON_RELOAD = true; +} + +/* +================ +hhWeapon::HideWeapon +================ +*/ +void hhWeapon::HideWeapon( void ) { + Hide(); + if ( worldModel.GetEntity() ) { + worldModel.GetEntity()->Hide(); + } +} + +/* +================ +hhWeapon::ShowWeapon +================ +*/ +void hhWeapon::ShowWeapon( void ) { + Show(); + if ( worldModel.GetEntity() ) { + worldModel.GetEntity()->Show(); + } +} + +/* +================ +hhWeapon::HideWorldModel +================ +*/ +void hhWeapon::HideWorldModel( void ) { + if ( worldModel.GetEntity() ) { + worldModel.GetEntity()->Hide(); + } +} + +/* +================ +hhWeapon::ShowWorldModel +================ +*/ +void hhWeapon::ShowWorldModel( void ) { + if ( worldModel.GetEntity() ) { + worldModel.GetEntity()->Show(); + } +} + +/* +================ +hhWeapon::OwnerDied +================ +*/ +void hhWeapon::OwnerDied( void ) { + Hide(); + if ( worldModel.GetEntity() ) { + worldModel.GetEntity()->Hide(); + } +} + +/* +================ +hhWeapon::BeginAltAttack +================ +*/ +void hhWeapon::BeginAltAttack( void ) { + WEAPON_ALTATTACK = true; + + //if ( status != WP_OUTOFAMMO ) { + // lastAttack = gameLocal.time; + //} +} + +/* +================ +hhWeapon::EndAltAttack +================ +*/ +void hhWeapon::EndAltAttack( void ) { + WEAPON_ALTATTACK = false; + +} + +/* +================ +hhWeapon::BeginAttack +================ +*/ +void hhWeapon::BeginAttack( void ) { + //if ( status != WP_OUTOFAMMO ) { + // lastAttack = gameLocal.time; + //} + + WEAPON_ATTACK = true; +} + +/* +================ +hhWeapon::EndAttack +================ +*/ +void hhWeapon::EndAttack( void ) { + if ( !WEAPON_ATTACK.IsLinked() ) { + return; + } + if ( WEAPON_ATTACK ) { + WEAPON_ATTACK = false; + } +} + +/* +================ +hhWeapon::isReady +================ +*/ +bool hhWeapon::IsReady( void ) const { + return !IsHidden() && ( ( status == WP_READY ) || ( status == WP_OUTOFAMMO ) ); +} + +/* +================ +hhWeapon::IsReloading +================ +*/ +bool hhWeapon::IsReloading( void ) const { + return ( status == WP_RELOAD ); +} + +/* +================ +hhWeapon::IsChangable +================ +*/ +bool hhWeapon::IsChangable( void ) const { + //HUMANHEAD: aob - same as IsReady except w/o IsHidden check + //Allows us to switch weapons while they are hidden + //Hope this doesn't fuck to many things + return ( ( status == WP_READY ) || ( status == WP_OUTOFAMMO ) ); +} + +/* +================ +hhWeapon::IsHolstered +================ +*/ +bool hhWeapon::IsHolstered( void ) const { + return ( status == WP_HOLSTERED ); +} + + +/* +================ +hhWeapon::IsLowered +================ +*/ +bool hhWeapon::IsLowered( void ) const { + return ( status == WP_HOLSTERED ) || ( status == WP_LOWERING ); +} + + +/* +================ +hhWeapon::IsRising +================ +*/ +bool hhWeapon::IsRising( void ) const { + return ( status == WP_RISING ); +} + + +/* +================ +hhWeapon::IsAside +================ +*/ +bool hhWeapon::IsAside( void ) const { + return ( status == WP_ASIDE ); +} + + +/* +================ +hhWeapon::ShowCrosshair +================ +*/ +bool hhWeapon::ShowCrosshair( void ) const { + //HUMANHEAD: aob - added cinematic check + return !( state == idStr( WP_RISING ) || state == idStr( WP_LOWERING ) || state == idStr( WP_HOLSTERED ) ) && !owner->InCinematic(); +} + +/* +===================== +hhWeapon::DropItem +===================== +*/ +idEntity* hhWeapon::DropItem( const idVec3 &velocity, int activateDelay, int removeDelay, bool died ) { + if ( !weaponDef || !worldModel.GetEntity() ) { + return NULL; + } + + const char *classname = weaponDef->dict.GetString( "def_dropItem" ); + if ( !classname[0] ) { + return NULL; + } + StopSound( SND_CHANNEL_BODY, !died ); //HUMANHEAD rww - on death, do not broadcast the stop, because the weapon itself is about to be removed + return idMoveableItem::DropItem( classname, worldModel.GetEntity()->GetPhysics()->GetOrigin(), worldModel.GetEntity()->GetPhysics()->GetAxis(), velocity, activateDelay, removeDelay ); +} + +/* +===================== +hhWeapon::CanDrop +===================== +*/ +bool hhWeapon::CanDrop( void ) const { + if ( !weaponDef || !worldModel.GetEntity() ) { + return false; + } + const char *classname = weaponDef->dict.GetString( "def_dropItem" ); + if ( !classname[ 0 ] ) { + return false; + } + return true; +} + +/*********************************************************************** + + Script state management + +***********************************************************************/ + +/* +===================== +hhWeapon::SetState +===================== +*/ +void hhWeapon::SetState( const char *statename, int blendFrames ) { + const function_t *func; + + func = scriptObject.GetFunction( statename ); + if ( !func ) { + gameLocal.Error( "Can't find function '%s' in object '%s'", statename, scriptObject.GetTypeName() ); + } + + thread->CallFunction( this, func, true ); + state = statename; + + animBlendFrames = blendFrames; + if ( g_debugWeapon.GetBool() ) { + gameLocal.Printf( "%d: weapon state : %s\n", gameLocal.time, statename ); + } + + idealState = ""; +} + +/*********************************************************************** + + Visual presentation + +***********************************************************************/ + +/* +================ +hhWeapon::MuzzleRise + +HUMANHEAD: aob +================ +*/ +void hhWeapon::MuzzleRise( idVec3 &origin, idMat3 &axis ) { + if( fireController ) { + fireController->CalculateMuzzleRise( origin, axis ); + } + + if( altFireController ) { + altFireController->CalculateMuzzleRise( origin, axis ); + } +} + +/* +================ +hhWeapon::ConstructScriptObject + +Called during idEntity::Spawn. Calls the constructor on the script object. +Can be overridden by subclasses when a thread doesn't need to be allocated. +================ +*/ +idThread *hhWeapon::ConstructScriptObject( void ) { + const function_t *constructor; + + //HUMANHEAD: aob + if( !thread ) { + return thread; + } + //HUMANHEAD END + + thread->EndThread(); + + // call script object's constructor + constructor = scriptObject.GetConstructor(); + if ( !constructor ) { + gameLocal.Error( "Missing constructor on '%s' for weapon", scriptObject.GetTypeName() ); + } + + // init the script object's data + scriptObject.ClearObject(); + thread->CallFunction( this, constructor, true ); + thread->Execute(); + + return thread; +} + +/* +================ +hhWeapon::DeconstructScriptObject + +Called during idEntity::~idEntity. Calls the destructor on the script object. +Can be overridden by subclasses when a thread doesn't need to be allocated. +Not called during idGameLocal::MapShutdown. +================ +*/ +void hhWeapon::DeconstructScriptObject( void ) { + const function_t *destructor; + + if ( !thread ) { + return; + } + + // don't bother calling the script object's destructor on map shutdown + if ( gameLocal.GameState() == GAMESTATE_SHUTDOWN ) { + return; + } + + thread->EndThread(); + + // call script object's destructor + destructor = scriptObject.GetDestructor(); + if ( destructor ) { + // start a thread that will run immediately and end + thread->CallFunction( this, destructor, true ); + thread->Execute(); + thread->EndThread(); + } + + // clear out the object's memory + scriptObject.ClearObject(); +} + +/* +================ +hhWeapon::UpdateScript +================ +*/ +void hhWeapon::UpdateScript( void ) { + int count; + + if ( !IsLinked() ) { + return; + } + + // only update the script on new frames + if ( !gameLocal.isNewFrame ) { + return; + } + + if ( idealState.Length() ) { + SetState( idealState, animBlendFrames ); + } + + // update script state, which may call Event_LaunchProjectiles, among other things + count = 10; + while( ( thread->Execute() || idealState.Length() ) && count-- ) { + // happens for weapons with no clip (like grenades) + if ( idealState.Length() ) { + SetState( idealState, animBlendFrames ); + } + } + + WEAPON_RELOAD = false; +} + +/* +================ +hhWeapon::PresentWeapon +================ +*/ +void hhWeapon::PresentWeapon( bool showViewModel ) { + UpdateScript(); + + //HUMANHEAD rww - added this gui owner logic here + bool allowGuiUpdate = true; + if ( owner.IsValid() && gameLocal.localClientNum != owner->entityNumber ) { + // if updating the hud for a followed client + if ( gameLocal.localClientNum >= 0 && gameLocal.entities[ gameLocal.localClientNum ] && gameLocal.entities[ gameLocal.localClientNum ]->IsType( idPlayer::Type ) ) { + idPlayer *p = static_cast< idPlayer * >( gameLocal.entities[ gameLocal.localClientNum ] ); + if ( !p->spectating || p->spectator != owner->entityNumber ) { + allowGuiUpdate = false; + } + } else { + allowGuiUpdate = false; + } + } + if (allowGuiUpdate) { + UpdateGUI(); + } + + // update animation + UpdateAnimation(); + + // only show the surface in player view + renderEntity.allowSurfaceInViewID = owner->entityNumber+1; + + // crunch the depth range so it never pokes into walls this breaks the machine gun gui + renderEntity.weaponDepthHack = true; + + // present the model + if ( showViewModel ) { + Present(); + } else { + FreeModelDef(); + } + + if ( worldModel.GetEntity() && worldModel.GetEntity()->GetRenderEntity() ) { + // deal with the third-person visible world model + // don't show shadows of the world model in first person +#ifdef HUMANHEAD + if ( gameLocal.isMultiplayer || (g_showPlayerShadow.GetBool() && pm_modelView.GetInteger() == 1) || pm_thirdPerson.GetBool() + || (pm_modelView.GetInteger() == 2 && owner->health <= 0)) { + worldModel->GetRenderEntity()->suppressShadowInViewID = 0; + } else { + worldModel->GetRenderEntity()->suppressShadowInViewID = owner->entityNumber+1; + worldModel->GetRenderEntity()->suppressShadowInLightID = LIGHTID_VIEW_MUZZLE_FLASH + owner->entityNumber; + } +#else + + // deal with the third-person visible world model + // don't show shadows of the world model in first person + if ( gameLocal.isMultiplayer || g_showPlayerShadow.GetBool() || pm_thirdPerson.GetBool() ) { + worldModel.GetEntity()->GetRenderEntity()->suppressShadowInViewID = 0; + } else { + worldModel.GetEntity()->GetRenderEntity()->suppressShadowInViewID = owner->entityNumber+1; + worldModel.GetEntity()->GetRenderEntity()->suppressShadowInLightID = LIGHTID_VIEW_MUZZLE_FLASH + owner->entityNumber; + } + } +#endif // HUMANHEAD pdm + } + + // update the muzzle flash light, so it moves with the gun + //HUMANHEAD: aob + if( fireController ) { + fireController->UpdateMuzzleFlash(); + } + + if( altFireController ) { + altFireController->UpdateMuzzleFlash(); + } + //HUMANHEAD END + + UpdateNozzleFx(); // expensive + + UpdateSound(); +} + + +/* +================ +hhWeapon::EnterCinematic +================ +*/ +void hhWeapon::EnterCinematic( void ) { + StopSound( SND_CHANNEL_ANY, false ); + + if ( IsLinked() ) { + SetState( "EnterCinematic", 0 ); + thread->Execute(); + + WEAPON_ATTACK = false; + WEAPON_RELOAD = false; +// WEAPON_NETRELOAD = false; +// WEAPON_NETENDRELOAD = false; + WEAPON_RAISEWEAPON = false; + WEAPON_LOWERWEAPON = false; + } + + //disabled = true; + + LowerWeapon(); +} + +/* +================ +hhWeapon::ExitCinematic +================ +*/ +void hhWeapon::ExitCinematic( void ) { + //disabled = false; + + if ( IsLinked() ) { + SetState( "ExitCinematic", 0 ); + thread->Execute(); + } + + RaiseWeapon(); +} + +/* +================ +hhWeapon::NetCatchup +================ +*/ +void hhWeapon::NetCatchup( void ) { + if ( IsLinked() ) { + SetState( "NetCatchup", 0 ); + thread->Execute(); + } +} + +/* +================ +hhWeapon::GetClipBits +================ +*/ +int hhWeapon::GetClipBits(void) const { + return ASYNC_PLAYER_INV_CLIP_BITS; +} + +/* +================ +hhWeapon::GetZoomFov +================ +*/ +int hhWeapon::GetZoomFov( void ) { + return zoomFov; +} + +/* +================ +hhWeapon::GetWeaponAngleOffsets +================ +*/ +void hhWeapon::GetWeaponAngleOffsets( int *average, float *scale, float *max ) { + *average = weaponAngleOffsetAverages; + *scale = weaponAngleOffsetScale; + *max = weaponAngleOffsetMax; +} + +/* +================ +hhWeapon::GetWeaponTimeOffsets +================ +*/ +void hhWeapon::GetWeaponTimeOffsets( float *time, float *scale ) { + *time = weaponOffsetTime; + *scale = weaponOffsetScale; +} + + +/*********************************************************************** + + Ammo + +**********************************************************************/ + +/* +================ +hhWeapon::WriteToSnapshot +================ +*/ +void hhWeapon::WriteToSnapshot( idBitMsgDelta &msg ) const { +#if 0 + //FIXME: need to add altFire stuff too + if( fireController ) { + msg.WriteBits( fireController->AmmoInClip(), GetClipBits() ); + } else { + msg.WriteBits( 0, GetClipBits() ); + } + msg.WriteBits(worldModel.GetSpawnId(), 32); +#else //rww - forget this silliness, let's do this the Right Way. + msg.WriteBits(owner.GetSpawnId(), 32); + msg.WriteBits(worldModel.GetSpawnId(), 32); + + if (fireController) { + fireController->WriteToSnapshot(msg); + } + else { //rwwFIXME this is an ugly hack + msg.WriteBits(0, 32); + msg.WriteBits(0, 32); + msg.WriteBits(0, GetClipBits()); + } + if (altFireController) { + altFireController->WriteToSnapshot(msg); + } + else { //rwwFIXME this is an ugly hack + msg.WriteBits(0, 32); + msg.WriteBits(0, 32); + msg.WriteBits(0, GetClipBits()); + } + + msg.WriteBits(WEAPON_ASIDEWEAPON, 1); + //HUMANHEAD PCF rww 05/09/06 - no need to sync these values + /* + msg.WriteBits(WEAPON_LOWERWEAPON, 1); + msg.WriteBits(WEAPON_RAISEWEAPON, 1); + */ + + WriteBindToSnapshot(msg); +#endif +} + +/* +================ +hhWeapon::ReadFromSnapshot +================ +*/ +void hhWeapon::ReadFromSnapshot( const idBitMsgDelta &msg ) { +#if 0 + //FIXME: need to add altFire stuff too + if( fireController ) { + fireController->AddToClip( msg.ReadBits(GetClipBits()) ); + } else { + msg.ReadBits( GetClipBits() ); + } + + worldModel.SetSpawnId(msg.ReadBits(32)); +#else + bool wmInit = false; + + owner.SetSpawnId(msg.ReadBits(32)); + + if (worldModel.SetSpawnId(msg.ReadBits(32))) { //do this once we finally get the entity in the snapshot + wmInit = true; + } + + //rwwFIXME this is a little hacky, i think. is there a way to do it in ::Spawn? but, the weapon doesn't know its owner at that point. + if (!dict) { + if (!owner.IsValid() || !owner.GetEntity()) { + gameLocal.Error("NULL owner in hhWeapon::ReadFromSnapshot!"); + } + + assert(owner.IsValid()); + + GetWeaponDef(spawnArgs.GetString("classname")); + //owner->ForceWeapon(this); it doesn't like this. + } + assert(fireController); + assert(altFireController); + + if (wmInit) { //need to do this once the ent is valid on the client + InitWorldModel( dict ); + GetJointHandle( weaponDef->dict.GetString( "nozzleJoint", "" ), nozzleJointHandle ); + fireController->UpdateWeaponJoints(); + altFireController->UpdateWeaponJoints(); + } + + fireController->ReadFromSnapshot(msg); + altFireController->ReadFromSnapshot(msg); + + WEAPON_ASIDEWEAPON = !!msg.ReadBits(1); + //HUMANHEAD PCF rww 05/09/06 - no need to sync these values + /* + WEAPON_LOWERWEAPON = !!msg.ReadBits(1); + WEAPON_RAISEWEAPON = !!msg.ReadBits(1); + */ + + ReadBindFromSnapshot(msg); +#endif +} + +/* +================ +hhWeapon::ClientPredictionThink +================ +*/ +void hhWeapon::ClientPredictionThink( void ) { + Think(); +} + +/*********************************************************************** + + Script events + +***********************************************************************/ + +/* +hhWeapon::Event_Raise +*/ +void hhWeapon::Event_Raise() { + Raise(); +} + + +/* +=============== +hhWeapon::Event_WeaponState +=============== +*/ +void hhWeapon::Event_WeaponState( const char *statename, int blendFrames ) { + const function_t *func; + + func = scriptObject.GetFunction( statename ); + if ( !func ) { + gameLocal.Error( "Can't find function '%s' in object '%s'", statename, scriptObject.GetTypeName() ); + } + + idealState = statename; + animBlendFrames = blendFrames; + thread->DoneProcessing(); +} + +/* +=============== +hhWeapon::Event_WeaponReady +=============== +*/ +void hhWeapon::Event_WeaponReady( void ) { + status = WP_READY; + WEAPON_RAISEWEAPON = false; + + //HUMANHEAD bjk PCF (4-27-06) - no setting unnecessary weapon state + //WEAPON_LOWERWEAPON = false; + //WEAPON_ASIDEWEAPON = false; +} + +/* +=============== +hhWeapon::Event_WeaponOutOfAmmo +=============== +*/ +void hhWeapon::Event_WeaponOutOfAmmo( void ) { + status = WP_OUTOFAMMO; + WEAPON_RAISEWEAPON = false; + + //HUMANHEAD bjk PCF (4-27-06) - no setting unnecessary weapon state + //WEAPON_LOWERWEAPON = false; + //WEAPON_ASIDEWEAPON = false; +} + +/* +=============== +hhWeapon::Event_WeaponReloading +=============== +*/ +void hhWeapon::Event_WeaponReloading( void ) { + status = WP_RELOAD; +} + +/* +=============== +hhWeapon::Event_WeaponHolstered +=============== +*/ +void hhWeapon::Event_WeaponHolstered( void ) { + status = WP_HOLSTERED; + WEAPON_LOWERWEAPON = false; +} + +/* +=============== +hhWeapon::Event_WeaponRising +=============== +*/ +void hhWeapon::Event_WeaponRising( void ) { + status = WP_RISING; + WEAPON_LOWERWEAPON = false; + + //HUMANHEAD bjk PCF (4-27-06) - no setting unnecessary weapon state + //WEAPON_ASIDEWEAPON = false; + + owner->WeaponRisingCallback(); +} + + +/* +=============== +hhWeapon::Event_WeaponAside +=============== +*/ +void hhWeapon::Event_Weapon_Aside( void ) { + status = WP_ASIDE; +} + +/* +=============== +hhWeapon::Event_Weapon_PuttingAside +=============== +*/ +void hhWeapon::Event_Weapon_PuttingAside( void ) { + status = WP_PUTTING_ASIDE; +} + +/* +=============== +hhWeapon::Event_Weapon_Uprighting +=============== +*/ +void hhWeapon::Event_Weapon_Uprighting( void ) { + status = WP_UPRIGHTING; +} + +/* +=============== +hhWeapon::Event_WeaponLowering +=============== +*/ +void hhWeapon::Event_WeaponLowering( void ) { + status = WP_LOWERING; + WEAPON_RAISEWEAPON = false; + + //HUMANHEAD: aob + WEAPON_ASIDEWEAPON = false; + //HUMANHEAD END + + owner->WeaponLoweringCallback(); +} + +/* +=============== +hhWeapon::Event_AddToClip +=============== +*/ +void hhWeapon::Event_AddToClip( int amount ) { + if( fireController ) { + fireController->AddToClip( amount ); + } +} + +/* +=============== +hhWeapon::Event_AmmoInClip +=============== +*/ +void hhWeapon::Event_AmmoInClip( void ) { + int ammo = AmmoInClip(); + idThread::ReturnFloat( ammo ); +} + +/* +=============== +hhWeapon::Event_AmmoAvailable +=============== +*/ +void hhWeapon::Event_AmmoAvailable( void ) { + int ammoAvail = AmmoAvailable(); + idThread::ReturnFloat( ammoAvail ); +} + +/* +=============== +hhWeapon::Event_ClipSize +=============== +*/ +void hhWeapon::Event_ClipSize( void ) { + idThread::ReturnFloat( ClipSize() ); +} + +/* +=============== +hhWeapon::Event_PlayAnim +=============== +*/ +void hhWeapon::Event_PlayAnim( int channel, const char *animname ) { + int anim = 0; + + anim = GetAnimator()->GetAnim( animname ); + if ( !anim ) { + //HUMANHEAD: aob + WEAPON_DEBUG( "missing '%s' animation on '%s'", animname, name.c_str() ); + //HUMANHEAD END + GetAnimator()->Clear( channel, gameLocal.GetTime(), FRAME2MS( animBlendFrames ) ); + animDoneTime = 0; + } else { + //Show(); AOB - removed so we can use hidden weapons Hope this doesn't fuck to many things + GetAnimator()->PlayAnim( channel, anim, gameLocal.GetTime(), FRAME2MS( animBlendFrames ) ); + animDoneTime = GetAnimator()->CurrentAnim( channel )->GetEndTime(); + if ( worldModel.GetEntity() ) { + anim = worldModel.GetEntity()->GetAnimator()->GetAnim( animname ); + if ( anim ) { + worldModel.GetEntity()->GetAnimator()->PlayAnim( channel, anim, gameLocal.GetTime(), FRAME2MS( animBlendFrames ) ); + } + } + } + animBlendFrames = 0; +} + +/* +=============== +hhWeapon::Event_AnimDone +=============== +*/ +void hhWeapon::Event_AnimDone( int channel, int blendFrames ) { + if ( animDoneTime - FRAME2MS( blendFrames ) <= gameLocal.time ) { + idThread::ReturnInt( true ); + } else { + idThread::ReturnInt( false ); + } +} + +/* +================ +hhWeapon::Event_Next +================ +*/ +void hhWeapon::Event_Next( void ) { + // change to another weapon if possible + owner->NextBestWeapon(); +} + +/* +================ +hhWeapon::Event_Flashlight +================ +*/ +void hhWeapon::Event_Flashlight( int enable ) { +/* + if ( enable ) { + lightOn = true; + MuzzleFlashLight(); + } else { + lightOn = false; + muzzleFlashEnd = 0; + } +*/ +} +/* +================ +hhWeapon::Event_FireProjectiles + +HUMANHEAD: aob +================ +*/ +void hhWeapon::Event_FireProjectiles() { + //HUMANHEAD: aob - moved logic to this helper function + LaunchProjectiles( fireController ); + //HUMANHEAD END +} + +/* +================ +hhWeapon::Event_GetOwner +================ +*/ +void hhWeapon::Event_GetOwner( void ) { + idThread::ReturnEntity( owner.GetEntity() ); +} + +/* +=============== +hhWeapon::Event_UseAmmo +=============== +*/ +void hhWeapon::Event_UseAmmo( int amount ) { + UseAmmo(); +} + +/* +=============== +hhWeapon::Event_UseAltAmmo +=============== +*/ +void hhWeapon::Event_UseAltAmmo( int amount ) { + UseAltAmmo(); +} + +/* +================ +hhWeapon::RestoreGUI + +HUMANHEAD: aob +================ +*/ +void hhWeapon::RestoreGUI( const char* guiKey, idUserInterface** gui ) { + if( guiKey && guiKey[0] && gui ) { + AddRenderGui( dict->GetString(guiKey), gui, dict ); + } +} + +/* +================ +hhWeapon::Think +================ +*/ +void hhWeapon::Think() { + if( thinkFlags & TH_TICKER ) { + if (owner.IsValid()) { + Ticker(); + } + } +} + +/* +================ +hhWeapon::SpawnWorldModel +================ +*/ +idEntity* hhWeapon::SpawnWorldModel( const char* worldModelDict, idActor* _owner ) { +/* + assert( _owner && worldModelDict ); + + idEntity* pEntity = NULL; + idDict* pArgs = declManager->FindEntityDefDict( worldModelDict, false ); + if( !pArgs ) { return NULL; } + + idStr ownerWeaponBindBone = _owner->spawnArgs.GetString( "bone_weapon_bind" ); + idStr attachBone = pArgs->GetString( "attach" ); + if( attachBone.Length() && ownerWeaponBindBone.Length() ) { + pEntity = gameLocal.SpawnEntityType( idEntity::Type, pArgs ); + HH_ASSERT( pEntity ); + + pEntity->GetPhysics()->SetContents( 0 ); + pEntity->GetPhysics()->DisableClip(); + pEntity->MoveJointToJoint( attachBone.c_str(), _owner, ownerWeaponBindBone.c_str() ); + pEntity->AlignJointToJoint( attachBone.c_str(), _owner, ownerWeaponBindBone.c_str() ); + pEntity->BindToJoint( _owner, ownerWeaponBindBone.c_str(), true ); + + // supress model in player views and in any views on the weapon itself + pEntity->renderEntity.suppressSurfaceInViewID = _owner->entityNumber + 1; + } +*/ + return NULL; +} + +/* +================ +hhWeapon::SetModel +================ +*/ +void hhWeapon::SetModel( const char *modelname ) { + assert( modelname ); + + if ( modelDefHandle >= 0 ) { + gameRenderWorld->RemoveDecals( modelDefHandle ); + } + + hhAnimatedEntity::SetModel( modelname ); + + // hide the model until an animation is played + Hide(); +} + +/* +================ +hhWeapon::FreeModelDef +================ +*/ +void hhWeapon::FreeModelDef() { + hhAnimatedEntity::FreeModelDef(); +} + +/* +================ +hhWeapon::GetPhysicsToVisualTransform +================ +*/ +bool hhWeapon::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + bool bResult = hhAnimatedEntity::GetPhysicsToVisualTransform( origin, axis ); + + assert(!FLOAT_IS_NAN(cameraShakeOffset.x)); + assert(!FLOAT_IS_NAN(cameraShakeOffset.y)); + assert(!FLOAT_IS_NAN(cameraShakeOffset.z)); + + if( !cameraShakeOffset.Compare(vec3_zero, VECTOR_EPSILON) ) { + origin = (bResult) ? cameraShakeOffset + origin : cameraShakeOffset; + origin *= GetPhysics()->GetAxis().Transpose(); + if( !bResult ) { axis = mat3_identity; } + return true; + } + + return bResult; +} + +/* +================ +hhWeapon::GetMasterDefaultPosition +================ +*/ +void hhWeapon::GetMasterDefaultPosition( idVec3 &masterOrigin, idMat3 &masterAxis ) const { + idActor* actor = NULL; + idEntity* master = GetBindMaster(); + + if( master ) { + if( master->IsType(idActor::Type) ) { + actor = static_cast( master ); + actor->DetermineOwnerPosition( masterOrigin, masterAxis ); + + masterOrigin = actor->ApplyLandDeflect( masterOrigin, 1.1f ); + } else { + hhAnimatedEntity::GetMasterDefaultPosition( masterOrigin, masterAxis ); + } + } +} + +/* +================ +hhWeapon::SetShaderParm +================ +*/ +void hhWeapon::SetShaderParm( int parmnum, float value ) { + hhAnimatedEntity::SetShaderParm(parmnum, value); + if ( worldModel.IsValid() ) { + worldModel->SetShaderParm(parmnum, value); + } +} + +/* +================ +hhWeapon::SetSkin +================ +*/ +void hhWeapon::SetSkin( const idDeclSkin *skin ) { + hhAnimatedEntity::SetSkin(skin); + if ( worldModel.IsValid() ) { + worldModel->SetSkin(skin); + } +} + +/* +================ +hhWeapon::PlayCycle +================ +*/ +void hhWeapon::Event_PlayCycle( int channel, const char *animname ) { + int anim; + + anim = GetAnimator()->GetAnim( animname ); + if ( !anim ) { + //HUMANHEAD: aob + WEAPON_DEBUG( "missing '%s' animation on '%s'", animname, name.c_str() ); + //HUMANHEAD END + GetAnimator()->Clear( channel, gameLocal.GetTime(), FRAME2MS( animBlendFrames ) ); + animDoneTime = 0; + } else { + //Show();AOB - removed so we can use hidden weapons Hope this doesn't fuck to many things + + // NLANOTE - Used to be CARandom + GetAnimator()->CycleAnim( channel, anim, gameLocal.GetTime(), FRAME2MS( animBlendFrames ) ); + animDoneTime = GetAnimator()->CurrentAnim( channel )->GetEndTime(); + if ( worldModel.GetEntity() ) { + anim = worldModel.GetEntity()->GetAnimator()->GetAnim( animname ); + // NLANOTE - Used to be CARandom + worldModel.GetEntity()->GetAnimator()->CycleAnim( channel, anim, gameLocal.GetTime(), FRAME2MS( animBlendFrames ) ); + } + } + animBlendFrames = 0; +} + +/* +================ +hhWeapon::Event_IsAnimPlaying +================ +*/ +void hhWeapon::Event_IsAnimPlaying( const char *animname ) { + idThread::ReturnInt( animator.IsAnimPlaying(animname) ); +} + + +/* +================ +hhWeapon::Event_EjectBrass +================ +*/ +void hhWeapon::Event_EjectBrass() { + if( fireController ) { + fireController->EjectBrass(); + } +} + +/* +================ +hhWeapon::Event_EjectAltBrass +================ +*/ +void hhWeapon::Event_EjectAltBrass() { + if( altFireController ) { + altFireController->EjectBrass(); + } +} + +/* +================ +hhWeapon::Event_PlayAnimWhenReady +================ +*/ +void hhWeapon::Event_PlayAnimWhenReady( const char* animName ) { +/* + if( !IsReady() && (!dict || !dict->GetBool("pickupHasRaise") || !IsRaising()) ) { + CancelEvents( &EV_PlayAnimWhenReady ); + PostEventSec( &EV_PlayAnimWhenReady, 0.5f, animName ); + return; + } + + PlayAnim( animName, 0, &EV_Weapon_Ready ); +*/ +} + +/* +================ +hhWeapon::Event_SpawnFXAlongBone +================ +*/ +void hhWeapon::Event_SpawnFXAlongBone( idList* fxParms ) { + if ( !owner->CanShowWeaponViewmodel() || !fxParms ) { + return; + } + + HH_ASSERT( fxParms->Num() == 2 ); + + hhFxInfo fxInfo; + fxInfo.UseWeaponDepthHack( true ); + BroadcastFxInfoAlongBone( dict->GetString((*fxParms)[0].c_str()), (*fxParms)[1].c_str(), &fxInfo, NULL, false ); //rww - default to not broadcast from events +} + +/* +============================== +hhWeapon::Event_FireAltProjectiles +============================== +*/ +void hhWeapon::Event_FireAltProjectiles() { + LaunchProjectiles( altFireController ); +} + +// This is called once per frame to precompute where the weapon is pointing. This is used for determining if crosshairs should display as targetted as +// well as fire controllers to update themselves. This must be called before UpdateCrosshairs(). +void hhWeapon::PrecomputeTraceInfo() { + // This is needed for fireControllers, even if there are no crosshairs + idVec3 eyePos = owner->GetEyePosition(); + idMat3 weaponAxis = GetAxis(); + float traceDist = 1024.0f; // was CM_MAX_TRACE_DIST + + // Perform eye trace + gameLocal.clip.TracePoint( eyeTraceInfo, eyePos, eyePos + weaponAxis[0] * traceDist, + MASK_SHOT_BOUNDINGBOX | CONTENTS_GAME_PORTAL, owner.GetEntity() ); + + // CJR: If the trace hit a portal, then force it to trace the max distance, as if it's tracing through the portal + if ( eyeTraceInfo.fraction < 1.0f ) { + idEntity *ent = gameLocal.GetTraceEntity( eyeTraceInfo ); + if ( ent->IsType( hhPortal::Type ) ) { + eyeTraceInfo.endpos = eyePos + weaponAxis[0] * traceDist; + eyeTraceInfo.fraction = 1.0f; + } + } + +} + +/* +============================== +hhWeapon::UpdateCrosshairs +============================== +*/ +void hhWeapon::UpdateCrosshairs( bool &crosshair, bool &targeting ) { + idEntity* ent = NULL; + trace_t traceInfo; + + crosshair = false; + targeting = false; + if (spawnArgs.GetBool("altModeWeapon")) { + if (WEAPON_ALTMODE) { // Moded weapon in alt-mode + if (altFireController) { + crosshair = altFireController->UsesCrosshair(); + } + } + else { // Moded weapon in normal mode + if (fireController) { + crosshair = fireController->UsesCrosshair(); + } + } + } + else { // Normal non-moded weapon + if (altFireController && altFireController->UsesCrosshair()) { + crosshair = true; + } + if (fireController && fireController->UsesCrosshair()) { + crosshair = true; + } + } + + ent = NULL; + if( crosshair ) { + traceInfo = GetEyeTraceInfo(); + if( traceInfo.fraction < 1.0f ) { + ent = gameLocal.GetTraceEntity(traceInfo); + } + targeting = ( ent && ent->fl.takedamage && !(ent->IsType( hhDoor::Type ) || ent->IsType( hhModelDoor::Type ) || ent->IsType( hhConsole::Type ) ) ); + } +} + +/* +============================== +hhWeapon::GetAmmoType +============================== +*/ +ammo_t hhWeapon::GetAmmoType( void ) const { + return (fireController) ? fireController->GetAmmoType() : 0; +} + +/* +============================== +hhWeapon::AmmoAvailable +============================== +*/ +int hhWeapon::AmmoAvailable( void ) const { + return (fireController) ? fireController->AmmoAvailable() : 0; +} + +/* +============================== +hhWeapon::AmmoInClip +============================== +*/ +int hhWeapon::AmmoInClip( void ) const { + return (fireController) ? fireController->AmmoInClip() : 0; +} + +/* +============================== +hhWeapon::ClipSize +============================== +*/ +int hhWeapon::ClipSize( void ) const { + return (fireController) ? fireController->ClipSize() : 0; +} + +/* +============================== +hhWeapon::AmmoRequired +============================== +*/ +int hhWeapon::AmmoRequired( void ) const { + return (fireController) ? fireController->AmmoRequired() : 0; +} + +/* +============================== +hhWeapon::LowAmmo +============================== +*/ +int hhWeapon::LowAmmo() { + return (fireController) ? fireController->LowAmmo() : 0; +} + +/* +============================== +hhWeapon::GetAltAmmoType +============================== +*/ +ammo_t hhWeapon::GetAltAmmoType( void ) const { + return (altFireController) ? altFireController->GetAmmoType() : 0; +} + +/* +============================== +hhWeapon::AltAmmoAvailable +============================== +*/ +int hhWeapon::AltAmmoAvailable( void ) const { + return (altFireController) ? altFireController->AmmoAvailable() : 0; +} + +/* +============================== +hhWeapon::AltAmmoInClip +============================== +*/ +int hhWeapon::AltAmmoInClip( void ) const { + return (altFireController) ? altFireController->AmmoInClip() : 0; +} + +/* +============================== +hhWeapon::AltClipSize +============================== +*/ +int hhWeapon::AltClipSize( void ) const { + return (altFireController) ? altFireController->ClipSize() : 0; +} + +/* +============================== +hhWeapon::AltAmmoRequired +============================== +*/ +int hhWeapon::AltAmmoRequired( void ) const { + return (altFireController) ? altFireController->AmmoRequired() : 0; +} + +/* +============================== +hhWeapon::LowAltAmmo +============================== +*/ +int hhWeapon::LowAltAmmo() { + return (altFireController) ? altFireController->LowAmmo() : 0; +} + +/* +============================== +hhWeapon::GetAltMode +HUMANHEAD bjk +============================== +*/ +bool hhWeapon::GetAltMode() const { + return WEAPON_ALTMODE != 0; + +} + +/* +============================== +hhWeapon::UseAmmo +============================== +*/ +void hhWeapon::UseAmmo() { + if (fireController) { + fireController->UseAmmo(); + } +} + +/* +============================== +hhWeapon::UseAltAmmo +============================== +*/ +void hhWeapon::UseAltAmmo() { + if (altFireController) { + altFireController->UseAmmo(); + } +} + +/* +============================== +hhWeapon::CheckDeferredProjectiles + +HUMANEAD: rww +============================== +*/ +void hhWeapon::CheckDeferredProjectiles(void) { + if (fireController) { + fireController->CheckDeferredProjectiles(); + } + if (altFireController) { + altFireController->CheckDeferredProjectiles(); + } +} + +/* +============================== +hhWeapon::LaunchProjectiles + +HUMANEAD: aob +============================== +*/ +void hhWeapon::LaunchProjectiles( hhWeaponFireController* controller ) { + if ( IsHidden() ) { + return; + } + + // wake up nearby monsters + if ( !spawnArgs.GetBool( "silent_fire" ) ) { + gameLocal.AlertAI( owner.GetEntity() ); + } + + // set the shader parm to the time of last projectile firing, + // which the gun material shaders can reference for single shot barrel glows, etc + SetShaderParm( SHADERPARM_DIVERSITY, gameLocal.random.CRandomFloat() ); + SetShaderParm( SHADERPARM_TIMEOFFSET, -MS2SEC( gameLocal.realClientTime ) ); + + float low = ((float)controller->AmmoAvailable()*controller->AmmoRequired())/controller->LowAmmo(); + + if( !controller->LaunchProjectiles(pushVelocity) ) { + return; + } + + if (!gameLocal.isClient) { //HUMANHEAD rww - let everyone hear this sound, and broadcast it (so don't try to play it for client-projectile weapons) + if( controller->AmmoAvailable()*controller->AmmoRequired() <= controller->LowAmmo() && low > 1 && spawnArgs.FindKey("snd_lowammo")) { + StartSound( "snd_lowammo", SND_CHANNEL_ANY, 0, true, NULL ); + } + } + + controller->UpdateMuzzleKick(); + + // add the light for the muzzleflash + controller->MuzzleFlash(); + + //HUMANEHAD: aob + controller->WeaponFeedback(); + //HUMANHEAD END +} + +/* +=============== +hhWeapon::SetViewAnglesSensitivity + +HUMANHEAD: aob +=============== +*/ +void hhWeapon::SetViewAnglesSensitivity( float fov ) { + if( owner.IsValid() ) { + owner->SetViewAnglesSensitivity( fov / g_fov.GetFloat() ); + } +} + +/* +=============== +hhWeapon::GetDict + +HUMANHEAD: aob +=============== +*/ +const idDict* hhWeapon::GetDict( const char* objectname ) { + return gameLocal.FindEntityDefDict( objectname, false ); +} + +/* +=============== +hhWeapon::GetFireInfoDict + +HUMANHEAD: aob +=============== +*/ +const idDict* hhWeapon::GetFireInfoDict( const char* objectname ) { + const idDict* dict = GetDict( objectname ); + if( !dict ) { + return NULL; + } + + return gameLocal.FindEntityDefDict( dict->GetString("def_fireInfo"), false ); +} + +/* +=============== +hhWeapon::GetAltFireInfoDict + +HUMANHEAD: aob +=============== +*/ +const idDict* hhWeapon::GetAltFireInfoDict( const char* objectname ) { + const idDict* dict = GetDict( objectname ); + if( !dict ) { + return NULL; + } + + return gameLocal.FindEntityDefDict( dict->GetString("def_altFireInfo"), false ); +} + +void hhWeapon::Event_GetString(const char *key) { + if ( fireController ) { + idThread::ReturnString( fireController->GetString(key) ); + } +} + +void hhWeapon::Event_GetAltString(const char *key) { + if ( altFireController ) { + idThread::ReturnString( altFireController->GetString(key) ); + } +} + +/* +=============== +hhWeapon::Event_AddToAltClip +=============== +*/ +void hhWeapon::Event_AddToAltClip( int amount ) { + HH_ASSERT( altFireController ); + + altFireController->AddToClip( amount ); +} + +/* +=============== +hhWeapon::Event_AmmoInClip +=============== +*/ +void hhWeapon::Event_AltAmmoInClip( void ) { + HH_ASSERT( altFireController ); + + idThread::ReturnFloat( altFireController->AmmoInClip() ); +} + +/* +=============== +hhWeapon::Event_AltAmmoAvailable +=============== +*/ +void hhWeapon::Event_AltAmmoAvailable( void ) { + HH_ASSERT( altFireController ); + + idThread::ReturnFloat( altFireController->AmmoAvailable() ); +} + +/* +=============== +hhWeapon::Event_ClipSize +=============== +*/ +void hhWeapon::Event_AltClipSize( void ) { + HH_ASSERT( altFireController ); + + idThread::ReturnFloat( altFireController->ClipSize() ); +} + +/* +============================== +hhWeapon::Event_GetFireDelay +============================== +*/ +void hhWeapon::Event_GetFireDelay() { + HH_ASSERT( fireController ); + + idThread::ReturnFloat( fireController->GetFireDelay() ); +} + +/* +============================== +hhWeapon::Event_GetAltFireDelay +============================== +*/ +void hhWeapon::Event_GetAltFireDelay() { + HH_ASSERT( altFireController ); + + idThread::ReturnFloat( altFireController->GetFireDelay() ); +} + +/* +============================== +hhWeapon::Event_HasAmmo +============================== +*/ +void hhWeapon::Event_HasAmmo() { + idThread::ReturnInt( fireController->HasAmmo() ); +} + +/* +============================== +hhWeapon::Event_HasAltAmmo +============================== +*/ +void hhWeapon::Event_HasAltAmmo() { + idThread::ReturnInt( altFireController->HasAmmo() ); +} + +/* +=============== +hhWeapon::Event_SetViewAnglesSensitivity + +HUMANHEAD: aob +=============== +*/ +void hhWeapon::Event_SetViewAnglesSensitivity( float fov ) { + SetViewAnglesSensitivity( fov ); +} + +/* +================ +hhWeapon::UpdateNozzleFx +================ +*/ +void hhWeapon::UpdateNozzleFx( void ) { + + if ( !nozzleFx || !g_projectileLights.GetBool()) { + return; + } + + if ( nozzleJointHandle.view == INVALID_JOINT ) { + return; + } + + // + // vent light + // + if ( nozzleGlowHandle == -1 ) { + memset(&nozzleGlow, 0, sizeof(nozzleGlow)); + if ( owner.IsValid() ) { + nozzleGlow.allowLightInViewID = owner->entityNumber+1; + } + nozzleGlow.pointLight = true; + nozzleGlow.noShadows = true; + nozzleGlow.lightRadius.x = nozzleGlowRadius; + nozzleGlow.lightRadius.y = nozzleGlowRadius; + nozzleGlow.lightRadius.z = nozzleGlowRadius; + nozzleGlow.shader = nozzleGlowShader; + GetJointWorldTransform( nozzleJointHandle, nozzleGlow.origin, nozzleGlow.axis ); + + nozzleGlow.origin += nozzleGlowOffset * nozzleGlow.axis; + nozzleGlowHandle = gameRenderWorld->AddLightDef(&nozzleGlow); + } + + GetJointWorldTransform(nozzleJointHandle, nozzleGlow.origin, nozzleGlow.axis ); + nozzleGlow.origin += nozzleGlowOffset * nozzleGlow.axis; + + nozzleGlow.shaderParms[ SHADERPARM_RED ] = nozzleGlowColor.x; + nozzleGlow.shaderParms[ SHADERPARM_GREEN ] = nozzleGlowColor.y; + nozzleGlow.shaderParms[ SHADERPARM_BLUE ] = nozzleGlowColor.z; + + // Copy parms from the weapon into this light + for( int i = 4; i < 8; i++ ) { + nozzleGlow.shaderParms[ i ] = GetRenderEntity()->shaderParms[ i ]; + } + + gameRenderWorld->UpdateLightDef(nozzleGlowHandle, &nozzleGlow); +} + +void hhWeapon::Event_HideWeapon(void) { + HideWeapon(); +} + +void hhWeapon::Event_ShowWeapon(void) { + ShowWeapon(); +} + +/* +=============== +hhWeapon::FillDebugVars +=============== +*/ +void hhWeapon::FillDebugVars(idDict *args, int page) { + idStr text; + + switch(page) { + case 1: + args->SetBool("WEAPON_LOWERWEAPON", WEAPON_LOWERWEAPON != 0); + args->SetBool("WEAPON_RAISEWEAPON", WEAPON_RAISEWEAPON != 0); + args->SetBool("WEAPON_ASIDEWEAPON", WEAPON_ASIDEWEAPON != 0); + args->SetBool("WEAPON_ATTACK", WEAPON_ATTACK != 0); + args->SetBool("WEAPON_ALTATTACK", WEAPON_ALTATTACK != 0); + args->SetBool("WEAPON_ALTMODE", WEAPON_ALTMODE != 0); + args->SetBool("WEAPON_RELOAD", WEAPON_RELOAD != 0); + break; + } +} diff --git a/src/Prey/prey_baseweapons.h b/src/Prey/prey_baseweapons.h new file mode 100644 index 0000000..e0e96d0 --- /dev/null +++ b/src/Prey/prey_baseweapons.h @@ -0,0 +1,365 @@ +#ifndef __HH_PREY_BASE_WEAPONS_H +#define __HH_PREY_BASE_WEAPONS_H + +class hhPlayer; + +extern const idEventDef EV_Weapon_Aside; +extern const idEventDef EV_PlayAnimWhenReady; +extern const idEventDef EV_Weapon_Feedback; +extern const idEventDef EV_PlayCycle; + +//HUMANHEAD: aob +extern const idEventDef EV_Weapon_FireAltProjectiles; +extern const idEventDef EV_Weapon_FireProjectiles; +//HUMANHEAD END + +/*********************************************************************** + + hhWeapon + + Base class for common weapon data and methods +***********************************************************************/ +class hhWeapon: public hhAnimatedEntity { + CLASS_PROTOTYPE( hhWeapon ); + + //HUMANHEAD: aob - used to allow physics object to have access to hhWeaponBases' private methods and vars + friend hhPhysics_StaticWeapon; + //HUMANHEAD END + + public: + hhWeapon(); + virtual ~hhWeapon(); + void Spawn(); + virtual void Think(); + + virtual void Clear(); + virtual void GetWeaponDef( const char *objectname, int ammoinclip ) { GetWeaponDef(objectname); } + virtual void GetWeaponDef( const char *objectname ); + virtual void SetOwner( idPlayer *_owner ); + + virtual void RestoreGUI( const char* guiKey, idUserInterface** gui ); + + virtual void EnterCinematic(); + virtual void ExitCinematic(); + void NetCatchup(); + + virtual int GetClipBits(void) const; + + // save games + void Save( idSaveGame *savefile ) const; // archives object for save game file + void Restore( idRestoreGame *savefile ); + + // GUIs + const char * Icon( void ) const; + virtual void UpdateGUI(); + + virtual void SetModel( const char *modelname ); + virtual void FreeModelDef(); + void GetJointHandle( const char* jointName, weaponJointHandle_t& handle ); + bool GetJointWorldTransform( const char* jointName, idVec3 &offset, idMat3 &axis ); + bool GetJointWorldTransform( const weaponJointHandle_t& handle, idVec3 &offset, idMat3 &axis, bool muzzleOnly = false ); //rww - added muzzleOnly parameter for mp + void SetPushVelocity( const idVec3 &pushVelocity ); + virtual void SetSkin( const idDeclSkin *skin ); + virtual void SetShaderParm( int parmnum, float value ); + + bool IsLinked( void ); + bool IsWorldModelReady( void ); + + // State control/player interface + virtual void Raise( void ); + virtual void PutAway( void ); + virtual void Reload( void ); + //HUMANHEAD: aob - put bodies here + void LowerWeapon( void ) {} + void RaiseWeapon( void ) {} + //HUMANHEAD END + virtual void HideWeapon( void ); + void ShowWeapon( void ); + void HideWorldModel( void ); + void ShowWorldModel( void ); + void OwnerDied( void ); + void BeginAttack( void ); + virtual void EndAttack( void ); + virtual bool IsReady( void ) const; + virtual bool IsReloading( void ) const; + bool IsChangable( void ) const; + virtual bool IsHolstered( void ) const; + bool IsLowered( void ) const; // nla + bool IsRising( void ) const; // nla + bool IsAside( void ) const; // nla + void PutAside( void ); // nla + void PutUpright( void ); // nla + virtual int GetAnimDoneTime( void ) const { return( animDoneTime ); } // nla + + bool ShowCrosshair( void ) const; + idEntity* DropItem( const idVec3 &velocity, int activateDelay, int removeDelay, bool died ); + bool CanDrop( void ) const; + void WeaponStolen( void ) {} + + // Script state management + virtual bool ShouldConstructScriptObjectAtSpawn( void ) const; + virtual idThread *ConstructScriptObject( void ); + virtual void DeconstructScriptObject( void ); + void SetState( const char *statename, int blendFrames ); + void UpdateScript( void ); + + // Visual presentation + virtual // HUMANHEAD: made virtual + void PresentWeapon( bool showViewModel ); + int GetZoomFov(); + void GetWeaponAngleOffsets( int *average, float *scale, float *max ); + void GetWeaponTimeOffsets( float *time, float *scale ); + + //Ammo + ammo_t GetAmmoType( void ) const; + int AmmoAvailable( void ) const; + int AmmoInClip( void ) const; + void ResetAmmoClip( void ) {} + int ClipSize( void ) const; + int AmmoRequired( void ) const; + void UseAmmo(); + int LowAmmo(); + + + //HUMANHEAD: altAmmo + ammo_t GetAltAmmoType( void ) const; + int AltAmmoAvailable( void ) const; + int AltAmmoInClip( void ) const; + int AltClipSize( void ) const; + int LowAltAmmo() const; + int AltAmmoRequired( void ) const; + void UseAltAmmo(); + int LowAltAmmo(); + //HUMANHEAD END + + bool GetAltMode() const; + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + // Visual presentation + void InitWorldModel( const idDict *dict ); + void MuzzleRise( idVec3 &origin, idMat3 &axis ); + + //HUMANHEAD: aob + virtual void ParseDef( const char* objectname ); + virtual void InitScriptObject( const char* objectType ); + static idEntity* SpawnWorldModel( const char* worldModelDict, idActor* _owner ); + virtual void GetMasterDefaultPosition( idVec3 &masterOrigin, idMat3 &masterAxis ) const; + virtual hhPlayer* GetOwner() const { return owner.GetEntity(); } + virtual void UpdateCrosshairs( bool &crosshair, bool &targeting ); + + virtual int GetHandedness() const { return( handedness ); } + + virtual void BeginAltAttack( void ); + void EndAltAttack( void ); + + int GetStatus() { return( status ); }; + const idDict * GetDict() { return( dict ); }; + + static const idDict* GetDict( const char* objectname ); + static const idDict* GetFireInfoDict( const char* objectname ); + static const idDict* GetAltFireInfoDict( const char* objectname ); + + int GetKickEndTime() const { return kick_endtime; } + void SetKickEndTime( int endTime ) { kick_endtime = endTime; } + idVec3 GetMuzzlePosition() const { return fireController->GetMuzzlePosition(); } + idVec3 GetAltMuzzlePosition() const { return altFireController->GetMuzzlePosition(); } + + void SnapDown(); + void SnapUp(); + + void PrecomputeTraceInfo(); + const trace_t& GetEyeTraceInfo() const { return eyeTraceInfo; } + + void SetViewAnglesSensitivity( float fov ); + + void UpdateNozzleFx( void ); + //HUMANEHAD END + + void CheckDeferredProjectiles(void); //HUMANHEAD rww + + virtual void FillDebugVars( idDict *args, int page ); //HUMANHEAD bjk + + protected: + virtual bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); + + void LaunchProjectiles( hhWeaponFireController* controller ); + + //needed for overridding + //FIXME: would like to make these templates just in case we change types. + ID_INLINE virtual hhWeaponFireController* CreateFireController(); + ID_INLINE virtual hhWeaponFireController* CreateAltFireController(); + //HUMANHEAD END + + protected: + //HUMANHEAD: aob + void Event_PlayAnimWhenReady( const char* animName ); + void Event_Raise(); // nla + void Event_Weapon_Aside(); + void Event_Weapon_PuttingAside(); + void Event_Weapon_Uprighting(); + + //Called from frame command + void Event_SpawnFXAlongBone( idList* fxParms ); + + void Event_WeaponOutOfAltAmmo(); + void Event_AddToAltClip( int amount ); + void Event_AltAmmoInClip(); + void Event_AltAmmoAvailable(); + void Event_AltClipSize(); + void Event_HasAltAmmo(); + + void Event_HasAmmo(); + + virtual void Event_FireAltProjectiles(); + virtual void Event_FireProjectiles(); + + void Event_EjectAltBrass( void ); + + void Event_IsAnimPlaying( const char *animname ); // CJR + + void Event_GetFireDelay(); + void Event_GetAltFireDelay(); + void Event_GetSpread(); + void Event_GetAltSpread(); + void Event_GetString(const char *key); + void Event_GetAltString(const char *key); + + void Event_SetViewAnglesSensitivity( float fov); + void Event_GetOwner( void ); + void Event_UseAmmo( int amount ); + void Event_UseAltAmmo( int amount ); + //HUMANHEAD END + + //idWeapon events + // script events + void Event_WeaponState( const char *statename, int blendFrames ); + void Event_SetWeaponStatus( float newStatus ); + void Event_WeaponReady( void ); + void Event_WeaponOutOfAmmo( void ); + void Event_WeaponReloading( void ); + void Event_WeaponHolstered( void ); + void Event_WeaponRising( void ); + void Event_WeaponLowering( void ); + void Event_AddToClip( int amount ); + void Event_AmmoInClip( void ); + void Event_AmmoAvailable( void ); + void Event_ClipSize( void ); + void Event_PlayAnim( int channel, const char *animname ); + void Event_PlayCycle( int channel, const char *animname ); + void Event_AnimDone( int channel, int blendFrames ); + void Event_WaitFrame( void ); + void Event_Next( void ); + void Event_SetSkin( const char *skinname ); + void Event_Flashlight( int enable ); + void Event_EjectBrass( void ); + void Event_HideWeapon( void ); + void Event_ShowWeapon( void ); + + protected: + hhWeaponFireController* fireController; + hhWeaponFireController* altFireController; + trace_t eyeTraceInfo; + + idVec3 cameraShakeOffset; + hhPhysics_StaticWeapon physicsObj; + + // 0 - no hands, 1 - right, 2 - left, 3 - both + int handedness; // nla - For determining which hands can be up with which weapons + + idScriptBool WEAPON_ALTATTACK; + idScriptBool WEAPON_ASIDEWEAPON; // nla + idScriptBool WEAPON_ALTMODE; // for moded weapons like rifle, whether in alt mode + + //idWeapon vars + // script control + idScriptBool WEAPON_ATTACK; + idScriptBool WEAPON_RELOAD; + idScriptBool WEAPON_RAISEWEAPON; + idScriptBool WEAPON_LOWERWEAPON; + idScriptFloat WEAPON_NEXTATTACK; //rww + weaponStatus_t status; + idThread * thread; + idStr state; + idStr idealState; + int animBlendFrames; + int animDoneTime; + + //HUMANHEAD: aob - made hhPlayer + idEntityPtr owner; + idEntityPtr worldModel; + + // weapon kick + int kick_endtime; + + idVec3 pushVelocity; + + // weapon definition + const idDeclEntityDef * weaponDef; + const idDict * dict; + + idStr icon; + + bool lightOn; + + // zoom + int zoomFov; // variable zoom fov per weapon + + // weighting for viewmodel angles + int weaponAngleOffsetAverages; + float weaponAngleOffsetScale; + float weaponAngleOffsetMax; + float weaponOffsetTime; + float weaponOffsetScale; + + // Nozzle FX + bool nozzleFx; + weaponJointHandle_t nozzleJointHandle; + renderLight_t nozzleGlow; // nozzle light + int nozzleGlowHandle; // handle for nozzle light + idVec3 nozzleGlowColor; // color of the nozzle glow + const idMaterial * nozzleGlowShader; // shader for glow light + float nozzleGlowRadius; // radius of glow light + idVec3 nozzleGlowOffset; // offset from bound bone + +}; + +/* +================ +hhWeapon::IsLinked +================ +*/ +ID_INLINE bool hhWeapon::IsLinked( void ) { + return scriptObject.HasObject(); +} + +/* +================ +hhWeapon::IsWorldModelReady +================ +*/ +ID_INLINE bool hhWeapon::IsWorldModelReady( void ) { + return worldModel.IsValid(); +} + +/* +================ +hhWeapon::CreateFireController +================ +*/ +ID_INLINE hhWeaponFireController* hhWeapon::CreateFireController() { + return new hhWeaponFireController; +} + +/* +================ +hhWeapon::CreateAltFireController +================ +*/ +ID_INLINE hhWeaponFireController* hhWeapon::CreateAltFireController() { + return CreateFireController(); +} + +#endif diff --git a/src/Prey/prey_beam.cpp b/src/Prey/prey_beam.cpp new file mode 100644 index 0000000..652bbff --- /dev/null +++ b/src/Prey/prey_beam.cpp @@ -0,0 +1,1150 @@ +//************************************************************************** +//** +//** PREY_BEAM.CPP +//** +//** Code for Prey-specific beams +//** +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +// Game code includes +#include "prey_local.h" + +#define DEBUG_BEAMS 0 + +static idVec3 CurveExtractSpline(idVec3 *cPoints, int cPointCount, float alpha, bool bThruCtrlPnts); + +const idEventDef EV_ToggleBeamLength("", "d"); +const idEventDef EV_SetBeamPhaseScale( "setBeamPhaseScale", "f" ); // bg +const idEventDef EV_SetBeamOffsetScale( "setBeamOffsetScale", "f" ); // bg + +CLASS_DECLARATION(idEntity, hhBeamSystem) + EVENT( EV_Activate, hhBeamSystem::Event_Activate ) + EVENT( EV_SetBeamPhaseScale, hhBeamSystem::Event_SetBeamPhaseScale) // bg + EVENT( EV_SetBeamOffsetScale, hhBeamSystem::Event_SetBeamOffsetScale) // bg +END_CLASS + + +hhBeamSystem::hhBeamSystem() { + beamList = NULL; + offsetScale = 1.0f; + phaseScale = 1.0f; + bActive = false; + + targetLocation = vec3_origin; + + decl = NULL; +} + +hhBeamSystem::~hhBeamSystem() { + if(renderEntity.beamNodes) { + Mem_Free( renderEntity.beamNodes ); + renderEntity.beamNodes = NULL; + } + + if(beamList) { + delete [] beamList; + beamList = NULL; + } +} + +void hhBeamSystem::Spawn(void) { + beamList = NULL; + + if (!gameLocal.isClient || fl.clientEntity) { //rww - for client in mp this gets done after the first snapshot is read... + InitSystem( spawnArgs.GetString("model") ); + } + + beamLength = spawnArgs.GetFloat( "lengthBeam" ); + bRigidBeamLength = spawnArgs.GetBool( "rigidBeamLength" ); + + targetEntity = NULL; + targetEntityId = 0; + targetEntityOffset.Zero(); + + SetArcVector( vec3_origin ); + + bActive = spawnArgs.GetBool( "start_off" ); + Activate( !bActive ); +} + +void hhBeamSystem::Activate( const bool bActivate ) { + if( bActive != bActivate ) { + bActive = bActivate; + + if( bActive ) { + BecomeActive( TH_UPDATEPARTICLES ); + Show(); + }else { + BecomeInactive( TH_UPDATEPARTICLES ); + Hide(); + } + } +} + +hhBeamSystem* hhBeamSystem::SpawnBeam( const idVec3& start, const char* modelName, const idMat3& axis, bool pureLocal ) { + idDict Args; + hhBeamSystem* beamSystem = NULL; + idStr modelString; + +#if !GOLD + //rww - checking for creation of beams at nan location + assert(!FLOAT_IS_NAN(start.x)); + assert(!FLOAT_IS_NAN(start.y)); + assert(!FLOAT_IS_NAN(start.z)); + assert(!FLOAT_IS_NAN(axis[0].x)); + assert(!FLOAT_IS_NAN(axis[0].y)); + assert(!FLOAT_IS_NAN(axis[0].z)); + assert(!FLOAT_IS_NAN(axis[1].x)); + assert(!FLOAT_IS_NAN(axis[1].y)); + assert(!FLOAT_IS_NAN(axis[1].z)); + assert(!FLOAT_IS_NAN(axis[2].x)); + assert(!FLOAT_IS_NAN(axis[2].y)); + assert(!FLOAT_IS_NAN(axis[2].z)); +#endif + + if ( !modelName || !modelName[0] ) { + return NULL; + } + + // Ensure that the modelname ends with a valid .beam extention + modelString = modelName; + modelString.DefaultFileExtension( ".beam" ); + + Args.Set( "spawnclass", "hhBeamSystem" ); + Args.Set( "model", modelString.c_str() ); + Args.SetVector( "origin", start ); + Args.SetMatrix( "rotation", axis ); + + HH_ASSERT(!gameLocal.isClient || pureLocal); + + beamSystem = (hhBeamSystem *)gameLocal.SpawnEntityTypeClient( hhBeamSystem::Type, &Args ); + if (beamSystem) { //make sure it's sync'd + beamSystem->fl.networkSync = !pureLocal; + } + HH_ASSERT( beamSystem ); + + beamSystem->SetTargetEntity( NULL ); + beamSystem->SetTargetLocation( start ); // Necessary in case the beam starts out hidden + beamSystem->beamAxis = mat3_identity; + + return beamSystem; +} + +//========================================================================== +// +// hhBeamSystem::InitSystem +// +// Reads beam system information from the entity definition +// +// Commands: +// beamNum - total number of beams in the system +// beamNodes - total number of nodes per beam +// +// Per beam (end number specifies the beam): +// +// beamThickness0 - Thickness +// beamTaperEndPoints0 - If the beam should have tapered end points +// beamShader0 - Shader name +// beamSpline0 - if this beam system uses splines +//========================================================================== + +void hhBeamSystem::InitSystem( const char* modelName ) { + int i; + + HH_ASSERT( modelName ); + + if(renderEntity.beamNodes) { // Delete the previous version before initializing + Mem_Free(renderEntity.beamNodes); + renderEntity.beamNodes = NULL; + } + + // Initialize beam data from the beam file + declName = modelName; + decl = declManager->FindBeam( modelName ); + renderEntity.declBeam = decl; + + // Get new model loaded + if( !renderEntity.hModel || idStr::Icmp( renderEntity.hModel->Name(), modelName ) ) { + renderEntity.hModel = renderModelManager->FindModel( modelName ); + } + + // Allocate the beams + if( beamList ) { + delete[] beamList; + } + beamList = new hhBeam[decl->numBeams]; + + // Allocate the number of beam infos (used for rendering) + renderEntity.beamNodes = (hhBeamNodes_t *)Mem_ClearedAlloc(sizeof(hhBeamNodes_t) * decl->numBeams); + + beamAxis = GetPhysics()->GetAxis(); + + // Initialize each beam with specific info + for(i = 0; i < decl->numBeams; i++) { + beamList[i].Init( this, &renderEntity.beamNodes[i] ); + } + + random.SetSeed( gameLocal.time ); + beamTime = gameLocal.time + random.RandomInt( 5000 ); // Don't let any beams stay in sync. +} + +//========================================================================== +// +// hhBeamSystem::SetTargetLocation +// +//========================================================================== + +void hhBeamSystem::SetTargetLocation(idVec3 newLoc) { + targetLocation = newLoc; + + beamLength = (targetLocation - GetPhysics()->GetOrigin()).Length(); +} + +//========================================================================== +// +// hhBeamSystem::SetTargetEntity +// +// Sets the entity to target from this beam. If joint is INVALID_JOINT, +// then the origin of the entity will be used. +// +// Passing in a NULL entity will clear the target entity, and set the targetLocation to offset +// +// NOTE: The offset passed in should be in world coordinates relative to the model. +// NOTE: The offset can be used as an offset from the joint as well. +//========================================================================== + +void hhBeamSystem::SetTargetEntity( idEntity *ent, int traceId, idVec3 &offset ) { + idVec3 origin; + idMat3 axis; + + targetEntity = ent; + + if ( ent == NULL ) { + targetEntityId = 0; + targetEntityOffset.Zero(); + SetTargetLocation( offset ); + return; + } + + targetEntityId = traceId; + targetEntityOffset = offset; + + // Update the targetLocation + GetTargetLocation(); + + // Calculate the beam length from the new target location + beamLength = (targetLocation - GetPhysics()->GetOrigin()).Length(); +} + +//========================================================================== +// +// hhBeamSystem::SetTargetEntity +// +// Bone name version +//========================================================================== + +void hhBeamSystem::SetTargetEntity( idEntity *ent, const char *boneName, idVec3 &offset ) { + idVec3 origin; + idMat3 axis; + + targetEntity = ent; + + if ( ent == NULL || boneName == NULL ) { + targetEntityId = 0; + targetEntityOffset.Zero(); + SetTargetLocation( offset ); + return; + } + + targetEntityId = ent->GetAnimator()->GetJointHandle( boneName ); + targetEntityOffset = offset; + + // Update the targetLocation + GetTargetLocation(); + + // Calculate the beam length from the new target location + beamLength = (targetLocation - GetPhysics()->GetOrigin()).Length(); +} + +//========================================================================== +// +// hhBeamSystem::GetTargetLocation +// +//========================================================================== + +idVec3 hhBeamSystem::GetTargetLocation( void ) { + idVec3 origin; + idMat3 axis; + idVec3 boneOrigin; + idMat3 boneAxis; + jointHandle_t joint; + idAFEntity_Base *af = NULL; + + if ( targetEntity.IsValid() ) { // Has a target entity, so compute the offset + if ( targetEntityId > 0 ) { // This is a specific joint: + joint = (jointHandle_t)targetEntityId; + } else { // This is a collision handle + joint = CLIPMODEL_ID_TO_JOINT_HANDLE( targetEntityId ); + } + + if ( targetEntity->IsType( idAFEntity_Base::Type ) ) { + af = static_cast( targetEntity.GetEntity() ); + } + + axis = targetEntity->GetAxis(); + origin = targetEntity->GetOrigin(); + + // Calculate the targetLocation, based upon the type of entity + if ( af && af->IsActiveAF() ) { // AF, use the clip model id for the origin + int body = af->BodyForClipModelId( targetEntityId ); + origin = af->GetPhysics()->GetOrigin( body ); + targetLocation = origin + axis * targetEntityOffset; + //rww - added check for targetEntity->GetAnimator() not being null, in case targetEntity is not animated + } else if ( joint != INVALID_JOINT && targetEntity->GetAnimator() && targetEntity->GetAnimator()->GetJointTransform( joint, gameLocal.time, boneOrigin, boneAxis ) ) { // Joint-based + targetLocation = origin + axis * boneOrigin; + } else { // Default - use origin of the entity + targetLocation = origin + axis * targetEntityOffset; + } + } + + return targetLocation; +} + +//========================================================================== +// +// hhBeamSystem::SetAxis +// +//========================================================================== +void hhBeamSystem::SetAxis( const idMat3 &axis ) { + if (!GetPhysics()->GetAxis().Compare(axis)) { + idEntity::SetAxis(axis); + } +} + +//========================================================================== +// +// hhBeamSystem::SetBeamLength +// +//========================================================================== +void hhBeamSystem::SetBeamLength( const float length ) { + beamLength = length; + + if( bRigidBeamLength && beamLength > VECTOR_EPSILON ) { + SetTargetLocation( GetPhysics()->GetOrigin() + GetPhysics()->GetAxis()[0] * beamLength ); + } +} + +/* +================= +hhBeamSystem::WriteToSnapshot +rww - write applicable beam values to snapshot +================= +*/ +void hhBeamSystem::WriteToSnapshot( idBitMsgDelta &msg ) const { +#if !GOLD + gameLocal.Warning("Beam %i being sync'd by server!\n", entityNumber); +#endif + GetPhysics()->WriteToSnapshot( msg ); + + msg.WriteFloat(beamTime); + + //handle this. + //hhBeam *beamList; + + //this is the common method, but doing WriteDirs might be more efficient? + idCQuat quat = beamAxis.ToCQuat(); + msg.WriteFloat(quat.x); + msg.WriteFloat(quat.y); + msg.WriteFloat(quat.z); + + msg.WriteFloat(arcVector[0]); + msg.WriteFloat(arcVector[1]); + msg.WriteFloat(arcVector[2]); + + msg.WriteFloat(targetLocation[0]); + msg.WriteFloat(targetLocation[1]); + msg.WriteFloat(targetLocation[2]); + + if (targetEntity.IsValid()) { + msg.WriteBits(1, 1); + msg.WriteBits(targetEntity->entityNumber, GENTITYNUM_BITS); + } + else { + msg.WriteBits(0, 1); + } + + msg.WriteFloat(targetEntityOffset[0]); + msg.WriteFloat(targetEntityOffset[1]); + msg.WriteFloat(targetEntityOffset[2]); + + //do i care about this? + //int targetEntityId; + + msg.WriteFloat(beamLength); + msg.WriteBits(bRigidBeamLength, 1); + + msg.WriteFloat(phaseScale); + msg.WriteFloat(offsetScale); + + msg.WriteBits(bActive, 1); + + //rwwFIXME this is horrible. oh so very horrible. all those calls to SpawnBeam in code need to be removed, + //and they must all use seperate entity defs, or there is no alternative to sending a real string. =| + msg.WriteString(spawnArgs.GetString("model")); +} + +/* +================= +hhBeamSystem::ReadFromSnapshot +rww - read applicable beam values from snapshot +================= +*/ +void hhBeamSystem::ReadFromSnapshot( const idBitMsgDelta &msg ) { + GetPhysics()->ReadFromSnapshot( msg ); + + beamTime = msg.ReadFloat(); + + idCQuat quat; + quat.x = msg.ReadFloat(); + quat.y = msg.ReadFloat(); + quat.z = msg.ReadFloat(); + beamAxis = quat.ToMat3(); + + arcVector[0] = msg.ReadFloat(); + arcVector[1] = msg.ReadFloat(); + arcVector[2] = msg.ReadFloat(); + + targetLocation[0] = msg.ReadFloat(); + targetLocation[1] = msg.ReadFloat(); + targetLocation[2] = msg.ReadFloat(); + + int hasEnt = msg.ReadBits(1); + if (hasEnt) { + int entNum = msg.ReadBits(GENTITYNUM_BITS); + targetEntity = gameLocal.entities[entNum]; + } + else { + targetEntity = NULL; + } + + targetEntityOffset[0] = msg.ReadFloat(); + targetEntityOffset[1] = msg.ReadFloat(); + targetEntityOffset[2] = msg.ReadFloat(); + + beamLength = msg.ReadFloat(); + bRigidBeamLength = !!msg.ReadBits(1); + + phaseScale = msg.ReadFloat(); + offsetScale = msg.ReadFloat(); + + bool active = !!msg.ReadBits(1); + if (active != bActive) { + Activate(active); + } + + char modelName[128]; + msg.ReadString(modelName, 128); + if (!beamList) { + //then init the system. + InitSystem(modelName); + } +} + +/* +================= +hhBeamSystem::ClientPredictionThink +rww - minimal think on client +================= +*/ +void hhBeamSystem::ClientPredictionThink( void ) { + if (fl.clientEntity && snapshotOwner.IsValid()) { + if (!bActive || !gameLocal.EntInClientSnapshot(snapshotOwner->entityNumber)) { //if the snapshot entity i'm associated with is not in the snapshot, i hide + Hide(); + } + else { + Show(); + } + } + + if (gameLocal.isNewFrame) { + Think(); + } +} + +//========================================================================== +// +// hhBeamSystem::Think +// +// NOTE: Beams will not think AT ALL if they are hidden or not visible +//========================================================================== + +void hhBeamSystem::Think( void ) { + + RunPhysics(); + + if (thinkFlags & TH_UPDATEPARTICLES) { + + if( targets.Num() > 0 ) { + SetTargetEntity( targets[0].GetEntity(), NULL ); + } + + // Update the beamAxis to correctly reflect the target + if( !bRigidBeamLength ) { + idVec3 vec = (GetTargetLocation() - GetPhysics()->GetOrigin()); + beamLength = vec.Normalize(); + + if ( beamLength <= 0 ) { // Ignore beams with no length + return; + } + + if( GetBindMaster() && fl.bindOrientated ) { + beamAxis = GetAxis(); + } else { + beamAxis = vec.ToMat3(); + + if ( targets.Num() > 0 ) { + SetAxis( beamAxis ); // Target beams should update renderEntity information + } else { + GetPhysics()->SetAxis( beamAxis ); // Normal beams don't need to update render entity information + } + } + } else { + SetTargetLocation( GetPhysics()->GetOrigin() + GetPhysics()->GetAxis()[0] * beamLength ); + beamAxis = GetPhysics()->GetAxis(); + } + + assert(decl); + for( int i = 0; i < decl->numBeams; i++) { + ExecuteBeam( i, &beamList[i] ); + beamList[i].TransformNodes(); + } + } + + Present(); +} + +//========================================================================== +// +// hhBeamSystem::Event_Activate +// +// Triggering a beam will toggle its visibility +//========================================================================== + +void hhBeamSystem::Event_Activate( idEntity *activator ) { + Activate( !IsActivated() ); +} + +//========================================================================== +// +// hhBeamSystem::Event_SetBeamPhaseScale +// +//========================================================================== + +void hhBeamSystem::Event_SetBeamPhaseScale( float scale ) { + SetBeamPhaseScale( scale ); +} + +//========================================================================== +// +// hhBeamSystem::Event_SetBeamOffsetScale +// +//========================================================================== + +void hhBeamSystem::Event_SetBeamOffsetScale( float scale ) { + SetBeamOffsetScale( scale ); +} + +//========================================================================== +// +// hhBeamSystem::UpdateModel +// +//========================================================================== + +void hhBeamSystem::UpdateModel( void ) { + + if( renderEntity.hModel && !this->fl.hidden ) { // Only calculate the bounds if the model is valid and the model is visible + renderEntity.bounds = renderEntity.hModel->Bounds( &renderEntity ); + } else { + renderEntity.bounds.Zero(); + renderEntity.bounds.AddPoint(idVec3(-16,-16,-16)); + renderEntity.bounds.AddPoint(idVec3( 16, 16, 16)); + } + + idEntity::UpdateModel(); +} + + +// BEAM CODE --------------------------------------------------------------------- + +//========================================================================== +// +// hhBeam::hhBeam +// +//========================================================================== + +hhBeam::hhBeam() { + nodeList = NULL; +} + +//========================================================================== +// +// hhBeam::~hhBeam +// +//========================================================================== + +hhBeam::~hhBeam() { + nodeList = NULL; +} + +//========================================================================== +// +// hhBeam::Init +// +//========================================================================== + +void hhBeam::Init( hhBeamSystem *newSystem, hhBeamNodes_t *newInfo ) { + int i; + + system = newSystem; + nodeList = newInfo->nodes; + + for(i = 0; i < MAX_BEAM_SPLINE_CONTROLS + EXTRA_SPLINE_CONTROLS; i++) { + splineList[i] = idVec3(0, 0, 0); + } +} + +//========================================================================== +// +// hhBeam::VerifyNodeIndex +// +//========================================================================== + +void hhBeam::VerifyNodeIndex(int index, const char *functionName) { +#if DEBUG_BEAMS + if(index < 0 || index >= system->decl->numNodes) { + gameLocal.Error("%s: %d\n", functionName, index); + } +#endif +} + +//========================================================================== +// +// hhBeam::VerifySplineIndex +// +//========================================================================== + +void hhBeam::VerifySplineIndex(int index, const char *functionName) { +#if DEBUG_BEAMS + if(index < 0 || index >= MAX_BEAM_SPLINE_CONTROLS) { + gameLocal.Error("%s: %d\n", functionName, index); + } +#endif +} + +//========================================================================== +// +// hhBeam::NodeGet +// +// Returns the node location in world space +//========================================================================== + +idVec3 hhBeam::NodeGet(int index) { +#if DEBUG_BEAMS + VerifyNodeIndex(index, "hhBeam::NodeGet"); +#endif + + return( nodeList[index] ); +} + +//========================================================================== +// +// hhBeam::NodeSet +// +//========================================================================== + +void hhBeam::NodeSet(int index, idVec3 value) { +#if DEBUG_BEAMS + VerifyNodeIndex(index, "hhBeam::NodeSet"); +#endif + + nodeList[index] = value; +} + +//========================================================================== +// +// hhBeam::SplineGet +// +//========================================================================== + +idVec3 hhBeam::SplineGet(int index) { +#if DEBUG_BEAMS + VerifySplineIndex(index, "hhBeam::SplineSet"); +#endif + + return(splineList[index]); +} + +//========================================================================== +// +// hhBeam::SplineSet +// +//========================================================================== + +void hhBeam::SplineSet(int index, idVec3 value) { +#if DEBUG_BEAMS + VerifySplineIndex(index, "hhBeam::SplineSet"); +#endif + + splineList[index] = value; +} + +//========================================================================== +// +// hhBeam::SplineLinear +// +//========================================================================== + +void hhBeam::SplineLinear(idVec3 start, idVec3 end) { + int i; + idVec3 loc; + idVec3 delta; + + loc = start; + delta = (end - start) * BEAM_SPLINE_CONTROL_STEP; + + for(i = 0; i < MAX_BEAM_SPLINE_CONTROLS; i++) { + splineList[i] = loc; + loc += delta; + } +} + +//========================================================================== +// +// hhBeam::SplineLinearToTarget +// +//========================================================================== + +void hhBeam::SplineLinearToTarget( void ) { + SplineLinear( system->GetRenderEntity()->origin, system->GetTargetLocation() ); +} + +//========================================================================== +// +// hhBeam::SplineArc +// +//========================================================================== + +void hhBeam::SplineArc( idVec3 start, idVec3 end, idVec3 startVec ) { + int i; + idMat3 axis; + idVec3 loc; + idVec3 delta; + + idVec3 temp = startVec; + temp.Normalize(); + + // Calculate the translation matrix + axis[0] = ( end - start ); + float dist = axis[0].Normalize(); + axis[2] = temp.Cross( axis[0] ); + axis[1] = axis[2].Cross( axis[0] ); + + float dp = temp * axis[0]; + + loc = start; + delta = (end - start) * BEAM_SPLINE_CONTROL_STEP; + + float x = 0; + for( i = 0; i < MAX_BEAM_SPLINE_CONTROLS; i++, x += BEAM_SPLINE_CONTROL_STEP ) { + float temp = -dist * dp * x * ( 1 - x ); + splineList[i] = loc + axis[1] * temp; + loc += delta; + } +} + +//========================================================================== +// +// hhBeam::SplineArcToTarget +// +//========================================================================== + +void hhBeam::SplineArcToTarget( void ) { + SplineArc( system->GetRenderEntity()->origin, system->GetTargetLocation(), system->GetArcVector() ); +} + +//========================================================================== +// +// hhBeam::SplineAddSin +// +//========================================================================== + +void hhBeam::SplineAddSin(int index, float phaseX, float phaseY, float phaseZ, float offsetX, float offsetY, float offsetZ) { + idVec3 sinOffset; + +#if DEBUG_BEAMS + VerifySplineIndex(index, "hhBeam::SplineAddSin"); +#endif + + sinOffset.x = offsetX * idMath::Sin(phaseX); + sinOffset.y = offsetY * idMath::Sin(phaseY); + sinOffset.z = offsetZ * idMath::Sin(phaseZ); + + splineList[index] += sinOffset * system->GetBeamAxis(); +} + +//========================================================================== +// +// hhBeam::SplineAddSinTime +// +// Multiplies the sin value by the current game time +//========================================================================== + +void hhBeam::SplineAddSinTime(int index, float phaseX, float phaseY, float phaseZ, float offsetX, float offsetY, float offsetZ) { + float t; + + t = (gameLocal.time - system->GetBeamTime()) * 0.01f; + SplineAddSin( index, phaseX * t, phaseY * t, phaseZ * t, offsetX, offsetY, offsetZ ); +} + +//========================================================================== +// +// hhBeam::SplineAddSinTimeScaled +// +// Multiplies the sin value by the current game time +//========================================================================== + +void hhBeam::SplineAddSinTimeScaled(int index, float phaseX, float phaseY, float phaseZ, float offsetX, float offsetY, float offsetZ) { + float t; + float offsetScale = system->GetBeamOffsetScale(); + + t = system->GetBeamPhaseScale() * (gameLocal.time - system->GetBeamTime()) * 0.01f; + SplineAddSin( index, phaseX * t, phaseY * t, phaseZ * t, + offsetX * offsetScale, offsetY * offsetScale, offsetZ * offsetScale ); +} + +//========================================================================== +// +// hhBeam::SplineAdd +// +//========================================================================== + +void hhBeam::SplineAdd(int index, idVec3 offset) { +#if DEBUG_BEAMS + VerifySplineIndex(index, "hhBeam::SplineAdd"); +#endif + + float offsetScale = system->GetBeamOffsetScale(); + splineList[index] += offset * offsetScale; +} + +//========================================================================== +// +// hhBeam::ConvertSplineToNodes +// +//========================================================================== + +void hhBeam::ConvertSplineToNodes(void) { + int i; + + // Copy the last spline point into the extra "slop" points + // The extra points are necessary to calculate the final spline segment + for(i = 0; i < EXTRA_SPLINE_CONTROLS; i++) { + splineList[MAX_BEAM_SPLINE_CONTROLS + i] = splineList[MAX_BEAM_SPLINE_CONTROLS - 1]; + } + + // Construct the beam nodes from the spline points + float inc = (float)(MAX_BEAM_SPLINE_CONTROLS) / system->decl->numNodes; + float alpha = 0; + + for(i = 0; i < system->decl->numNodes; i++) { + nodeList[i] = CurveExtractSpline(splineList, MAX_BEAM_SPLINE_CONTROLS + EXTRA_SPLINE_CONTROLS, alpha, true); + alpha += inc; + } +} + +//========================================================================== +// +// hhBeam::NodeLinear +// +//========================================================================== + +void hhBeam::NodeLinear(idVec3 start, idVec3 end) { + int i; + + idVec3 sLoc = start; + idVec3 sDelta = (end - start) / (system->decl->numNodes - 1); + + for(i = 0; i < system->decl->numNodes; i++) { + nodeList[i] = sLoc; + sLoc += sDelta; + } +} + +//========================================================================== +// +// hhBeam::NodeLinearToTarget +// +//========================================================================== + +void hhBeam::NodeLinearToTarget( void ) { + NodeLinear( system->GetRenderEntity()->origin, system->GetTargetLocation() ); +} + +//========================================================================== +// +// hhBeam::NodeAdd +// +//========================================================================== + +void hhBeam::NodeAdd(int index, idVec3 offset) { +#if DEBUG_BEAMS + VerifyNodeIndex(index,"hhBeam::NodeAdd"); +#endif + + nodeList[index] += offset; +} + +//========================================================================== +// +// hhBeam::NodeElectric +// +//========================================================================== + +void hhBeam::NodeElectric(float x, float y, float z, bool bNotEnds) { + int i; + int start = 0; + int end = system->decl->numNodes; + + if(bNotEnds) { + start = 1; + end = system->decl->numNodes - 1; + } + + for(i = start; i < end; i++) { + nodeList[i].x += system->random.CRandomFloat() * x; + nodeList[i].y += system->random.CRandomFloat() * y; + nodeList[i].z += system->random.CRandomFloat() * z; + } +} + +//========================================================================== +// +// hhBeam::TransformNodes +// +// Transform the worldspace nodes into model local space +//========================================================================== + +void hhBeam::TransformNodes(void) { + idVec3 origin = system->GetRenderEntity()->origin; + idMat3 axis = system->GetRenderEntity()->axis.Transpose(); + for(int i = 0; i < system->decl->numNodes; i++) { + nodeList[i] = ( nodeList[i] - origin ) * axis; + } +} + +//========================================================================== +// +// hhBeam::GetBounds +// +// Get local bounds of beam +//========================================================================== +idBounds hhBeam::GetBounds() { + idBounds bounds; + + if( !nodeList ) { + return bounds_zero; + } + + for( int ix = 0; ix < system->decl->numNodes; ++ix ) { + bounds.AddPoint( nodeList[ix] ); + } + + return bounds; +} + +//================ +//hhBeam::Save +//================ +void hhBeam::Save( idSaveGame *savefile ) const { + for( int i = 0; i < MAX_BEAM_SPLINE_CONTROLS + EXTRA_SPLINE_CONTROLS; i++ ) { + savefile->WriteVec3( splineList[i] ); + } +} + +//================ +//hhBeam::Restore +//================ +void hhBeam::Restore( idRestoreGame *savefile, hhBeamSystem *newSystem, hhBeamNodes_t *newInfo) { + system = newSystem; + nodeList = newInfo->nodes; + + for( int i = 0; i < MAX_BEAM_SPLINE_CONTROLS + EXTRA_SPLINE_CONTROLS; i++ ) { + savefile->ReadVec3( splineList[i] ); + } +} + +//============================================================================= +// +// CurveExtractSpline +// +//============================================================================= + +static idVec3 CurveExtractSpline(idVec3 *cPoints, int cPointCount, float alpha, bool bThruCtrlPnts ) { + float t, t2, t3, w1, w2, w3, w4; + int p1, p2, p3, p4, intAlpha; + idVec3 result; + + intAlpha = alpha; + p2 = intAlpha; + p1 = (p2 == 0) ? p2 : p2-1; + p3 = p2+1; + p4 = (p3 == cPointCount-1) ? p3 : p3+1; + t = alpha-intAlpha; + t2 = t * t; + t3 = t2 * t; + + if(!bThruCtrlPnts) { + w1 = (1-t) * (1-t) * (1-t); + w2 = 3.0f * t3 - 6.0f * t2 + 4; + w3 = -3.0f * t3 + 3.0f * t2 + 3.0f * t + 1; + w4 = t3; + return + ( w1 * cPoints[p1] + + w2 * cPoints[p2] + + w3 * cPoints[p3] + + w4 * cPoints[p4]) * (1.0f/6.0f); + } + + // Uses Catmull-Rom to pass thru the control points. + + if((cPoints[p3] - cPoints[p2]).Length() <= 4.0f) // was 16 + { // If points are close enough, just linearly interpolate + result = cPoints[p2] + t * (cPoints[p3] - cPoints[p2]); + } + else { + result = 0.5f * ((-cPoints[p1] + 3 * cPoints[p2] - 3 * cPoints[p3] + cPoints[p4]) * t3 + + (2 * cPoints[p1] - 5 * cPoints[p2]+ 4 * cPoints[p3] - cPoints[p4]) * t2 + + (-cPoints[p1] + cPoints[p3]) * t + 2 * cPoints[p2]); + } + + return(result); +} + +//============================================================================= +// +// hhBeamSystem::ExecuteBeam +// +// Applies the commands to the specified beam +//============================================================================= + +void hhBeamSystem::ExecuteBeam( int index, hhBeam *beam ) { + int i; + const beamCmd_t *cmd; + + for(i = 0; i < decl->cmds[index].Num(); i++) { + cmd = &decl->cmds[index][i]; + + switch( cmd->type ) { + case BEAMCMD_SplineLinearToTarget: + beam->SplineLinearToTarget(); + break; + case BEAMCMD_SplineArcToTarget: + beam->SplineArcToTarget(); + break; + case BEAMCMD_SplineAdd: + beam->SplineAdd( cmd->index, cmd->offset ); + break; + case BEAMCMD_SplineAddSin: + beam->SplineAddSin( cmd->index, cmd->phase.x, cmd->phase.y, cmd->phase.z, cmd->offset.x, cmd->offset.y, cmd->offset.z ); + break; + case BEAMCMD_SplineAddSinTim: + beam->SplineAddSinTime( cmd->index, cmd->phase.x, cmd->phase.y, cmd->phase.z, cmd->offset.x, cmd->offset.y, cmd->offset.z ); + break; + case BEAMCMD_SplineAddSinTimeScaled: + beam->SplineAddSinTimeScaled( cmd->index, cmd->phase.x, cmd->phase.y, cmd->phase.z, cmd->offset.x, cmd->offset.y, cmd->offset.z ); + break; + case BEAMCMD_ConvertSplineToNodes: + beam->ConvertSplineToNodes(); + break; + case BEAMCMD_NodeLinearToTarget: + beam->NodeLinearToTarget(); + break; + case BEAMCMD_NodeElectric: + beam->NodeElectric( cmd->offset.x, cmd->offset.y, cmd->offset.z, true ); + break; + } + } +} + +//================ +//hhBeamSystem::Save +//================ +void hhBeamSystem::Save( idSaveGame *savefile ) const { + savefile->WriteString( declName ); + savefile->WriteInt( random.GetSeed() ); + savefile->WriteFloat( beamTime ); + + for( int i = 0; i < decl->numBeams; i++) { + beamList[i].Save( savefile ); + } + + savefile->WriteMat3( beamAxis ); + savefile->WriteVec3( arcVector ); + savefile->WriteVec3( targetLocation ); + targetEntity.Save( savefile ); + savefile->WriteVec3( targetEntityOffset ); + savefile->WriteInt( targetEntityId ); + savefile->WriteFloat( beamLength ); + savefile->WriteBool( bRigidBeamLength ); + savefile->WriteFloat( phaseScale ); + savefile->WriteFloat( offsetScale ); + savefile->WriteBool( bActive ); +} + +//================ +//hhBeamSystem::Restore +//================ +void hhBeamSystem::Restore( idRestoreGame *savefile ) { + savefile->ReadString( declName ); + + // Initialize beam data from the beam file + decl = declManager->FindBeam( declName ); + HH_ASSERT( renderEntity.declBeam == decl ); + + // Initialize the random number generator + int seed; + savefile->ReadInt( seed ); + random.SetSeed( seed ); + + savefile->ReadFloat( beamTime ); + + // Allocate the beams + if( beamList ) { + delete[] beamList; + } + beamList = new hhBeam[decl->numBeams]; + + for( int i = 0; i < decl->numBeams; i++) { + beamList[i].Restore( savefile, this, &renderEntity.beamNodes[i]); + } + + savefile->ReadMat3( beamAxis ); + savefile->ReadVec3( arcVector ); + savefile->ReadVec3( targetLocation ); + targetEntity.Restore( savefile ); + savefile->ReadVec3( targetEntityOffset ); + savefile->ReadInt( targetEntityId ); + savefile->ReadFloat( beamLength ); + savefile->ReadBool( bRigidBeamLength ); + savefile->ReadFloat( phaseScale ); + savefile->ReadFloat( offsetScale ); + savefile->ReadBool( bActive ); + + Activate(bActive); +} + diff --git a/src/Prey/prey_beam.h b/src/Prey/prey_beam.h new file mode 100644 index 0000000..191cb4e --- /dev/null +++ b/src/Prey/prey_beam.h @@ -0,0 +1,206 @@ + +#ifndef __PREY_BEAM_H__ +#define __PREY_BEAM_H__ + +#define MAX_BEAM_SPLINE_CONTROLS 6 +#define BEAM_SPLINE_CONTROL_STEP ( 1.0f / ( MAX_BEAM_SPLINE_CONTROLS - 1 ) ) +#define EXTRA_SPLINE_CONTROLS 2 + +#define BEAM_HZ 30 +#define BEAM_MSEC (1000 / BEAM_HZ) + +extern const idEventDef EV_ToggleBeamLength; + +class hhBeamSystem; +class hhBeam; +class hhDeclBeam; + +/* +class beamCmd { +public: + virtual void Execute( hhBeam *beam ); +}; + +// BEAM COMMANDS +class beamCmd_SplineLinearToTarget : public beamCmd { +public: + void Execute( hhBeam *beam ); +}; + +class beamCmd_SplineArcToTarget : public beamCmd { +public: + void Execute( hhBeam *beam ); +}; + +class beamCmd_SplineAdd : public beamCmd { +public: + void Execute( hhBeam *beam ); + + int index; + idVec3 offset; +}; + +class beamCmd_SplineAddSin : public beamCmd_SplineAdd { +public: + void Execute( hhBeam *beam ); + + idVec3 phase; +}; + +class beamCmd_SplineAddSinTime : public beamCmd_SplineAddSin { +public: + void Execute( hhBeam *beam ); +}; + +class beamCmd_SplineAddSinTimeScaled : public beamCmd_SplineAddSin { +public: + void Execute( hhBeam *beam ); +}; + +class beamCmd_ConvertSplineToNodes : public beamCmd { +public: + void Execute( hhBeam *beam ); +}; + +class beamCmd_NodeLinearToTarget : public beamCmd { +public: + void Execute( hhBeam *beam ); +}; + +class beamCmd_NodeElectric : public beamCmd { +public: + void Execute( hhBeam *beam ); + + idVec3 offset; + bool bNotEnds; +}; +// END BEAM COMMANDS +*/ + +class hhBeam { +public: + hhBeam(); + ~hhBeam(); + void Init( hhBeamSystem *newSystem, hhBeamNodes_t *newInfo ); + + idBounds GetBounds(); + + void VerifyNodeIndex(int index, const char *functionName); + void VerifySplineIndex(int index, const char *functionName); + idVec3 NodeGet(int index); + void NodeSet(int index, idVec3 value); + idVec3 SplineGet(int index); + void SplineSet(int index, idVec3 value); + + void SplineLinear(idVec3 start, idVec3 end); + void SplineLinearToTarget( void ); + void SplineArc( idVec3 start, idVec3 end, idVec3 startVec ); + void SplineArcToTarget( void ); + + void SplineAddSin(int index, float phaseX, float phaseY, float phaseZ, float offsetX, float offsetY, float offsetZ); + void SplineAddSinTime(int index, float phaseX, float phaseY, float phaseZ, float offsetX, float offsetY, float offsetZ); + void SplineAddSinTimeScaled(int index, float phaseX, float phaseY, float phaseZ, float offsetX, float offsetY, float offsetZ); + void SplineAdd(int index, idVec3 offset); + void ConvertSplineToNodes(void); + + void NodeLinear(idVec3 start, idVec3 end); + void NodeLinearToTarget( void ); + void NodeAdd(int index, idVec3 offset); + void NodeElectric(float x, float y, float z, bool bNotEnds); + + void TransformNodes(void); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile, hhBeamSystem *newSystem, hhBeamNodes_t *newInfo ); + +protected: + idVec3 *nodeList; // index into renderEntity nodes + hhBeamSystem *system; + idVec3 splineList[MAX_BEAM_SPLINE_CONTROLS + EXTRA_SPLINE_CONTROLS]; +}; + + +class hhBeamSystem : public idEntity { +public: + CLASS_PROTOTYPE( hhBeamSystem ); + + void Spawn(void); + hhBeamSystem(); + ~hhBeamSystem(); + + virtual void Think(void); + virtual void UpdateModel( void ); + + //rww - network friendliness + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + virtual void InitSystem( const char* modelName ); + virtual float GetBeamTime( void ) { return beamTime; } + virtual void SetTargetLocation(idVec3 newLoc); + virtual void SetTargetEntity( idEntity *ent, int traceId=0, idVec3 &offset=vec3_origin ); + virtual void SetTargetEntity( idEntity *ent, const char *boneName, idVec3 &offset=vec3_origin ); + + virtual bool IsActivated() const { return bActive; } + virtual void Activate( const bool bActivate ); + + virtual idVec3 GetTargetLocation( void ); + virtual idMat3 GetBeamAxis( void ) { return( beamAxis ); } + + virtual void SetArcVector( idVec3 vec ) { arcVector = vec; } + virtual idVec3 GetArcVector( void ) { return( arcVector ); } + + virtual float GetBeamPhaseScale( void ) { return phaseScale; } + virtual void SetBeamPhaseScale( float scale ) { phaseScale = scale; } + virtual float GetBeamOffsetScale( void ) { return offsetScale; } + virtual void SetBeamOffsetScale( float scale ) { offsetScale = scale; } + + //AOB + virtual void SetAxis( const idMat3 &axis ); + virtual float GetBeamLength( void ) { return( beamLength ); } + virtual void SetBeamLength( const float length ); + virtual void ToggleBeamLength( bool rigid ) { bRigidBeamLength = rigid; } + + virtual void ExecuteBeam( int index, hhBeam *beam ); + + static hhBeamSystem* SpawnBeam( const idVec3& start, const char* modelName, const idMat3& axis = mat3_identity, bool pureLocal = false ); + + idRandom random; + idStr declName; + const hhDeclBeam *decl; + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - for functionality regarding client entity beams. + idEntityPtr snapshotOwner; +protected: + virtual void Event_Activate( idEntity *activator ); + void Event_SetBeamPhaseScale( float scale ); // bg + void Event_SetBeamOffsetScale( float scale ); // bg + + float beamTime; + hhBeam *beamList; + + idMat3 beamAxis; + idVec3 arcVector; // For SplineArc + + idVec3 targetLocation; + idEntityPtr targetEntity; + idVec3 targetEntityOffset; + int targetEntityId; + + //AOB + float beamLength; + bool bRigidBeamLength; + + // Used to scale the phase/offset of the beam in code + float phaseScale; + float offsetScale; + +protected: + bool bActive; +}; + +#endif /* __PREY_BEAM_H__ */ diff --git a/src/Prey/prey_bonecontroller.cpp b/src/Prey/prey_bonecontroller.cpp new file mode 100644 index 0000000..86bb669 --- /dev/null +++ b/src/Prey/prey_bonecontroller.cpp @@ -0,0 +1,247 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/********************************************************************** + +hhBoneController + +**********************************************************************/ + +/* +================ +hhBoneController::hhBoneController +================ +*/ +hhBoneController::hhBoneController() { + m_JointHandle = INVALID_JOINT; + m_pOwner = NULL; + + m_Factor.Set(1.0f, 1.0f, 1.0f); + m_TurnRate.Set(0.0f, 0.0f, 0.0f); + + m_IdealAng.Zero(); + m_CurrentAng.Zero(); + m_MinAngles.Zero(); + m_MaxAngles.Zero(); + m_Fraction.Zero(); + m_MaxDelta.Zero(); +} + +/* +================ +hhBoneController::Setup +================ +*/ +void hhBoneController::Setup( idEntity *pOwner, const char *pJointname, idAngles &MinAngles, idAngles &MaxAngles, idAngles& Rate, idAngles& Factor ) { + jointHandle_t Joint = ( pOwner ) ? pOwner->GetAnimator()->GetJointHandle( pJointname ) : INVALID_JOINT; + + Setup( pOwner, Joint, MinAngles, MaxAngles, Rate, Factor ); +} + +/* +================ +hhBoneController::Setup +================ +*/ +void hhBoneController::Setup( idEntity *pOwner, jointHandle_t Joint, idAngles &MinAngles, idAngles &MaxAngles, idAngles& Rate, idAngles& Factor ) { + m_IdealAng.Zero(); + m_CurrentAng.Zero(); + m_Fraction.Zero(); + + m_JointHandle = Joint; + + m_MinAngles = MinAngles; + m_MaxAngles = MaxAngles; + m_Factor = Factor; + m_TurnRate = Rate; + m_pOwner = pOwner; +} + +/* +================ +hhBoneController::Update +================ +*/ +void hhBoneController::Update( int iCurrentTime ) { + idAngles Delta; + int iIndex; + idAngles Turn; + idAngles Angle; + + if ( !m_pOwner || + ( m_JointHandle == INVALID_JOINT ) ) { + return; + } + + Turn = m_TurnRate * MS2SEC(gameLocal.msec) * gameLocal.GetTimeScale(); + + Delta = m_IdealAng - m_CurrentAng; + for( iIndex = 0; iIndex < 3; ++iIndex ) { + if ( Delta[ iIndex ] > Turn[iIndex] ) { + Delta[ iIndex ] = Turn[iIndex]; + } + if ( Delta[ iIndex ] < -Turn[iIndex] ) { + Delta[ iIndex ] = -Turn[iIndex]; + } + } + + m_CurrentAng += Delta; + for(iIndex = 0; iIndex < 3; ++iIndex) { + Angle[iIndex] = m_CurrentAng[iIndex] * m_Factor[iIndex]; + } + + m_pOwner->GetAnimator()->SetJointAxis( m_JointHandle, JOINTMOD_WORLD, Angle.ToMat3() ); + + m_Fraction.pitch = 1.0f - ( (m_MaxDelta.pitch > VECTOR_EPSILON) ? idMath::Fabs((m_IdealAng.pitch - m_CurrentAng.pitch)) / m_MaxDelta.pitch : 0.0f ); + m_Fraction.yaw = 1.0f - ( (m_MaxDelta.yaw > VECTOR_EPSILON) ? idMath::Fabs((m_IdealAng.yaw - m_CurrentAng.yaw)) / m_MaxDelta.yaw : 0.0f ); + m_Fraction.roll = 1.0f - ( (m_MaxDelta.roll > VECTOR_EPSILON) ? idMath::Fabs((m_IdealAng.roll - m_CurrentAng.roll)) / m_MaxDelta.roll : 0.0f ); +} + +/* +================ +hhBoneController::TurnTo +================ +*/ +bool hhBoneController::TurnTo( idAngles &Target ) { + m_IdealAng = Target; + bool bClampAngles = !ClampAngles(); + + m_MaxDelta.pitch = idMath::Fabs(m_IdealAng.pitch - m_CurrentAng.pitch); + m_MaxDelta.yaw = idMath::Fabs(m_IdealAng.yaw - m_CurrentAng.yaw); + m_MaxDelta.roll = idMath::Fabs(m_IdealAng.roll - m_CurrentAng.roll); + + return bClampAngles; +} + +/* +================ +hhBoneController::AimAt +================ +*/ +bool hhBoneController::AimAt( idVec3 &Target ) { + idVec3 dir; + idVec3 localDir; + + if ( !m_pOwner ) { + return false; + } + + dir = Target - m_pOwner->GetOrigin(); + dir.Normalize(); + + m_pOwner->GetAxis().ProjectVector( dir, localDir ); + + m_IdealAng.yaw = idMath::AngleNormalize180( localDir.ToYaw() ); + m_IdealAng.pitch = -idMath::AngleNormalize180( localDir.ToPitch() ); + + bool bClampAngles = !ClampAngles(); + + m_MaxDelta.pitch = idMath::Fabs(m_IdealAng.pitch - m_CurrentAng.pitch); + m_MaxDelta.yaw = idMath::Fabs(m_IdealAng.yaw - m_CurrentAng.yaw); + + return bClampAngles; +} + +/* +===================== +hhBoneController::ClampAngles +===================== +*/ +bool hhBoneController::ClampAngles( void ) { + int i; + bool clamp; + + clamp = false; + for( i = 0; i < 3; i++ ) { + if ( m_IdealAng[ i ] > m_MaxAngles[ i ] ) { + m_IdealAng[ i ] = m_MaxAngles[ i ]; + clamp = true; + } + if ( m_IdealAng[ i ] < m_MinAngles[ i ] ) { + m_IdealAng[ i ] = m_MinAngles[ i ]; + clamp = true; + } + } + + return clamp; +} + +/* +===================== +hhBoneController::SetRotationFactor +===================== +*/ +void hhBoneController::SetRotationFactor(idAngles& RotationFactor) { + m_Factor = RotationFactor; +} + +/* +===================== +hhBoneController::IsFinishedMoving +===================== +*/ +bool hhBoneController::IsFinishedMoving(int iAxis) { + return (m_IdealAng[iAxis] == m_CurrentAng[iAxis]); +} + +/* +===================== +hhBoneController::IsFinishedMoving +===================== +*/ +bool hhBoneController::IsFinishedMoving() { + return (m_IdealAng == m_CurrentAng); +} + +/* +===================== +hhBoneController::AdjustScanRateToLinearizeBonePath +===================== +*/ +void hhBoneController::AdjustScanRateToLinearizeBonePath(float fLinearScanRate) { + float fDeltaPitch = idMath::Fabs( (m_IdealAng.pitch - m_CurrentAng.pitch) * m_Factor.pitch ); + float fDeltaYaw = idMath::Fabs( (m_IdealAng.yaw - m_CurrentAng.yaw) * m_Factor.yaw ); + + float fHypotenuse = idMath::Sqrt(fDeltaPitch * fDeltaPitch + fDeltaYaw * fDeltaYaw); + if(fHypotenuse) { + float fFrac = fLinearScanRate / fHypotenuse; + + m_TurnRate.pitch = fDeltaPitch * fFrac; + m_TurnRate.yaw = fDeltaYaw * fFrac; + } +} + +//================ +//hhBoneController::Save +//================ +void hhBoneController::Save( idSaveGame *savefile ) const { + savefile->WriteAngles( m_Fraction ); + savefile->WriteAngles( m_MaxDelta ); + savefile->WriteInt( m_JointHandle ); + savefile->WriteAngles( m_IdealAng ); + savefile->WriteAngles( m_CurrentAng ); + savefile->WriteAngles( m_MinAngles ); + savefile->WriteAngles( m_MaxAngles ); + savefile->WriteAngles( m_Factor ); + savefile->WriteAngles( m_TurnRate ); + savefile->WriteObject( m_pOwner ); +} + +//================ +//hhBoneController::Restore +//================ +void hhBoneController::Restore( idRestoreGame *savefile ) { + savefile->ReadAngles( m_Fraction ); + savefile->ReadAngles( m_MaxDelta ); + savefile->ReadInt( reinterpret_cast ( m_JointHandle ) ); + savefile->ReadAngles( m_IdealAng ); + savefile->ReadAngles( m_CurrentAng ); + savefile->ReadAngles( m_MinAngles ); + savefile->ReadAngles( m_MaxAngles ); + savefile->ReadAngles( m_Factor ); + savefile->ReadAngles( m_TurnRate ); + savefile->ReadObject( reinterpret_cast ( m_pOwner ) ); +} + diff --git a/src/Prey/prey_bonecontroller.h b/src/Prey/prey_bonecontroller.h new file mode 100644 index 0000000..4bfffe2 --- /dev/null +++ b/src/Prey/prey_bonecontroller.h @@ -0,0 +1,57 @@ +#ifndef __HH_BONE_CONTROLLER_H +#define __HH_BONE_CONTROLLER_H + +/*********************************************************************** + + hhBoneController + +***********************************************************************/ + +class hhBoneController { + public: + hhBoneController(); + + void SetTurnRate( idAngles& Rate ) { m_TurnRate = Rate; } + const idAngles& GetTurnRate() const { return m_TurnRate; } + + void SetRotationFactor( idAngles& RotationFactor ); + const idAngles& GetRotationFactor() const { return m_Factor; } + + void Setup( idEntity *pOwner, const char *pJointname, idAngles &MinAngles, idAngles &MaxAngles, idAngles& Rate, idAngles& Factor ); + void Setup( idEntity *pOwner, jointHandle_t Joint, idAngles &MinAngles, idAngles &MaxAngles, idAngles& Rate, idAngles& Factor ); + void Update( int iCurrentTime ); + bool TurnTo( idAngles &Target ); + bool AimAt( idVec3 &Target ); + bool IsFinishedMoving( int iAxis ); + bool IsFinishedMoving(); + + void AdjustScanRateToLinearizeBonePath( float fLinearScanRate ); + + bool Add( idAngles &Ang ) { return TurnTo( Ang + m_IdealAng ); }; + void Clear( void ) { m_IdealAng.Zero(); }; + const idAngles &CurrentAngles() const { return m_CurrentAng; }; + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + bool ClampAngles( void ); + + public: + idAngles m_Fraction; + + protected: + idAngles m_MaxDelta; + + jointHandle_t m_JointHandle; + idAngles m_IdealAng; + idAngles m_CurrentAng; + idAngles m_MinAngles; + idAngles m_MaxAngles; + + idAngles m_Factor; + idAngles m_TurnRate; + idEntity *m_pOwner; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_camerainterpolator.cpp b/src/Prey/prey_camerainterpolator.cpp new file mode 100644 index 0000000..c3ff174 --- /dev/null +++ b/src/Prey/prey_camerainterpolator.cpp @@ -0,0 +1,672 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define CAMERA_INTERP_LERP_DEBUG if( p_camInterpDebug.GetInteger() == 1 ) gameLocal.Printf +#define CAMERA_INTERP_SLERP_DEBUG if( p_camInterpDebug.GetInteger() == 2 ) gameLocal.Printf + +/********************************************************************** + +hhCameraInterpolator + +**********************************************************************/ + +/* +================ +NoInterpEvaluate +================ +*/ +float NoInterpEvaluate( float& interpVal, float deltaVal ) { + return 1.0f; +} + +/* +================ +VariableMidPointSinusoidalEvaluate +================ +*/ +float VariableMidPointSinusoidalEvaluate( float& interpVal, float deltaVal ) { + interpVal = hhMath::ClampFloat( 0.0f, 1.0f, interpVal + deltaVal ); + + if( interpVal <= 0.0f || interpVal >= 1.0f ) { + return interpVal; + } + + float debug = hhMath::Sin( DEG2RAD(hhMath::MidPointLerp(0.0f, 60.0f, 90.0f, interpVal)) ); + return debug; +} + +/* +================ +LinearEvaluate +================ +*/ +float LinearEvaluate( float& interpVal, float deltaVal ) { + interpVal = hhMath::ClampFloat( 0.0f, 1.0f, interpVal + deltaVal ); + + if( interpVal <= 0.0f || interpVal >= 1.0f ) { + return interpVal; + } + + return interpVal; +} + +/* +================ +InverseEvaluate +================ +*/ +float InverseEvaluate( float& interpVal, float deltaVal ) { + interpVal = hhMath::ClampFloat( 0.0f, 1.0f, interpVal + deltaVal ); + + if( interpVal <= 0.0f || interpVal >= 1.0f ) { + return interpVal; + } + + const float minLerpVal = deltaVal; + if( deltaVal <= 0.0f ) { + return interpVal; + } + + const float scale = (1.0f / minLerpVal) - 1.0f; + float debug = ((1.0f / interpVal) - 1.0f) / scale; + return debug; +} + +/* +================ +hhCameraInterpolator::hhCameraInterpolator +================ +*/ +hhCameraInterpolator::hhCameraInterpolator() : + clipBounds( idBounds(idVec3(-1, -1, 10), idVec3(1, 1, 12)) ) { + + ClearFuncList(); + RegisterFunc( NoInterpEvaluate, IT_None ); + RegisterFunc( VariableMidPointSinusoidalEvaluate, IT_VariableMidPointSinusoidal ); + RegisterFunc( LinearEvaluate, IT_Linear ); + RegisterFunc( InverseEvaluate, IT_Inverse ); + + SetSelf( NULL ); + Setup( 0.0f, IT_None ); + Reset( vec3_origin, mat3_identity[2], 0.0f ); +} + +/* +================ +hhCameraInterpolator::SetSelf +================ +*/ +void hhCameraInterpolator::SetSelf( hhPlayer* self ) { + this->self = self; + clipBounds.SetOwner( self ); +} + +/* +================ +hhCameraInterpolator::GetCurrentEyeHeight +================ +*/ +float hhCameraInterpolator::GetCurrentEyeHeight() const { + return eyeOffsetInfo.current; +} + +/* +================ +hhCameraInterpolator::GetIdealEyeHeight +================ +*/ +float hhCameraInterpolator::GetIdealEyeHeight() const { + return eyeOffsetInfo.end; +} + +/* +================ +hhCameraInterpolator::GetCurrentEyeOffset +================ +*/ +idVec3 hhCameraInterpolator::GetCurrentEyeOffset() const { + return GetCurrentUpVector() * GetCurrentEyeHeight(); +} + +/* +================ +hhCameraInterpolator::GetEyePosition +================ +*/ +idVec3 hhCameraInterpolator::GetEyePosition() const { + return GetCurrentPosition() + GetCurrentEyeOffset(); +} + +/* +================ +hhCameraInterpolator::GetCurrentPosition +================ +*/ +idVec3 hhCameraInterpolator::GetCurrentPosition() const { + return positionInfo.current; +} + +/* +================ +hhCameraInterpolator::GetIdealPosition +================ +*/ +idVec3 hhCameraInterpolator::GetIdealPosition() const { + return positionInfo.end; +} + +/* +================ +hhCameraInterpolator::GetCurrentUpVector +================ +*/ +idVec3 hhCameraInterpolator::GetCurrentUpVector() const { + return GetCurrentAxis()[2]; +} + +/* +================ +hhCameraInterpolator::GetCurrentAxis +================ +*/ +idMat3 hhCameraInterpolator::GetCurrentAxis() const { + return GetCurrentRotation().ToMat3(); +} + +/* +================ +hhCameraInterpolator::GetCurrentAngles +================ +*/ +idAngles hhCameraInterpolator::GetCurrentAngles() const { + return GetCurrentRotation().ToAngles(); +} + +/* +================ +hhCameraInterpolator::GetCurrentRotation +================ +*/ +idQuat hhCameraInterpolator::GetCurrentRotation() const { + return rotationInfo.current; +} + +/* +================ +hhCameraInterpolator::GetIdealUpVector +================ +*/ +idVec3 hhCameraInterpolator::GetIdealUpVector() const { + return GetIdealAxis()[2]; +} + +/* +================ +hhCameraInterpolator::GetIdealAxis +================ +*/ +idMat3 hhCameraInterpolator::GetIdealAxis() const { + return GetIdealRotation().ToMat3(); +} + +/* +================ +hhCameraInterpolator::GetIdealAngles +================ +*/ +idAngles hhCameraInterpolator::GetIdealAngles() const { + return GetIdealRotation().ToAngles(); +} + +/* +================ +hhCameraInterpolator::GetIdealRotation +================ +*/ +idQuat hhCameraInterpolator::GetIdealRotation() const { + return rotationInfo.end; +} + +/* +================ +hhCameraInterpolator::UpdateViewAngles +================ +*/ +idAngles hhCameraInterpolator::UpdateViewAngles( const idAngles& viewAngles ) { + return (viewAngles.ToMat3() * GetCurrentAxis()).ToAngles(); +} + +/* +================ +hhCameraInterpolator::UpdateTarget +================ +*/ +void hhCameraInterpolator::UpdateTarget( const idVec3& idealPos, const idMat3& idealAxis, float eyeOffset, int interpFlags ) { + if( !positionInfo.end.Compare(idealPos, VECTOR_EPSILON) ) { + SetTargetPosition( idealPos, interpFlags ); + } + + if( !idealAxis[2].Compare(GetIdealUpVector(), VECTOR_EPSILON) ) { + SetTargetAxis( idealAxis, interpFlags ); + } + + if( hhMath::Fabs(eyeOffset - hhMath::Fabs(eyeOffsetInfo.end)) >= VECTOR_EPSILON ) { + SetTargetEyeOffset( eyeOffset, interpFlags ); + } +} + +/* +================ +hhCameraInterpolator::SetTargetPosition +================ +*/ +void hhCameraInterpolator::SetTargetPosition( const idVec3& idealPos, int interpFlags ) { + if( interpFlags & INTERPOLATE_POSITION ) { + positionInfo.Set( idealPos ); + } else { + positionInfo.start = idealPos - (positionInfo.end - positionInfo.start); + positionInfo.current = idealPos - (positionInfo.end - positionInfo.current); + positionInfo.end = idealPos; + } +} + +/* +================ +hhCameraInterpolator::SetTargetAxis +================ +*/ +void hhCameraInterpolator::SetTargetAxis( const idMat3& idealAxis, int interpFlags ) { + idQuat cachedRotation; + idQuat idealRotation = DetermineIdealRotation( idealAxis[2] ); + if( interpFlags & INTERPOLATE_ROTATION ) { + rotationInfo.Set( idealRotation ); + } else { + cachedRotation = idealRotation * rotationInfo.end.Inverse(); + rotationInfo.start = cachedRotation * rotationInfo.start; + rotationInfo.current = cachedRotation * rotationInfo.current; + rotationInfo.end = idealRotation; + } +} + +/* +================ +hhCameraInterpolator::SetTargetEyeOffset +================ +*/ +void hhCameraInterpolator::SetTargetEyeOffset( float idealEyeOffset, int interpFlags ) { + if( interpFlags & INTERPOLATE_EYEOFFSET ) { + eyeOffsetInfo.Set( idealEyeOffset ); + } +} + +/* +================ +hhCameraInterpolator::SetInterpolationType +================ +*/ +InterpolationType hhCameraInterpolator::SetInterpolationType( InterpolationType type ) { + InterpolationType cachedType = interpType; + + interpType = type; + + return cachedType; +} + +/* +================ +hhCameraInterpolator::Setup +================ +*/ +void hhCameraInterpolator::Setup( const float lerpScale, const InterpolationType type ) { + this->lerpScale = hhMath::hhMax( 0.01f, lerpScale ); + SetInterpolationType( type ); +} + +/* +================ +hhCameraInterpolator::Reset +================ +*/ +void hhCameraInterpolator::Reset( const idVec3& position, const idVec3& idealUpVector, float eyeOffset ) { + positionInfo.Reset( position ); + rotationInfo.Reset( DetermineIdealRotation(idealUpVector) ); + eyeOffsetInfo.Reset( eyeOffset ); +} + +/* +================ +hhCameraInterpolator::DetermineIdealRotation +================ +*/ +idQuat hhCameraInterpolator::DetermineIdealRotation( const idVec3& idealUpVector, const idVec3& viewDir, const idMat3& untransformedViewAxis ) { + idMat3 mat; + idVec3 newViewVector( viewDir ); + + newViewVector.ProjectOntoPlane( idealUpVector ); + if( newViewVector.LengthSqr() < VECTOR_EPSILON ) { + newViewVector = -Sign( newViewVector * idealUpVector ); + } + + newViewVector.Normalize(); + mat[0] = newViewVector; + mat[1] = idealUpVector.Cross( newViewVector ); + mat[2] = idealUpVector; + + mat = untransformedViewAxis.Transpose() * mat; + return mat.ToQuat(); +} + +/* +================ +hhCameraInterpolator::DetermineIdealRotation +================ +*/ +idQuat hhCameraInterpolator::DetermineIdealRotation( const idVec3& idealUpVector ) { + if( !self ) { + return mat3_identity.ToQuat(); + } + + return DetermineIdealRotation( idealUpVector, self->GetAxis()[0], self->GetUntransformedViewAxis() ); +} + +/* +================ +hhCameraInterpolator::ClearFuncList +================ +*/ +void hhCameraInterpolator::ClearFuncList() { + funcList.SetNum( IT_NumTypes ); + + for( int ix = 0; ix < funcList.Num(); ++ix ) { + funcList[ix] = NULL; + } +} + +/* +================ +hhCameraInterpolator::RegisterFunc +================ +*/ +void hhCameraInterpolator::RegisterFunc( InterpFunc func, InterpolationType type ) { + int index = (int)type; + + if( funcList[index] ) { + gameLocal.Warning( "Function already registered for interpolation type %d", type ); + } + + funcList[index] = func; +} + +/* +================ +hhCameraInterpolator::DetermineFunc +================ +*/ +InterpFunc hhCameraInterpolator::DetermineFunc( InterpolationType type ) { + int index = (int)( p_disableCamInterp.GetBool() ? IT_None : type ); + return funcList[ index ]; +} + +/* +================ +hhCameraInterpolator::Evaluate +================ +*/ +void hhCameraInterpolator::Evaluate( float deltaTime ) { + float baseTime = deltaTime * gameLocal.GetTimeScale(); + float scaledDeltaTime = baseTime * lerpScale; + float lenFactor = (positionInfo.current-positionInfo.end).Length()*0.2f; + if (lenFactor < 1.0f) { + lenFactor = 1.0f; + } + float scaledPosDeltaTime = baseTime * (lerpScale*lenFactor); + float scaledEyeDeltaTime = baseTime * (lerpScale*4.0f); + + InterpFunc func = DetermineFunc( interpType ); + if( !func ) { + return; + } + + EvaluatePosition( func(positionInfo.interpVal, scaledPosDeltaTime) ); + EvaluateRotation( func(rotationInfo.interpVal, scaledDeltaTime) ); + EvaluateEyeOffset( func(eyeOffsetInfo.interpVal, scaledEyeDeltaTime) ); + + VerifyEyeOffset( eyeOffsetInfo.current ); +} + +/* +================ +hhCameraInterpolator::EvaluatePosition +================ +*/ +void hhCameraInterpolator::EvaluatePosition( float interpVal ) { + if( interpVal >= 1.0f ) { + positionInfo.Reset( positionInfo.end ); + return; + } + + positionInfo.current.Lerp( positionInfo.start, positionInfo.end, interpVal ); +} + +/* +================ +hhCameraInterpolator::EvaluateRotation +================ +*/ +void hhCameraInterpolator::EvaluateRotation( float interpVal ) { + if( interpVal >= 1.0f ) { + rotationInfo.Reset( rotationInfo.end ); + return; + } + + rotationInfo.current.Slerp( rotationInfo.start, rotationInfo.end, interpVal ); +} + +/* +================ +hhCameraInterpolator::EvaluateEyeOffset +================ +*/ +void hhCameraInterpolator::EvaluateEyeOffset( float interpVal ) { + if( interpVal >= 1.0f ) { + eyeOffsetInfo.Reset( eyeOffsetInfo.end ); + return; + } + + eyeOffsetInfo.current = hhMath::Lerp( eyeOffsetInfo.start, eyeOffsetInfo.end, interpVal ); +} + +/* +================ +hhCameraInterpolator::VerifyEyeOffset +================ +*/ +void hhCameraInterpolator::VerifyEyeOffset( float& eyeOffset ) { + idPhysics* selfPhysics = NULL; + + if( !self ) { + return; + } + + selfPhysics = self->GetPhysics(); + if( !selfPhysics ) { + return; + } + + if( clipBounds.GetBounds().Translate(GetCurrentUpVector() * eyeOffset).IntersectsBounds(selfPhysics->GetBounds()) ) { + return; + } + + trace_t trace; + gameLocal.clip.Translation( trace, GetCurrentPosition(), GetCurrentPosition() + GetCurrentUpVector() * eyeOffset, &clipBounds, GetCurrentAxis(), selfPhysics->GetClipMask(), NULL ); + eyeOffset *= trace.fraction; +} + +//================ +//hhCameraInterpolator::Save +//================ +void hhCameraInterpolator::Save( idSaveGame *savefile ) const { + savefile->WriteQuat( rotationInfo.start ); + savefile->WriteQuat( rotationInfo.end ); + savefile->WriteQuat( rotationInfo.current ); + savefile->WriteFloat( rotationInfo.interpVal ); + + savefile->WriteVec3( positionInfo.start ); + savefile->WriteVec3( positionInfo.end ); + savefile->WriteVec3( positionInfo.current ); + savefile->WriteFloat( positionInfo.interpVal ); + + savefile->WriteFloat( eyeOffsetInfo.start ); + savefile->WriteFloat( eyeOffsetInfo.end ); + savefile->WriteFloat( eyeOffsetInfo.current ); + savefile->WriteFloat( eyeOffsetInfo.interpVal ); + + savefile->WriteInt( reinterpret_cast ( interpType ) ); + savefile->WriteFloat( lerpScale ); + savefile->WriteObject( self ); + + clipBounds.Save( savefile ); +} + +//================ +//hhCameraInterpolator::Restore +//================ +void hhCameraInterpolator::Restore( idRestoreGame *savefile ) { + savefile->ReadQuat( rotationInfo.start ); + savefile->ReadQuat( rotationInfo.end ); + savefile->ReadQuat( rotationInfo.current ); + savefile->ReadFloat( rotationInfo.interpVal ); + + savefile->ReadVec3( positionInfo.start ); + savefile->ReadVec3( positionInfo.end ); + savefile->ReadVec3( positionInfo.current ); + savefile->ReadFloat( positionInfo.interpVal ); + + savefile->ReadFloat( eyeOffsetInfo.start ); + savefile->ReadFloat( eyeOffsetInfo.end ); + savefile->ReadFloat( eyeOffsetInfo.current ); + savefile->ReadFloat( eyeOffsetInfo.interpVal ); + + savefile->ReadInt( reinterpret_cast ( interpType ) ); + savefile->ReadFloat( lerpScale ); + savefile->ReadObject( reinterpret_cast ( self ) ); + + clipBounds.Restore( savefile ); +} + +//================ +//hhCameraInterpolator::WriteToSnapshot +//================ +void hhCameraInterpolator::WriteToSnapshot( idBitMsgDelta &msg, const hhPlayer *pl ) const +{ + idCQuat sq, q; + + idVec3 plPos = ((idPlayer *)pl)->GetPlayerPhysics()->GetOrigin(); +#if 1 + sq = rotationInfo.start.ToCQuat(); + msg.WriteFloat(sq.x); + msg.WriteFloat(sq.y); + msg.WriteFloat(sq.z); + q = rotationInfo.current.ToCQuat(); + msg.WriteDeltaFloat(sq.x, q.x); + msg.WriteDeltaFloat(sq.y, q.y); + msg.WriteDeltaFloat(sq.z, q.z); + q = rotationInfo.end.ToCQuat(); + msg.WriteDeltaFloat(sq.x, q.x); + msg.WriteDeltaFloat(sq.y, q.y); + msg.WriteDeltaFloat(sq.z, q.z); + msg.WriteFloat(rotationInfo.interpVal, 4, 4); + + msg.WriteDeltaFloat(plPos[0], positionInfo.start[0]); + msg.WriteDeltaFloat(plPos[1], positionInfo.start[1]); + msg.WriteDeltaFloat(plPos[2], positionInfo.start[2]); + msg.WriteDeltaFloat(positionInfo.start[0], positionInfo.current[0]); + msg.WriteDeltaFloat(positionInfo.start[1], positionInfo.current[1]); + msg.WriteDeltaFloat(positionInfo.start[2], positionInfo.current[2]); + msg.WriteDeltaFloat(positionInfo.start[0], positionInfo.end[0]); + msg.WriteDeltaFloat(positionInfo.start[1], positionInfo.end[1]); + msg.WriteDeltaFloat(positionInfo.start[2], positionInfo.end[2]); + msg.WriteFloat(positionInfo.interpVal, 4, 4); + + msg.WriteFloat(eyeOffsetInfo.start); + msg.WriteDeltaFloat(eyeOffsetInfo.start, eyeOffsetInfo.current); + msg.WriteDeltaFloat(eyeOffsetInfo.start, eyeOffsetInfo.end); + msg.WriteFloat(eyeOffsetInfo.interpVal, 4, 4); +#else + q = rotationInfo.current.ToCQuat(); + msg.WriteFloat(q.x); + msg.WriteFloat(q.y); + msg.WriteFloat(q.z); + + msg.WriteDeltaFloat(plPos[0], positionInfo.current[0]); + msg.WriteDeltaFloat(plPos[1], positionInfo.current[1]); + msg.WriteDeltaFloat(plPos[2], positionInfo.current[2]); + + msg.WriteFloat(eyeOffsetInfo.current); +#endif + + //msg.WriteFloat(lerpScale); +} + +//================ +//hhCameraInterpolator::ReadFromSnapshot +//================ +void hhCameraInterpolator::ReadFromSnapshot( const idBitMsgDelta &msg, hhPlayer *pl ) +{ + idCQuat sq, q; + + idVec3 plPos = pl->GetPlayerPhysics()->GetOrigin(); + +#if 1 + sq.x = msg.ReadFloat(); + sq.y = msg.ReadFloat(); + sq.z = msg.ReadFloat(); + rotationInfo.start = sq.ToQuat(); + q.x = msg.ReadDeltaFloat(sq.x); + q.y = msg.ReadDeltaFloat(sq.y); + q.z = msg.ReadDeltaFloat(sq.z); + rotationInfo.current = q.ToQuat(); + q.x = msg.ReadDeltaFloat(sq.x); + q.y = msg.ReadDeltaFloat(sq.y); + q.z = msg.ReadDeltaFloat(sq.z); + rotationInfo.end = q.ToQuat(); + rotationInfo.interpVal = msg.ReadFloat(4, 4); + + positionInfo.start[0] = msg.ReadDeltaFloat(plPos[0]); + positionInfo.start[1] = msg.ReadDeltaFloat(plPos[1]); + positionInfo.start[2] = msg.ReadDeltaFloat(plPos[2]); + positionInfo.current[0] = msg.ReadDeltaFloat(positionInfo.start[0]); + positionInfo.current[1] = msg.ReadDeltaFloat(positionInfo.start[1]); + positionInfo.current[2] = msg.ReadDeltaFloat(positionInfo.start[2]); + positionInfo.end[0] = msg.ReadDeltaFloat(positionInfo.start[0]); + positionInfo.end[1] = msg.ReadDeltaFloat(positionInfo.start[1]); + positionInfo.end[2] = msg.ReadDeltaFloat(positionInfo.start[2]); + positionInfo.interpVal = msg.ReadFloat(4, 4); + + eyeOffsetInfo.start = msg.ReadFloat(); + eyeOffsetInfo.current = msg.ReadDeltaFloat(eyeOffsetInfo.start); + eyeOffsetInfo.end = msg.ReadDeltaFloat(eyeOffsetInfo.start); + eyeOffsetInfo.interpVal = msg.ReadFloat(4, 4); +#else + q.x = msg.ReadFloat(); + q.y = msg.ReadFloat(); + q.z = msg.ReadFloat(); + rotationInfo.current = q.ToQuat(); + + positionInfo.current[0] = msg.ReadDeltaFloat(plPos[0]); + positionInfo.current[1] = msg.ReadDeltaFloat(plPos[1]); + positionInfo.current[2] = msg.ReadDeltaFloat(plPos[2]); + + eyeOffsetInfo.current = msg.ReadFloat(); + + rotationInfo.start = rotationInfo.current; + rotationInfo.end = rotationInfo.current; + positionInfo.start = positionInfo.current; + positionInfo.end = positionInfo.current; + eyeOffsetInfo.start = eyeOffsetInfo.current; + eyeOffsetInfo.end = eyeOffsetInfo.current; +#endif + //lerpScale = msg.ReadFloat(); +} diff --git a/src/Prey/prey_camerainterpolator.h b/src/Prey/prey_camerainterpolator.h new file mode 100644 index 0000000..c5ad582 --- /dev/null +++ b/src/Prey/prey_camerainterpolator.h @@ -0,0 +1,134 @@ +#ifndef __HH_CAMERA_INTERPOLATOR_H +#define __HH_CAMERA_INTERPOLATOR_H + +/********************************************************************** + +hhCameraInterpolator + +**********************************************************************/ +class hhCameraInterpolator; + +enum InterpolationType { + IT_None, + IT_VariableMidPointSinusoidal, + IT_Linear, + IT_Inverse, + IT_NumTypes +}; + +const int INTERPOLATE_NONE = 0; +const int INTERPOLATE_POSITION = BIT( 1 ); +const int INTERPOLATE_ROTATION = BIT( 2 ); +const int INTERPOLATE_EYEOFFSET = BIT( 3 ); + +const int INTERPMASK_ALL = -1; +const int INTERPMASK_WALLWALK = INTERPOLATE_POSITION | INTERPOLATE_ROTATION; + + +typedef float (*InterpFunc)( float&, float ); + +class hhPlayer; + +class hhCameraInterpolator { + public: + hhCameraInterpolator(); + + void SetSelf( hhPlayer* self ); + + float GetCurrentEyeHeight() const; + float GetIdealEyeHeight() const; + idVec3 GetCurrentEyeOffset() const; + idVec3 GetEyePosition() const; + idVec3 GetCurrentPosition() const; + idVec3 GetIdealPosition() const; + + idVec3 GetCurrentUpVector() const; + idMat3 GetCurrentAxis() const; + idAngles GetCurrentAngles() const; + idQuat GetCurrentRotation() const; + + idVec3 GetIdealUpVector() const; + idMat3 GetIdealAxis() const; + idAngles GetIdealAngles() const; + idQuat GetIdealRotation() const; + + void UpdateEyeOffset(); + + void UpdateTarget( const idVec3& idealPosition, const idMat3& idealAxis, const float eyeOffset, int interpFlags ); + void SetTargetPosition( const idVec3& idealPos, int interpFlags ); + void SetTargetAxis( const idMat3& idealAxis, int interpFlags ); + void SetTargetEyeOffset( float idealEyeOffset, int interpFlags ); + void SetIdealRotation( const idQuat& idealRotation ) { rotationInfo.end = idealRotation; } + + void Setup( const float lerpScale, const InterpolationType type ); + + void Reset( const idVec3& position, const idVec3& idealUpVector, float eyeOffset ); + + idAngles UpdateViewAngles( const idAngles& viewAngles ); + + InterpolationType SetInterpolationType( InterpolationType type ); + void Evaluate( float frameTime ); + + idQuat DetermineIdealRotation( const idVec3& idealUpVector, const idVec3& viewDir, const idMat3& untransformedViewAxis ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - send over net + void WriteToSnapshot( idBitMsgDelta &msg, const hhPlayer *pl ) const; + void ReadFromSnapshot( const idBitMsgDelta &msg, hhPlayer *pl ); + + protected: + void ClearFuncList(); + void RegisterFunc( InterpFunc func, InterpolationType type ); + InterpFunc DetermineFunc( InterpolationType type ); + + idQuat DetermineIdealRotation( const idVec3& idealUpVector ); + + void EvaluatePosition( float interpVal ); + void EvaluateRotation( float interpVal ); + void EvaluateEyeOffset( float interpVal ); + + void VerifyEyeOffset( float& eyeOffset ); + + protected: + template + struct LerpInfo_t { + Type start; + Type end; + Type current; + float interpVal; + + void Set( Type endVal ) { + end = endVal; + start = current; + interpVal = 0.0f; + } + + void Reset( Type endVal ) { + end = endVal; + start = end; + current = start; + interpVal = 1.0f; + } + + void IsDone() const { + return interpVal >= 1.0f; + } + }; + + LerpInfo_t rotationInfo; + LerpInfo_t positionInfo; + LerpInfo_t eyeOffsetInfo; + + InterpolationType interpType; + + float lerpScale; + + hhPlayer* self; + idList funcList; + + idClipModel clipBounds; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_firecontroller.cpp b/src/Prey/prey_firecontroller.cpp new file mode 100644 index 0000000..cdeb71f --- /dev/null +++ b/src/Prey/prey_firecontroller.cpp @@ -0,0 +1,548 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +ABSTRACT_DECLARATION( idClass, hhFireController ) +END_CLASS + +/* +================ +hhFireController::hhFireController +================ +*/ +hhFireController::hhFireController() : muzzleFlashHandle(-1) { + // Register us so we can be saved -mdl + gameLocal.RegisterUniqueObject( this ); +} + +/* +================ +hhFireController::~hhFireController +================ +*/ +hhFireController::~hhFireController() { + gameLocal.UnregisterUniqueObject( this ); + SAFE_FREELIGHT( muzzleFlashHandle ); + Clear(); +} + +/* +================ +hhFireController::Init +================ +*/ +void hhFireController::Init( const idDict* viewDict ) { + const char *shader = NULL; + + Clear(); + + dict = viewDict; + + if( !dict ) { + return; + } + + ammoRequired = dict->GetInt( "ammoRequired" ); + + // set up muzzleflash render light + const idMaterial*flashShader; + idVec3 flashColor; + idVec3 flashTarget; + idVec3 flashUp; + idVec3 flashRight; + //HUMANHEAD: aob - changed from float to idVec3 + idVec3 flashRadius; + //HUMANHEAD END + bool flashPointLight; + + // get the projectile + SetProjectileDict( dict->GetString("def_projectile") ); + + dict->GetString( "mtr_flashShader", "muzzleflash", &shader ); + flashShader = declManager->FindMaterial( shader, false ); + flashPointLight = dict->GetBool( "flashPointLight", "1" ); + dict->GetVector( "flashColor", "0 0 0", flashColor ); + flashTime = SEC2MS( dict->GetFloat( "flashTime", "0.08" ) ); + flashTarget = dict->GetVector( "flashTarget" ); + flashUp = dict->GetVector( "flashUp" ); + flashRight = dict->GetVector( "flashRight" ); + + memset( &muzzleFlash, 0, sizeof( muzzleFlash ) ); + + muzzleFlash.pointLight = flashPointLight; + muzzleFlash.shader = flashShader; + muzzleFlash.shaderParms[ SHADERPARM_RED ] = flashColor[0]; + muzzleFlash.shaderParms[ SHADERPARM_GREEN ] = flashColor[1]; + muzzleFlash.shaderParms[ SHADERPARM_BLUE ] = flashColor[2]; + muzzleFlash.shaderParms[ SHADERPARM_TIMESCALE ] = 1.0f; + + //HUMANHEAD: aob + if( dict->GetFloat("flashRadius", "0", flashRadius[0]) ) { // if 0, no light will spawn + flashRadius[2] = flashRadius[1] = flashRadius[0]; + } else { + flashRadius = dict->GetVector( "flashSize" ); + //muzzleFlash.lightCenter.Set( flashRadius[0] * 0.5f, 0.0f, 0.0f ); + } + muzzleFlash.lightRadius = flashRadius; + //HUMANHEAD END + + muzzleFlash.noShadows = true; //HUMANHEAD bjk + + if ( !flashPointLight ) { + muzzleFlash.target = flashTarget; + muzzleFlash.up = flashUp; + muzzleFlash.right = flashRight; + muzzleFlash.end = flashTarget; + } + + //HUMANHEAD: aob + fireDelay = dict->GetFloat( "fireRate" ); + spread = DEG2RAD( dict->GetFloat("spread") ); + + yawSpread = dict->GetFloat("yawSpread"); + + bCrosshair = dict->GetBool("crosshair"); + + numProjectiles = dict->GetInt( "numProjectiles" ); + //HUMANHEAD END + + deferProjNum = 0; //HUMANHEAD rww + deferProjTime = 0; //HUMANHEAD rww +} + +/* +================ +hhFireController::Clear +================ +*/ +void hhFireController::Clear() { + dict = NULL; + + muzzleOrigin.Zero(); + muzzleAxis.Identity(); + + // weapon definition + numProjectiles = 0; + projectile = NULL; + projDictName = ""; + + memset( &muzzleFlash, 0, sizeof( muzzleFlash ) ); + SAFE_FREELIGHT( muzzleFlashHandle ); + + muzzleFlashEnd = 0;; + flashTime = 0; + + // ammo management + ammoRequired = 0; // amount of ammo to use each shot. 0 means weapon doesn't need ammo. + + fireDelay = 0.0f; + spread = 0.0f; + + yawSpread = 0.0f; + + bCrosshair = false; + + projectileMaxHalfDimension = 0.0f; +} + + +/* +================ +hhFireController::SetProjectileDict +================ +*/ +void hhFireController::SetProjectileDict( const char* name ) { + idVec3 mins; + idVec3 maxs; + projectileMaxHalfDimension = 1.0f; + + projDictName = name; + + if ( name[0] ) { + projectile = gameLocal.FindEntityDefDict( name, false ); + if ( !projectile ) { + gameLocal.Warning( "Unknown projectile '%s'", name ); + } else { + const char *spawnclass = projectile->GetString( "spawnclass" ); + idTypeInfo *cls = idClass::GetClass( spawnclass ); + if ( !cls || !cls->IsType( idProjectile::Type ) ) { + gameLocal.Error( "Invalid spawnclass '%s' on projectile '%s'", spawnclass, name ); + } + + mins = projectile->GetVector( "mins" ); + maxs = projectile->GetVector( "maxs" ); + projectileMaxHalfDimension = hhMath::hhMax( (maxs.y - mins.y) * 0.5f, (maxs.z - mins.z) * 0.5f ); + } + } else { + projectile = NULL; + } +} + +/* +================ +hhFireController::DetermineProjectileAxis +================ +*/ +idMat3 hhFireController::DetermineProjectileAxis( const idMat3& axis ) { + idVec3 dir = hhUtils::RandomSpreadDir( axis, spread ); + idAngles projectileAngles( dir.ToAngles() ); + + projectileAngles[YAW] += yawSpread*hhMath::Sin(gameLocal.random.RandomFloat()*hhMath::TWO_PI); //rww - seperate optional yaw spread + + projectileAngles[2] = axis.ToAngles()[2]; + + return projectileAngles.ToMat3(); +} + +/* +================ +hhFireController::LaunchProjectiles +================ +*/ +bool hhFireController::LaunchProjectiles( const idVec3& pushVelocity ) { + if( !GetProjectileOwner() ) { + return false; + } + + // check if we're out of ammo or the clip is empty + if( !HasAmmo() ) { + return false; + } + + UseAmmo(); + + // calculate the muzzle position + CalculateMuzzlePosition( muzzleOrigin, muzzleAxis ); + + if ( !gameLocal.isClient || dict->GetBool("net_clientProjectiles", "1") ) { //HUMANHEAD rww - clientside projectiles, because our weapons make the god of bandwidth weep. + //HUMANHEAD: aob + idVec3 adjustedOrigin = AssureInsideCollisionBBox( muzzleOrigin, GetSelf()->GetAxis(), GetCollisionBBox(), projectileMaxHalfDimension ); + idMat3 aimAxis = DetermineAimAxis( adjustedOrigin, GetSelf()->GetAxis() ); + //HUMANHEAD END + + LaunchProjectiles( adjustedOrigin, aimAxis, pushVelocity, GetProjectileOwner() ); + + //rww - remove this from here and only do it on the client, we need to create the effect locally and with knowledge + //of if we should get the viewmodel bone or the worldmodel one + //CreateMuzzleFx( muzzleOrigin, muzzleAxis ); + } + + if (gameLocal.GetLocalPlayer()) + { //rww - create muzzle fx on client + idVec3 localMuzzleOrigin = muzzleOrigin; + idMat3 localMuzzleAxis = muzzleAxis; + if (gameLocal.isMultiplayer) { //rww - check if we should display the actual muzzle flash from a point different than the projectile launch location + idVec3 newOrigin; + idMat3 newAxis; + if (CheckThirdPersonMuzzle(newOrigin, newAxis)) { + localMuzzleOrigin = newOrigin; + localMuzzleAxis = newAxis; + } + } + CreateMuzzleFx( localMuzzleOrigin, localMuzzleAxis ); + } + + return true; +} + +/* +================ +hhFireController::CheckDeferredProjectiles +================ +*/ +void hhFireController::CheckDeferredProjectiles(void) { //rww + assert(!gameLocal.isClient); + if (deferProjTime > gameLocal.time) { + return; + } + + //FIXME compensate for intermediate time? + + deferProjTime = gameLocal.time + 50; //time is rather arbitrary. + + int i = 0; + while (deferProjNum > 0 && i < MAX_NET_PROJECTILES) { + if (!deferProjOwner.IsValid()) { + return; + } + + hhProjectile *projectile = SpawnProjectile(); + + projectile->Create(deferProjOwner.GetEntity(), deferProjLaunchOrigin, deferProjLaunchAxis); + projectile->spawnArgs.Set( "weapontype", GetSelf()->spawnArgs.GetString("ddaname", "") ); + projectile->Launch(deferProjLaunchOrigin, DetermineProjectileAxis(deferProjLaunchAxis), deferProjPushVelocity, 0.0f, 1.0f ); + + deferProjNum--; + i++; + } +} + +/* +================ +hhFireController::LaunchProjectiles +================ +*/ +void hhFireController::LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ) { + if (gameLocal.isMultiplayer && numProjectiles > MAX_NET_PROJECTILES && !dict->GetBool("net_noProjectileDefer", "0") && + !dict->GetBool("net_clientProjectiles", "1") && !gameLocal.isClient) { + //HUMANHEAD rww - in mp our gigantic single-snapshot projectile spawns tend to be very destructive toward bandwidth. + //and so, projectile deferring. + deferProjNum = numProjectiles; + deferProjTime = 0; + deferProjLaunchOrigin = launchOrigin; + deferProjLaunchAxis = aimAxis; + deferProjPushVelocity = pushVelocity; + deferProjOwner = projOwner; + } + else { + if (gameLocal.isClient && !gameLocal.isNewFrame) { + return; + } + + hhProjectile* projectile = NULL; + bool clientProjectiles = dict->GetBool("net_clientProjectiles", "1"); + for( int ix = 0; ix < numProjectiles; ++ix ) { + if (clientProjectiles) { //HUMANHEAD rww - clientside projectiles! + projectile = hhProjectile::SpawnClientProjectile( GetProjectileDict() ); + } + else { + projectile = SpawnProjectile(); + } + + projectile->Create( projOwner, launchOrigin, aimAxis ); + + projectile->spawnArgs.Set( "weapontype", GetSelf()->spawnArgs.GetString("ddaname", "") ); + + projectile->Launch( launchOrigin, DetermineProjectileAxis(aimAxis), pushVelocity, 0.0f, 1.0f ); + } + } +} + +/* +================ +hhFireController::AssureInsideCollisionBBox +================ +*/ +idVec3 hhFireController::AssureInsideCollisionBBox( const idVec3& origin, const idMat3& axis, const idBounds& ownerAbsBounds, float projMaxHalfDim ) const { + float distance = 0.0f; + if( !ownerAbsBounds.RayIntersection(origin, -axis[0], distance) ) { + distance = 0.0f; + } + + // HUMANHEAD CJR: If the player is touching a portal, then set the projectile inside the player so it has a chance to collide with the portal + idEntity *owner = GetProjectileOwner(); + if ( owner && owner->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast(owner); + if ( player->IsPortalColliding() ) { // Player is touching a portal, so force it inside the player bounds + distance = 2.0f * idMath::Fabs( owner->GetPhysics()->GetBounds()[1].y ); // Push back by the player's size + } + } // HUMANHEAD END + + //Need to come back half the size of the projectiles bbox to + //guarentee that the whole projectile is in the owners bbox + return origin + (distance + projMaxHalfDim) * -axis[0]; +} + +/* +================ +hhFireController::CalculateMuzzlePosition +================ +*/ +void hhFireController::CalculateMuzzlePosition( idVec3& origin, idMat3& axis ) { + origin = GetMuzzlePosition(); + axis = GetSelf()->GetAxis(); + + if( g_showProjectileLaunchPoint.GetBool() ) { + idVec3 v = (origin - GetSelf()->GetOrigin()) * GetSelf()->GetAxis().Transpose(); + gameLocal.Printf( "Launching from: (relative to weapon): %s\n", v.ToString() ); + hhUtils::DebugCross( colorGreen, origin, 5, 5000 ); + gameRenderWorld->DebugLine( colorBlue, GetSelf()->GetOrigin(), GetSelf()->GetOrigin() + (v * GetSelf()->GetAxis()), 5000 ); + } +} + +/* +================ +hhFireController::UpdateMuzzleFlashPosition +================ +*/ +void hhFireController::UpdateMuzzleFlashPosition() { + muzzleFlash.axis = GetSelf()->GetAxis(); + muzzleFlash.origin = AssureInsideCollisionBBox( muzzleOrigin, muzzleFlash.axis, GetProjectileOwner()->GetPhysics()->GetAbsBounds(), projectileMaxHalfDimension ); + + //TEST + /*trace_t trace; + idVec3 flashSize = dict->GetVector( "flashSize" ); + if( gameLocal.clip.TracePoint(trace, muzzleFlash.origin, muzzleFlash.origin + muzzleFlash.axis[0] * flashSize[0], MASK_VISIBILITY, GetSelf()) ) { + flashSize[0] *= trace.fraction; + } + muzzleFlash.lightRadius = flashSize; + muzzleFlash.origin += muzzleFlash.axis[0] * flashSize[0] * 0.5f; + muzzleFlash.lightCenter.Set( flashSize[0] * -0.5f, 0.0f, 0.0f ); + + hhUtils::Swap( muzzleFlash.lightRadius[0], muzzleFlash.lightRadius[2] ); + hhUtils::Swap( muzzleFlash.lightCenter[0], muzzleFlash.lightCenter[2] ); + muzzleFlash.axis = hhUtils::SwapXZ( muzzleFlash.axis );*/ + //TEST + + // put the world muzzle flash on the end of the joint, no matter what + //GetGlobalJointTransform( false, flashJointWorld, muzzleOrigin, muzzleFlash.axis ); +} + +/* +================ +hhFireController::MuzzleFlash +================ +*/ +void hhFireController::MuzzleFlash() { + if (!g_muzzleFlash.GetBool()) { + return; + } + + UpdateMuzzleFlashPosition(); + + if( muzzleFlash.lightRadius[0] < VECTOR_EPSILON || muzzleFlash.lightRadius[1] < VECTOR_EPSILON || muzzleFlash.lightRadius[2] < VECTOR_EPSILON ) { + return; + } + + // these will be different each fire + muzzleFlash.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.GetTime() ); + muzzleFlash.shaderParms[ SHADERPARM_DIVERSITY ] = gameLocal.random.RandomFloat(); + + //info.worldMuzzleFlash.shaderParms[ SHADERPARM_TIMEOFFSET ] = -MS2SEC( gameLocal.GetTime() ); + //info.worldMuzzleFlash.shaderParms[ SHADERPARM_DIVERSITY ] = renderEntity.shaderParms[ SHADERPARM_DIVERSITY ]; + + // the light will be removed at this time + muzzleFlashEnd = gameLocal.GetTime() + flashTime; + + if ( muzzleFlashHandle != -1 ) { + gameRenderWorld->UpdateLightDef( muzzleFlashHandle, &muzzleFlash ); + //gameRenderWorld->UpdateLightDef( info.worldMuzzleFlashHandle, &info.worldMuzzleFlash ); + } else { + muzzleFlashHandle = gameRenderWorld->AddLightDef( &muzzleFlash ); + //info.worldMuzzleFlashHandle = gameRenderWorld->AddLightDef( &info.worldMuzzleFlash ); + } +} + +/* +================ +hhFireController::UpdateMuzzleFlash +================ +*/ +void hhFireController::UpdateMuzzleFlash() { + AttemptToRemoveMuzzleFlash(); + + if( muzzleFlashHandle != -1 ) { + UpdateMuzzleFlashPosition(); + if( muzzleFlash.lightRadius[0] < VECTOR_EPSILON || muzzleFlash.lightRadius[1] < VECTOR_EPSILON || muzzleFlash.lightRadius[2] < VECTOR_EPSILON ) { //rww - added to mimic the behaviour of hhFireController::MuzzleFlash + return; + } + gameRenderWorld->UpdateLightDef( muzzleFlashHandle, &muzzleFlash ); + //gameRenderWorld->UpdateLightDef( worldMuzzleFlashHandle, &worldMuzzleFlash ); + } +} + +/* +================ +hhFireController::CreateMuzzleFx +================ +*/ +void hhFireController::CreateMuzzleFx( const idVec3& pos, const idMat3& axis ) { + hhFxInfo fxInfo; + + if (!gameLocal.GetLocalPlayer()) { //rww - not at all necessary for ded server + return; + } + if( GetSelf()->IsHidden() || !GetSelf()->GetRenderEntity()->hModel ) { + return; + } + + fxInfo.SetNormal( axis[0] ); + fxInfo.RemoveWhenDone( true ); + fxInfo.SetEntity( GetSelf() ); + + + //GetSelf()->BroadcastFxInfo( dict->GetString("fx_muzzleFlash"), pos, axis, &fxInfo ); + //rww - this is now client-only. + GetSelf()->SpawnFxLocal( dict->GetString("fx_muzzleFlash"), pos, axis, &fxInfo, true ); +} + +/* +================ +hhFireController::Save +================ +*/ +void hhFireController::Save( idSaveGame *savefile ) const { + savefile->WriteDict( dict ); + savefile->WriteFloat( yawSpread ); + savefile->WriteString( projDictName ); + savefile->WriteVec3( muzzleOrigin ); + savefile->WriteMat3( muzzleAxis ); + savefile->WriteRenderLight( muzzleFlash ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->WriteInt( muzzleFlashHandle ); + savefile->WriteInt( muzzleFlashEnd ); + savefile->WriteInt( flashTime ); + savefile->WriteInt( ammoRequired ); + savefile->WriteFloat( fireDelay ); + savefile->WriteFloat( spread ); + savefile->WriteBool( bCrosshair ); + savefile->WriteFloat( projectileMaxHalfDimension ); + savefile->WriteInt( numProjectiles ); + //HUMANHEAD PCF mdl 05/04/06 - Save whether the light is active + savefile->WriteBool( muzzleFlashHandle != -1 ); +} + +/* +================ +hhFireController::Restore +================ +*/ +void hhFireController::Restore( idRestoreGame *savefile ) { + savefile->ReadDict( &restoredDict ); + dict = &restoredDict; + savefile->ReadFloat( yawSpread ); + savefile->ReadString( projDictName ); + savefile->ReadVec3( muzzleOrigin ); + savefile->ReadMat3( muzzleAxis ); + savefile->ReadRenderLight( muzzleFlash ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->ReadInt( muzzleFlashHandle ); + savefile->ReadInt( muzzleFlashEnd ); + savefile->ReadInt( flashTime ); + savefile->ReadInt( ammoRequired ); + savefile->ReadFloat( fireDelay ); + savefile->ReadFloat( spread ); + savefile->ReadBool( bCrosshair ); + savefile->ReadFloat( projectileMaxHalfDimension ); + savefile->ReadInt( numProjectiles ); + + //HUMANHEAD PCF mdl 05/04/06 - Restore the light if necessary + bool bLight; + savefile->ReadBool( bLight ); + if ( bLight ) { + muzzleFlashHandle = gameRenderWorld->AddLightDef( &muzzleFlash ); + } + + SetProjectileDict( projDictName ); +} + +/* +============================== +hhFireController::GetMuzzlePosition +============================== +*/ +idVec3 hhFireController::GetMuzzlePosition() const { + idVec3 muzzle( dict->GetVector("muzzleOffset", "2 0 0") ); + return GetSelfConst()->GetOrigin() + ( muzzle * GetSelfConst()->GetAxis() ); +} + +/* +============================== +hhFireController::GetMuzzlePosition +============================== +*/ +bool hhFireController::CheckThirdPersonMuzzle(idVec3 &origin, idMat3 &axis) { //rww + return false; +} diff --git a/src/Prey/prey_firecontroller.h b/src/Prey/prey_firecontroller.h new file mode 100644 index 0000000..878b4a4 --- /dev/null +++ b/src/Prey/prey_firecontroller.h @@ -0,0 +1,160 @@ +#ifndef __HH_FIRE_CONTROLLER_H +#define __HH_FIRE_CONTROLLER_H + +#include "gamesys/Class.h" + +#define MAX_NET_PROJECTILES 3 //rww + +class hhFireController : public idClass { + ABSTRACT_PROTOTYPE(hhFireController) + +public: + hhFireController(); + virtual ~hhFireController(); + + virtual void Clear(); + virtual void Init( const idDict* viewDict ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool UsesCrosshair() const = 0; + virtual idVec3 GetMuzzlePosition() const; + ID_INLINE virtual bool HasAmmo() const; + virtual void UseAmmo() = 0; + virtual int AmmoAvailable() const = 0; + ID_INLINE virtual int AmmoRequired() const; + ID_INLINE float GetFireDelay() const; + + void UpdateMuzzleFlash(); + virtual void CreateMuzzleFx( const idVec3& pos, const idMat3& axis ); + + ID_INLINE virtual idMat3 DetermineProjectileAxis( const idMat3& axis ); + virtual bool LaunchProjectiles( const idVec3& pushVelocity ); + + void MuzzleFlash(); + virtual void WeaponFeedback() = 0; + + virtual //HUMANHEAD bjk + const idDict* GetProjectileDict() const { return projectile; } + + void SetWeaponDict( const idDict *wdict ) { dict = wdict; yawSpread = dict->GetFloat("yawSpread"); } + + void CheckDeferredProjectiles(void); //rww + + virtual bool CheckThirdPersonMuzzle(idVec3 &origin, idMat3 &axis); //rww +protected: + void SetProjectileDict( const char* name ); + + virtual void LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ); + virtual idEntity* GetProjectileOwner() const = 0; + ID_INLINE virtual hhProjectile* SpawnProjectile(); + + virtual const idBounds& GetCollisionBBox() = 0; + virtual void CalculateMuzzlePosition( idVec3& origin, idMat3& axis ); + + ID_INLINE void AttemptToRemoveMuzzleFlash(); + void UpdateMuzzleFlashPosition(); + + idVec3 AssureInsideCollisionBBox( const idVec3& origin, const idMat3& axis, const idBounds& ownerAbsBounds, float projMaxHalfDim ) const; + virtual idMat3 DetermineAimAxis( const idVec3& muzzlePos, const idMat3& weaponAxis ); + + virtual hhRenderEntity *GetSelf() = 0; + virtual const hhRenderEntity *GetSelfConst() const = 0; + +protected: + idDict restoredDict; + const idDict* dict; + + const idDict * projectile; + idStr projDictName; + + idVec3 muzzleOrigin; + idMat3 muzzleAxis; + + // muzzle flash + renderLight_t muzzleFlash; // positioned on view weapon bone + int muzzleFlashHandle; + + int muzzleFlashEnd; + int flashTime; + + // ammo management + int ammoRequired; // amount of ammo to use each shot. 0 means weapon doesn't need ammo. + + float fireDelay; + float spread; + + float yawSpread; //rww - extra spread on yaw + + bool bCrosshair; + + float projectileMaxHalfDimension; + + int numProjectiles; + + //rww - projectile deferring + int deferProjNum; + int deferProjTime; + idVec3 deferProjLaunchOrigin; + idMat3 deferProjLaunchAxis; + idVec3 deferProjPushVelocity; + idEntityPtr deferProjOwner; +}; + +/* +================= +hhFireController::DetermineAimAxis +================= +*/ +ID_INLINE idMat3 hhFireController::DetermineAimAxis( const idVec3& muzzlePos, const idMat3& weaponAxis ) { + return weaponAxis; +} + +/* +================ +hhFireController::HasAmmo +================ +*/ +ID_INLINE bool hhFireController::HasAmmo() const { + return AmmoAvailable() != 0; +} + +/* +================ +hhFireController::SpawnProjectile +================ +*/ +ID_INLINE hhProjectile* hhFireController::SpawnProjectile() { + return hhProjectile::SpawnProjectile( GetProjectileDict() ); +} + +/* +================ +hhFireController::AttemptToRemoveMuzzleFlash +================ +*/ +ID_INLINE void hhFireController::AttemptToRemoveMuzzleFlash() { + if( gameLocal.GetTime() >= muzzleFlashEnd ) { + SAFE_FREELIGHT( muzzleFlashHandle ); + } +} + +/* +================ +hhFireController::AmmoRequired +================ +*/ +ID_INLINE int hhFireController::AmmoRequired() const { + return ammoRequired; +} + +/* +================ +hhFireController::GetFireDelay +================ +*/ +ID_INLINE float hhFireController::GetFireDelay() const { + return fireDelay; +} + +#endif diff --git a/src/Prey/prey_game.cpp b/src/Prey/prey_game.cpp new file mode 100644 index 0000000..2e0a981 --- /dev/null +++ b/src/Prey/prey_game.cpp @@ -0,0 +1,1438 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" +//#include "../prey/win32/fxdlg.h" + +extern idCVar com_forceGenericSIMD; + +//HUMANHEAD: aob - needed for networking to send the least amount of bits +const int DECL_MAX_TYPES_NUM_BITS = hhMath::BitsForInteger( DECL_MAX_TYPES ); +//HUMANHEAD END + +//============================================================================= +// Overridden functions +//============================================================================= + +//--------------------------------------------------- +// +// hhGameLocal::Init +// +//--------------------------------------------------- +void hhGameLocal::Init( void ) { + + //HUMANHEAD rww - moved into idGameLocal::Init, this should be done after the managed heap is initialized. + //ddaManager = new hhDDAManager; //must be before calling idGameLocal::Init(), otherwise if the map can't load, will cause a crash. + //HUMANHEAD END + + idGameLocal::Init(); + + dwWorldClipModel = NULL; +#if _HH_INLINED_PROC_CLIPMODELS + inlinedProcClipModels.Clear(); //HUMANHEAD rww +#endif + + sunCorona = NULL; // CJR + lastAIAlertRadius = 0; + + P_InitConsoleCommands(); + + //HUMANHEAD rww - check lglcd validity and reset values + logitechLCDEnabled = sys->LGLCD_Valid(); + logitechLCDDisplayAlt = false; + logitechLCDButtonsLast = 0; + logitechLCDUpdateTime = 0; + //HUMANHEAD END +} + +//--------------------------------------------------- +// +// hhGameLocal::Shutdown +// +//--------------------------------------------------- +void hhGameLocal::Shutdown( void ) { + +#if INGAME_DEBUGGER_ENABLED + debugger.Shutdown(); // HUMANHEAD pdm: Shut down the debugger before idDict gets shut down +#endif + + idGameLocal::Shutdown(); + + //HUMANHEAD rww - this must be done before managed heap destruction to correspond + //delete ddaManager; + //ddaManager = NULL; + //HUMANHEAD END +} + + +//--------------------------------------------------- +// +// hhGameLocal::UnregisterEntity +// +//--------------------------------------------------- +void hhGameLocal::UnregisterEntity( idEntity *ent ) { + assert( ent ); + + if ( talonTargets.Find( ent ) ) { + talonTargets.Remove( ent ); + } + + idGameLocal::UnregisterEntity( ent ); +} + +//--------------------------------------------------- +// +// hhGameLocal::MapShutdown +// +//--------------------------------------------------- +void hhGameLocal::MapShutdown( void ) { + //HUMANHEAD: mdc - added support for automatically dumping stats on map switch/game exit + if( !isMultiplayer && gamestate != GAMESTATE_NOMAP && g_dumpDDA.GetBool() ) { //make sure we actually have a valid map to export + GetDDA()->Export(NULL); //export the stats + } + //Always clear our tracking stats and create a default node + GetDDA()->ClearTracking(); //Clear Tracking statistics +// GetDDA()->CreateDefaultSectionNode(); //Setup our default section-node + + idGameLocal::MapShutdown(); + + sunCorona = NULL; + + talonTargets.Clear(); +} + +//--------------------------------------------------- +// +// hhGameLocal::InitFromNewMap +// +//--------------------------------------------------- +void hhGameLocal::InitFromNewMap( const char *mapName, idRenderWorld *renderWorld, idSoundWorld *soundWorld, bool isServer, bool isClient, int randseed ) { + + //HUMANHEAD rww - throw up the prey logo if using the logitech lcd screen + if (logitechLCDEnabled) { + sys->LGLCD_UploadImage(NULL, -1, -1, false, true); + } + //HUMANHEAD END + + talonTargets.Clear(); // CJR: Must be before idGameLocal::InitFromNewMap() + +#if INGAME_DEBUGGER_ENABLED + debugger.Reset(); +#endif + + hands.Clear(); + + idGameLocal::InitFromNewMap(mapName, renderWorld, soundWorld, isServer, isClient, randseed); + + // CJR: Determine if this map is a LOTA map by looking for a special entity on the map + bIsLOTA = false; + if ( FindEntity( "LOTA_ThisMapIsLOTA" ) ) { + bIsLOTA = true; + } +} + +//============================================================================= +// +// hhGameLocal::CacheDictionaryMedia +// +// This is called after parsing an EntityDef and for each entity spawnArgs before +// merging the entitydef. It could be done post-merge, but that would +// avoid the fast pre-cache check associated with each entityDef +// +// HUMANHEAD pdm: Override for doing our own precaching +//============================================================================= +void hhGameLocal::CacheDictionaryMedia( const idDict *dict ) { +/* HUMANHEAD mdc - fix so com_makingBuild stuff will work correctly... (this was causing too early of an out) + if ( dict == NULL ) { + return; + } +*/ + const idKeyValue *kv = NULL; + const idDecl *decl = NULL; + idFile *file = NULL; + idStr fxName; + idStr buffer; + + idGameLocal::CacheDictionaryMedia( dict ); + + if( dict == NULL ) { //mdc - added early out + return; + } +//HUMANHEAD mdc - skip caching if we are in development and not making a build + if( !g_precache.GetBool() && !sys_forceCache.GetBool() && !cvarSystem->GetCVarBool("com_makingBuild") ) { + return; + } +//HUMANHEAD END + + + // TEMP: precache clip models until they become model_clip + kv = dict->MatchPrefix( "clipmodel", NULL ); + while( kv ) { + if ( kv->GetValue().Length() ) { + declManager->MediaPrint( "Precaching clipmodel: %s\n", kv->GetValue().c_str() ); + renderModelManager->FindModel( kv->GetValue() ); + } + kv = dict->MatchPrefix( "clipmodel", kv ); + } + + //HUMANHEAD bjk: removed smoke_wound_. now using fx_wound and is cached in fx_. no seperate handling needed + + kv = dict->MatchPrefix( "beam", NULL ); + while( kv ) { + if ( kv->GetValue().Length() ) { + if (kv->GetValue().Find(".beam") >= 0) { + declManager->MediaPrint( "Precaching beam %s\n", kv->GetValue().c_str() ); + declManager->FindBeam( kv->GetValue() ); // Cache the beam decl + renderModelManager->FindModel(kv->GetValue()); // Ensure the beam model is cached as well, since beams are a combination of a decl and a rendermodel + } + else { // Must be a reference to a beam entity + declManager->MediaPrint( "Precaching beam entityDef %s\n", kv->GetValue().c_str() ); + FindEntityDef( kv->GetValue().c_str(), false ); + } + } + kv = dict->MatchPrefix( "beam", kv ); + } +} + + +//============================================================================= +// New functionality +//============================================================================= + +//============================================================================= +// +// hhGameLocal::SpawnAppendedMapEntities +// +// HUMANHEAD pdm: Support for level appending +//============================================================================= +#if DEATHWALK_AUTOLOAD +void hhGameLocal::SpawnAppendedMapEntities() { + if (additionalMapFile) { + // Swap mapFile and additionalMapFile + idMapFile *mainMapFile = mapFile; + mapFile = additionalMapFile; + + // This routine based largely on idGameLocal::SpawnMapEntities + + // parse the key/value pairs and spawn entities for dw + // From SpawnMapEntities(); + idMapEntity *mapEnt; + int numEntities; + idDict args; + + numEntities = mapFile->GetNumEntities(); + if ( numEntities == 0 ) { + Error( "...no entities" ); + } + + for ( int i = 1 ; i < numEntities ; i++ ) { // skip worldspawn + mapEnt = mapFile->GetEntity( i ); + args = mapEnt->epairs; + + if ( !InhibitEntitySpawn( args ) ) { + // precache any media specified in the map entity + gameLocal.CacheDictionaryMedia( &args ); + + //HUMANHEAD rww - ok on client + SpawnEntityDef( args, NULL, true, false, true ); + } + } + + // Restore main mapfile + delete additionalMapFile; + additionalMapFile = NULL; + mapFile = mainMapFile; + + + // Since the world collision is always collision model 0, link our model in like an entity would + dwWorldClipModel = new idClipModel("dw_worldmap"); + dwWorldClipModel->SetContents(CONTENTS_SOLID); + dwWorldClipModel->SetEntity(world); + dwWorldClipModel->Link(clip); + } +} +#endif + +//============================================================================= +// +// hhGameLocal::RegisterTalonTarget +// +//============================================================================= +void hhGameLocal::RegisterTalonTarget( idEntity *ent ) { + talonTargets.Append(ent); +} + +//============================================================================= +// +// hhGameLocal::SpawnClientObject +// rww - meant for spawning client ents only. +//============================================================================= +idEntity* hhGameLocal::SpawnClientObject( const char* objectName, idDict* additionalArgs ) { + idDict localArgs; + idDict* args = NULL; + idEntity* object = NULL; + bool clientEnt = true; + + if (!gameLocal.isClient) { //if it's the server spawning a client entity we want to spawn it for listen servers but keep it local + clientEnt = false; + } + + if( !objectName || !objectName[0]) { + Error( "hhGameLocal::SpawnObject: Invalid object name\n" ); + } + + args = ( additionalArgs != NULL ) ? additionalArgs : &localArgs; + + args->Set( "classname", objectName ); + if( !SpawnEntityDef( *args, &object, true, clientEnt ) ) { + Error( "hhGameLocal::SpawnObject: Failed to spawn %s\n", objectName ); + } + + if (object) { + object->fl.clientEntity = clientEnt; + object->fl.networkSync = false; + } + + return object; +} + +//============================================================================= +// +// hhGameLocal::SpawnObject +// +//============================================================================= +idEntity* hhGameLocal::SpawnObject( const char* objectName, idDict* additionalArgs ) { + idDict localArgs; + idDict* args = NULL; + idEntity* object = NULL; + + //rww - are you hitting this? then the code it's coming from is doing something it shouldn't be. + //the client should never, ever call SpawnObject. + assert(!gameLocal.isClient); + + if( !objectName || !objectName[0]) { + Error( "hhGameLocal::SpawnObject: Invalid object name\n" ); + } + + args = ( additionalArgs != NULL ) ? additionalArgs : &localArgs; + + args->Set( "classname", objectName ); + if( !SpawnEntityDef( *args, &object ) ) { + Error( "hhGameLocal::SpawnObject: Failed to spawn %s\n", objectName ); + } + + return object; +} + + +// +// hhGameLocal::RadiusDamage() +// PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +// +void hhGameLocal::RadiusDamage( const idVec3 &origin, idEntity *inflictor, idEntity *attacker, idEntity *ignoreDamage, idEntity *ignorePush, const char *damageDefName, float dmgPower ) { + float distSquared, radiusSquared, damageScale, attackerDamageScale, attackerPushScale; + idEntity * ent; + idEntity * entityList[ MAX_GENTITIES ]; + int numListedEntities; + idBounds bounds; + idVec3 v, damagePoint, dir; + int i, e, damage, radius, push; + // HUMANHEAD AOB + int pushRadius; + trace_t result; + // HUMANHEAD END + + const idDict *damageDef = FindEntityDefDict( damageDefName, false ); + if ( !damageDef ) { + Warning( "Unknown damageDef '%s'", damageDefName ); + return; + } + + damageDef->GetInt( "damage", "0", damage ); // HUMANHEAD JRM - changed to ZERO not 20 default + damageDef->GetInt( "radius", "50", radius ); + damageDef->GetInt( "push", va( "%d", damage * 100 ), push ); + damageDef->GetFloat( "attackerDamageScale", "0.5", attackerDamageScale ); + damageDef->GetFloat( "attackerPushScale", "0", attackerPushScale ); + + // HUMANHEAD aob + pushRadius = Max( 1.0f, damageDef->GetFloat("push_radius", va("%d", radius)) ); + if ( damageDef->GetBool( "nopush" ) ) { + push = 0; + } + // HUMANHEAD END + + if ( radius < 1 ) { + radius = 1; + } + radiusSquared = radius*radius; + + bounds = idBounds( origin ).Expand( radius ); + + // get all entities touching the bounds + numListedEntities = clip.EntitiesTouchingBounds( bounds, -1, entityList, MAX_GENTITIES ); + + if ( inflictor && inflictor->IsType( idAFAttachment::Type ) ) { + inflictor = static_cast(inflictor)->GetBody(); + } + if ( attacker && attacker->IsType( idAFAttachment::Type ) ) { + attacker = static_cast(attacker)->GetBody(); + } + if ( ignoreDamage && ignoreDamage->IsType( idAFAttachment::Type ) ) { + ignoreDamage = static_cast(ignoreDamage)->GetBody(); + } + + // apply damage to the entities + if( damage > 0 ) { // HUMANHEAD JRM - only do damage if WE HAVE damage + for ( e = 0; e < numListedEntities; e++ ) { + ent = entityList[ e ]; + assert( ent ); + + if ( !ent->fl.takedamage ) { + continue; + } + + if ( ent == inflictor /*|| ( ent->IsType( idAFAttachment::Type ) && static_cast(ent)->GetBody() == inflictor )*/ ) { + continue; + } + + if ( ent == ignoreDamage /*|| ( ent->IsType( idAFAttachment::Type ) && static_cast(ent)->GetBody() == ignoreDamage )*/ ) { + continue; + } + + if ( ent->IsType( idAFAttachment::Type ) ) { // bjk: no double splash damage from heads + continue; + } + + // don't damage a dead player + if ( isMultiplayer && ent->entityNumber < MAX_CLIENTS && ent->IsType( idPlayer::Type ) && static_cast< idPlayer * >( ent )->health < 0 ) { + continue; + } + + // find the distance from the edge of the bounding box + for ( i = 0; i < 3; i++ ) { + if ( origin[ i ] < ent->GetPhysics()->GetAbsBounds()[0][ i ] ) { + v[ i ] = ent->GetPhysics()->GetAbsBounds()[0][ i ] - origin[ i ]; + } else if ( origin[ i ] > ent->GetPhysics()->GetAbsBounds()[1][ i ] ) { + v[ i ] = origin[ i ] - ent->GetPhysics()->GetAbsBounds()[1][ i ]; + } else { + v[ i ] = 0; + } + } + + distSquared = v.LengthSqr(); + if ( distSquared >= radiusSquared ) { + continue; + } + + //HUMANHEAD: aob - see if radius damage is blocked. CanDamage is used by more than just RadiusDamage + clip.TracePoint( result, origin, (ent->GetPhysics()->GetAbsBounds()[0] + ent->GetPhysics()->GetAbsBounds()[1]) * 0.5f, CONTENTS_BLOCK_RADIUSDAMAGE, ignoreDamage ); + if( result.fraction < 1.0f && result.c.entityNum != ent->entityNumber ) { + continue; + } + //HUMANHEAD END + + bool canDamage = true; + if ( GERMAN_VERSION || g_nogore.GetBool() ) { + if ( ent->IsType(idActor::Type) ) { + idActor *actor = reinterpret_cast< idActor * > ( ent ); + if ( actor->IsActiveAF() && !actor->spawnArgs.GetBool( "not_gory", "0" ) ) { + canDamage = false; + } + } + } + + + if ( canDamage && ent->CanDamage( origin, damagePoint ) ) { + // push the center of mass higher than the origin so players + // get knocked into the air more + dir = ent->GetPhysics()->GetOrigin() - origin; + dir[ 2 ] += 24; + + // get the damage scale + damageScale = dmgPower * ( 1.0f - (distSquared / radiusSquared) ); + if ( ent == attacker || ( ent->IsType( idAFAttachment::Type ) && static_cast(ent)->GetBody() == attacker ) ) { + damageScale *= attackerDamageScale; + } + + ent->Damage( inflictor, attacker, dir, damageDefName, damageScale, INVALID_JOINT ); + } + } + } // HUMANHEAD END + + // push physics objects + if ( push ) { + //HUMANHEAD: aob - changed radius to pushRadius + RadiusPush( origin, pushRadius, push * dmgPower, attacker, ignorePush, attackerPushScale, false ); + } +} + +/* +============== +hhGameLocal::RadiusPush + PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +============== +*/ +void hhGameLocal::RadiusPush( const idVec3 &origin, const float radius, const float push, const idEntity *inflictor, const idEntity *ignore, float inflictorScale, const bool quake ) { + int i, numListedClipModels; + idClipModel *clipModel; + idClipModel *clipModelList[ MAX_GENTITIES ]; + idVec3 dir; + idBounds bounds; + modelTrace_t result; + idEntity *ent; + float scale; + trace_t trace; //HUMANHEAD: aob + + dir.Set( 0.0f, 0.0f, 1.0f ); + + bounds = idBounds( origin ).Expand( radius ); + + // get all clip models touching the bounds + numListedClipModels = clip.ClipModelsTouchingBounds( bounds, -1, clipModelList, MAX_GENTITIES ); + + if ( inflictor && inflictor->IsType( idAFAttachment::Type ) ) { + inflictor = static_cast(inflictor)->GetBody(); + } + if ( ignore && ignore->IsType( idAFAttachment::Type ) ) { + ignore = static_cast(ignore)->GetBody(); + } + + // apply impact to all the clip models through their associated physics objects + for ( i = 0; i < numListedClipModels; i++ ) { + + clipModel = clipModelList[i]; + + // never push render models + if ( clipModel->IsRenderModel() ) { + continue; + } + + ent = clipModel->GetEntity(); + + // never push projectiles + if ( ent->IsType( idProjectile::Type ) ) { + continue; + } + + // players use "knockback" in idPlayer::Damage + if ( ent->IsType( idPlayer::Type ) && !quake ) { + continue; + } + + // don't push the ignore entity + if ( ent == ignore || ( ent->IsType( idAFAttachment::Type ) && static_cast(ent)->GetBody() == ignore ) ) { + continue; + } + + if ( gameRenderWorld->FastWorldTrace( result, origin, clipModel->GetAbsBounds().GetCenter() ) ) { + continue; + } + + // Don't affect ragdolls in non-gore mode + if ( GERMAN_VERSION || g_nogore.GetBool() ) { + if ( ent->IsType(idActor::Type) ) { + idActor *actor = reinterpret_cast< idActor * > ( ent ); + if ( actor->IsActiveAF() && !actor->spawnArgs.GetBool( "not_gory", "0" ) ) { + continue; + } + } + } + + // scale the push for the inflictor + if ( ent == inflictor || ( ent->IsType( idAFAttachment::Type ) && static_cast(ent)->GetBody() == inflictor ) ) { + scale = inflictorScale; + } else if ( ent->IsType(hhMoveable::Type) ) { + scale = ent->spawnArgs.GetFloat("radiusPush", "4.0"); + } else if( !ent->IsType(idActor::Type) ) { + scale = 4.0f; + } else { + scale = 1.0f; + } + + //HUMANHEAD: aob - see if radius damage is blocked + clip.TracePoint( trace, origin, clipModel->GetEntity()->GetOrigin(), CONTENTS_BLOCK_RADIUSDAMAGE, ignore ); + if( trace.fraction < 1.0f && trace.c.entityNum != clipModel->GetEntity()->entityNumber ) { + continue; + } + //HUMANHEAD END + + if ( quake ) { + clipModel->GetEntity()->ApplyImpulse( world, clipModel->GetId(), clipModel->GetOrigin(), scale * push * dir ); + } else { + //RadiusPushClipModel( origin, scale * push, clipModel ); + + //HUMANHEAD bjk + idVec3 impulse = clipModel->GetAbsBounds().GetCenter() - origin; + float dist = impulse.Normalize() / radius; + impulse.z += 1.0f; + dist = 0.6f - 0.3f*dist; + impulse *= push * dist * scale; + clipModel->GetEntity()->ApplyImpulse( world, clipModel->GetId(), clipModel->GetOrigin(), impulse ); + //HUMANHEAD END + } + } +} + +//HUMANHEAD rww +void hhGameLocal::LogitechLCDUpdate(void) { + hhPlayer *pl = (hhPlayer *)GetLocalPlayer(); + if (!pl) { + return; + } + + DWORD buttons; + sys->LGLCD_ReadSoftButtons(&buttons); + if (logitechLCDButtonsLast != buttons && buttons) { + logitechLCDDisplayAlt = !logitechLCDDisplayAlt; + } + logitechLCDButtonsLast = buttons; + + if (logitechLCDUpdateTime >= gameLocal.time) { //only update the screen at 20fps + return; + } + logitechLCDUpdateTime = gameLocal.time + 50; + + sys->LGLCD_DrawBegin(); + if (logitechLCDDisplayAlt) { //primary/secondary ammo + char *ammoStr = va("%s%s", common->GetLanguageDict()->GetString("#str_41150"), common->GetLanguageDict()->GetString("#str_41152")); //"Ammo: N/A"; + char *ammoAltStr = va("%s%s", common->GetLanguageDict()->GetString("#str_41151"), common->GetLanguageDict()->GetString("#str_41152")); //"Alt. Ammo: N/A"; + if (pl->weapon.IsValid()) { + int a = pl->weapon->AmmoAvailable(); + if (a >= 0) { + if (a > 999) { + a = 999; + } + ammoStr = va("%s%i", common->GetLanguageDict()->GetString("#str_41150"), a); //"Ammo: %i" + } + if (pl->weapon->GetAmmoType() != pl->weapon->GetAltAmmoType()) { + a = pl->weapon->AltAmmoAvailable(); + if (a >= 0) { + if (a > 999) { + a = 999; + } + ammoAltStr = va("%s%i", common->GetLanguageDict()->GetString("#str_41151"), a); //"Alt. Ammo: %i" + } + } + } + + sys->LGLCD_DrawText(ammoStr, 46, 6, true); + sys->LGLCD_DrawText(ammoAltStr, 46, 22, true); + + //draw the weapon icon (disabled for now, since it isn't very..recognizable) + //sys->LGLCD_DrawShape(3, 54, 0, 0, 0, 0, true); + } + else { //health/spirit + int v; + v = pl->health; + if (v > 999) { + v = 999; + } + else if (v < 0) { + v = 0; + } + sys->LGLCD_DrawText(va("%s%i", common->GetLanguageDict()->GetString("#str_41153"), v), 70, 6, true); //"Health: %i" + v = pl->GetSpiritPower(); + if (v > 999) { + v = 999; + } + else if (v < 0) { + v = 0; + } + sys->LGLCD_DrawText(va("%s%i", common->GetLanguageDict()->GetString("#str_41154"), v), 70, 22, true); //"Spirit: %i" + + //draw the tommy and talon + float eyePitch; + if (pl->InVehicle()) { + eyePitch = 0.0f; + } + else { + eyePitch = pl->GetEyeAxis()[2].ToAngles().pitch+90.0f; + if (eyePitch < 1.0f && eyePitch > -1.0f) { + eyePitch = 0.0f; + } + } + sys->LGLCD_DrawShape(2, 42, 0, 0, 0, (int)eyePitch, true); + } + + //draw the compass + sys->LGLCD_DrawShape(0, 0, 0, 0, 0, 0, false); //base + sys->LGLCD_DrawShape(1, 21, 21, 13, 1, (int)-pl->GetViewAngles().yaw, true); //line + sys->LGLCD_DrawFinish(false); +} +//HUMANHEAD END + +// +// RunFrame() +// +// PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +gameReturn_t hhGameLocal::RunFrame( const usercmd_t *clientCmds ) { + idEntity * ent; + int num; + float ms; + idTimer timer_think, timer_events, timer_singlethink; + gameReturn_t ret; + idPlayer *player; + const renderView_t *view; + // HUMANHEAD pdm + idTimer timer_singledormant; + bool dormant; + const float thinkalpha = 0.98f; // filter with historical timings + // HUMANHEAD END + + ret.sessionCommand[0] = 0; + +#ifdef _DEBUG + if ( isMultiplayer ) { + assert( !isClient ); + } +#endif + + player = GetLocalPlayer(); + + if ( !isMultiplayer && g_stopTime.GetBool() ) { + // clear any debug lines from a previous frame + gameRenderWorld->DebugClearLines( time + 1 ); + + // set the user commands for this frame + memcpy( usercmds, clientCmds, numClients * sizeof( usercmds[ 0 ] ) ); + + // Fake these categories so it still displays any that are in the history when time is stopped + { PROFILE_SCOPE("Misc_Think", PROFMASK_NORMAL); } + { PROFILE_SCOPE("Dormant Tests", PROFMASK_NORMAL); } + { PROFILE_SCOPE("Sound", PROFMASK_NORMAL); } + { PROFILE_SCOPE("Animation", PROFMASK_NORMAL); } + { PROFILE_SCOPE("Scripting", PROFMASK_NORMAL); } + + if ( player ) { + player->Think(); + if ( player->InVehicle() && player->GetVehicleInterface() ) { + if ( player->GetVehicleInterface()->GetVehicle() ) { + player->GetVehicleInterface()->GetVehicle()->Think(); + } + } + } + } else do { + // update the game time + framenum++; + previousTime = time; + time += msec; + realClientTime = time; + timeRandom = time; //HUMANHEAD rww + +#ifdef GAME_DLL + // allow changing SIMD usage on the fly + if ( com_forceGenericSIMD.IsModified() ) { + idSIMD::InitProcessor( "game", com_forceGenericSIMD.GetBool() ); + } +#endif + + // make sure the random number counter is used each frame so random events + // are influenced by the player's actions + random.RandomInt(); + + if ( player ) { + // update the renderview so that any gui videos play from the right frame + view = player->GetRenderView(); + if ( view ) { + gameRenderWorld->SetRenderView( view ); + } + } + + // clear any debug lines from a previous frame + gameRenderWorld->DebugClearLines( time ); + + // clear any debug polygons from a previous frame + gameRenderWorld->DebugClearPolygons( time ); + + // set the user commands for this frame + memcpy( usercmds, clientCmds, numClients * sizeof( usercmds[ 0 ] ) ); + + // free old smoke particles + smokeParticles->FreeSmokes(); + + // process events on the server + ServerProcessEntityNetworkEventQueue(); + + // update our gravity vector if needed. + UpdateGravity(); + + // create a merged pvs for all players + SetupPlayerPVS(); + + // sort the active entity list + SortActiveEntityList(); + + timer_think.Clear(); + timer_think.Start(); + PROFILE_START("Misc_Think", PROFMASK_NORMAL); // HUMANHEAD pdm + + // HUMANHEAD pdm: This loop reworked to support debugger and dormant timings + // let entities think + if ( g_timeentities.GetFloat() || g_debugger.GetInteger() || g_dormanttests.GetBool()) { + num = 0; + for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { + if ( g_cinematic.GetBool() && inCinematic && !ent->cinematic ) { + ent->GetPhysics()->UpdateTime( time ); + continue; + } + dormant = false; + timer_singledormant.Clear(); + if (!ent->fl.neverDormant) { + timer_singledormant.Start(); + dormant = ent->CheckDormant(); + timer_singledormant.Stop(); + } + + timer_singlethink.Clear(); + if( !dormant ) { + timer_singlethink.Start(); + ent->Think(); + timer_singlethink.Stop(); + } + + ms = timer_singlethink.Milliseconds(); + if ( g_timeentities.GetFloat() && ms >= g_timeentities.GetFloat() ) { + gameLocal.Printf( "%d: entity '%s': %.1f ms\n", time, ent->name.c_str(), ms ); + } + if (g_debugger.GetInteger() || g_dormanttests.GetBool()) { + ent->thinkMS = ent->thinkMS*thinkalpha + (1.0-thinkalpha)*ms; + } + if (g_dormanttests.GetBool() && !ent->fl.neverDormant) { + float msDormant = timer_singledormant.Milliseconds(); + ent->dormantMS = ent->dormantMS*thinkalpha + (1.0f-thinkalpha)*msDormant; + + if (ent->dormantMS > ent->thinkMS && !dormant) { + // If we're spending more time on dormant checks than on actually thinking, + // turn dormant checks off on that class. + Printf("%30s Dms=%6.4f Tms=%6.4f\n", ent->GetClassname(), ent->dormantMS, ent->thinkMS); + } + } + num++; + } + } else { + if ( inCinematic ) { + num = 0; + for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { + if ( g_cinematic.GetBool() && !ent->cinematic ) { + ent->GetPhysics()->UpdateTime( time ); + continue; + } + // HUMANHEAD JRM + if( !ent->CheckDormant() ) { + ent->Think(); + } + num++; + } + } else { + num = 0; + for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { + // HUMANHEAD JRM + if( !ent->CheckDormant() ) { + ent->Think(); + } + num++; + } + } + } + + // remove any entities that have stopped thinking + if ( numEntitiesToDeactivate ) { + idEntity *next_ent; + int c = 0; + for( ent = activeEntities.Next(); ent != NULL; ent = next_ent ) { + next_ent = ent->activeNode.Next(); + if ( !ent->thinkFlags ) { + ent->activeNode.Remove(); + c++; + } + } + //assert( numEntitiesToDeactivate == c ); + numEntitiesToDeactivate = 0; + } + + PROFILE_STOP("Misc_Think", PROFMASK_NORMAL); + + timer_think.Stop(); + timer_events.Clear(); + timer_events.Start(); + + // service any pending events + idEvent::ServiceEvents(); + + timer_events.Stop(); + + // free the player pvs + FreePlayerPVS(); + + // do multiplayer related stuff + if ( isMultiplayer ) { + mpGame.Run(); + } + + // display how long it took to calculate the current game frame + if ( g_frametime.GetBool() ) { + Printf( "game %d: all:%.1f th:%.1f ev:%.1f %d ents \n", + time, timer_think.Milliseconds() + timer_events.Milliseconds(), + timer_think.Milliseconds(), timer_events.Milliseconds(), num ); + } + + // build the return value + ret.consistencyHash = 0; + ret.sessionCommand[0] = 0; + + if ( !isMultiplayer && player ) { + ret.health = player->health; + ret.heartRate = 0; //player->heartRate; // HUMANHEAD pdm: not used + ret.stamina = idMath::FtoiFast( player->stamina ); + // combat is a 0-100 value based on lastHitTime and lastDmgTime + // each make up 50% of the time spread over 10 seconds + ret.combat = 0; + if ( player->lastDmgTime > 0 && time < player->lastDmgTime + 10000 ) { + ret.combat += 50.0f * (float) ( time - player->lastDmgTime ) / 10000; + } + if ( player->lastHitTime > 0 && time < player->lastHitTime + 10000 ) { + ret.combat += 50.0f * (float) ( time - player->lastHitTime ) / 10000; + } + } + + // see if a target_sessionCommand has forced a changelevel + if ( sessionCommand.Length() ) { + strncpy( ret.sessionCommand, sessionCommand, sizeof( ret.sessionCommand ) ); + break; + } + + // make sure we don't loop forever when skipping a cinematic + if ( skipCinematic && ( time > cinematicMaxSkipTime ) ) { + Warning( "Exceeded maximum cinematic skip length. Cinematic may be looping infinitely." ); + skipCinematic = false; + break; + } + } while( ( inCinematic || ( time < cinematicStopTime ) ) && skipCinematic ); + + ret.syncNextGameFrame = skipCinematic; + if ( skipCinematic ) { + soundSystem->SetMute( false ); + skipCinematic = false; + } + + // show any debug info for this frame + RunDebugInfo(); + D_DrawDebugLines(); + + //HUMANHEAD rww + if (logitechLCDEnabled) { + PROFILE_START("LogitechLCDUpdate", PROFMASK_NORMAL); + LogitechLCDUpdate(); + PROFILE_STOP("LogitechLCDUpdate", PROFMASK_NORMAL); + } + //HUMANHEAD END + + return ret; +} + +// PDMMERGE PERSISTENTMERGE: Overridden, Done for 6-03-05 merge +bool hhGameLocal::Draw( int clientNum ) { + //HUMANHEAD rww - dedicated server update + if (clientNum == -1) { + gameSoundWorld->PlaceListener( vec3_origin, mat3_identity, -1, gameLocal.time, "Undefined" ); + return false; + } + //HUMANHEAD END + + if ( isMultiplayer ) { + return mpGame.Draw( clientNum ); + } + + //HUMANHEAD: aob - changed idPlayer to hhPlayer + hhPlayer *player = static_cast(entities[ clientNum ]); + + if ( !player ) { + return false; + } + + // render the scene + // HUMANHEAD pdm: added case of vehicle determining the player hud + if (player->InVehicle()) { + player->playerView.RenderPlayerView( player->GetVehicleInterfaceLocal()->GetHUD() ); + } + else { + player->playerView.RenderPlayerView( player->hud ); + } + // HUMANHEAD END + +#if INGAME_DEBUGGER_ENABLED // HUMANHEAD pdm + debugger.UpdateDebugger(); +#endif + + return true; +} + + +float hhGameLocal::GetTimeScale() const { + return hhMath::ClampFloat( VECTOR_EPSILON, 10.0f, cvarSystem->GetCVarFloat("timescale") ); +} + +// +// SimpleMonstersWithinRadius() +// TODO: Use this exclusively and remove MonstersWithinRadius when old AI is removed +int hhGameLocal::SimpleMonstersWithinRadius( const idVec3 org, float radius, idAI **monstList, int maxCount ) const { + idBounds bo( org ); + int entCount = 0; + + bo.ExpandSelf( radius ); + for(int i=0;iGetPhysics()->GetAbsBounds().IntersectsBounds( bo ) ) { + monstList[entCount++] = hhMonsterAI::allSimpleMonsters[i]; + } + } + + return entCount; +} + +// +// SpawnMapEntities() +// +void hhGameLocal::SpawnMapEntities( void ) { + if ( reactionHandler ) { + delete reactionHandler; + } + reactionHandler = new hhReactionHandler; + idGameLocal::SpawnMapEntities(); +} + +/* +============== +hhGameLocal::SendMessageAI +============== +*/ +void hhGameLocal::SendMessageAI( const idEntity* entity, const idVec3& origin, float radius, const idEventDef& message ) { + idAI *simplemonsters[ MAX_GENTITIES ]; + + if( radius < VECTOR_EPSILON || isClient ) { + return; + } + for( int i = gameLocal.SimpleMonstersWithinRadius(origin, radius, simplemonsters) - 1; i >= 0; --i ) { + if( simplemonsters[i]->GetHealth() > 0 && simplemonsters[i]->IsActive() && !simplemonsters[i]->IsHidden() ) { + simplemonsters[i]->ProcessEvent( &message, entity ); + } + } +} + +/* +============== +hhGameLocal::MatterTypeToMatterName +============== +*/ +const char* hhGameLocal::MatterTypeToMatterName( surfTypes_t type ) const { + return sufaceTypeNames[ type ]; +} + +/* +============== +hhGameLocal::MatterTypeToMatterKey +============== +*/ +const char* hhGameLocal::MatterTypeToMatterKey( const char* prefix, surfTypes_t type ) const { + return va( "%s_%s", prefix, MatterTypeToMatterName(type) ); +} + +/* +============== +hhGameLocal::MatterNameToMatterType +============== +*/ +surfTypes_t hhGameLocal::MatterNameToMatterType( const char* name ) const { + surfTypes_t type = SURFTYPE_NONE; + + for( int ix = 0; ix < MAX_SURFACE_TYPES; ++ix ) { + type = (surfTypes_t)ix; + if( idStr::Icmp(name, MatterTypeToMatterName(type)) ) { + continue; + } + + return type; + } + + return SURFTYPE_NONE; +} + +/* +============== +hhGameLocal::GetMatterType +============== +*/ +surfTypes_t hhGameLocal::GetMatterType( const trace_t& trace, const char* descriptor ) const { + return GetMatterType( gameLocal.GetTraceEntity(trace), trace.c.material, descriptor ); +} + +/* +============== +hhGameLocal::GetMatterType +============== +*/ +surfTypes_t hhGameLocal::GetMatterType( const idEntity *ent, const idMaterial *material, const char* descriptor ) const { + surfTypes_t type = SURFTYPE_NONE; + const char *matterName = NULL; + const idMaterial *remapped = material; + + // If the entityDef has a matter specified, always use it + // Otherwise, use the materials matter. If none, default to metal. + if( ent && ent->spawnArgs.GetString("matter", NULL, &matterName ) ) { + type = MatterNameToMatterType( matterName ); + } + else { + if (ent && ent->GetSkin()) { + remapped = ent->GetSkin()->RemapShaderBySkin(material); + } + type = (remapped != NULL) ? remapped->GetSurfaceType() : SURFTYPE_METAL; + } + + // OBS: Will never happen + if( !type ) { +#if 0 + Warning("No matter for hit surface"); + Warning(" Entity: %s", ent ? ent->name.c_str() : "none"); + Warning(" Class: %s", ent ? ent->GetClassname() : "none"); + Warning(" Material: %s", material ? material->GetName() : "none"); +#endif + type = SURFTYPE_METAL; + } + + if( g_debugMatter.GetInteger() > 0 && descriptor && descriptor[0] ) { + Printf("%s: [%s] ent=[%s] mat=[%s]\n", + descriptor, + MatterTypeToMatterName(type), + ent ? ent->GetName() : "none", + material ? material->GetName() : "none"); + } + + return type; +} + +void hhGameLocal::AlertAI( idEntity *ent ) { + if ( ent && ent->IsType( idActor::Type ) ) { + // alert them for the next frame + lastAIAlertTime = time + msec; + lastAIAlertEntity = static_cast( ent ); + lastAIAlertRadius = 0; //no radius required by default + } +} + +void hhGameLocal::AlertAI( idEntity *ent, float radius ) { + if ( ent && ent->IsType( idActor::Type ) ) { + // alert them for the next frame + lastAIAlertTime = time + msec; + lastAIAlertEntity = static_cast( ent ); + lastAIAlertRadius = radius; //radius of effect + } +} + +//================ +//hhGameLocal::Save +//================ +void hhGameLocal::Save( idSaveGame *savefile ) const { + int i, num = talonTargets.Num(); + savefile->WriteInt( num ); + for( i = 0; i < num; i++ ) { + savefile->WriteObject( talonTargets[i] ); + } + + reactionHandler->Save( savefile ); + savefile->WriteClipModel( dwWorldClipModel ); + //HUMANHEAD rww +#if _HH_INLINED_PROC_CLIPMODELS + savefile->WriteInt(inlinedProcClipModels.Num()); + for (i = 0; i < inlinedProcClipModels.Num(); i++) { + savefile->WriteClipModel(inlinedProcClipModels[i]); + } +#endif + //HUMANHEAD END + savefile->WriteVec3( gravityNormal ); + savefile->WriteObject( sunCorona ); + ddaManager->Save( savefile ); + savefile->WriteBool( bIsLOTA ); + savefile->WriteFloat( lastAIAlertRadius ); + + num = staticClipModels.Num(); + savefile->WriteInt( num ); + for( i = 0; i < num; i++ ) { + savefile->WriteClipModel( staticClipModels[i] ); + } + + num = staticRenderEntities.Num(); + savefile->WriteInt( num ); + for( i = 0; i < num; i++ ) { + savefile->WriteRenderEntity( *staticRenderEntities[i] ); + } + + savefile->WriteInt( hands.Num() ); + for ( i = 0; i < hands.Num(); i++ ) { + hands[i].Save( savefile ); + } +} + +//================ +//hhGameLocal::Restore +//================ +void hhGameLocal::Restore( idRestoreGame *savefile ) { + idEntity *ent; + int i, num; + idClipModel *model; + renderEntity_t *renderEnt; + savefile->ReadInt( num ); + talonTargets.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->ReadObject( reinterpret_cast ( ent ) ); + talonTargets[i] = ent; + } + + reactionHandler = new hhReactionHandler; + reactionHandler->Restore( savefile ); + savefile->ReadClipModel( dwWorldClipModel ); + //HUMANHEAD rww +#if _HH_INLINED_PROC_CLIPMODELS + int numInlinedProcClipModels; + savefile->ReadInt(numInlinedProcClipModels); + for (i = 0; i < numInlinedProcClipModels; i++) { + savefile->ReadClipModel(inlinedProcClipModels[i]); + } +#endif + //HUMANHEAD END + savefile->ReadVec3( gravityNormal ); + savefile->ReadObject( reinterpret_cast ( sunCorona ) ); + ddaManager->Restore ( savefile ); + savefile->ReadBool( bIsLOTA ); + savefile->ReadFloat( lastAIAlertRadius ); + + savefile->ReadInt( num ); + staticClipModels.DeleteContents( false ); + staticClipModels.SetNum( num ); + for( i = 0; i < num; i++ ) { + savefile->ReadClipModel( model ); + staticClipModels[i] = model; + } + + savefile->ReadInt( num ); + staticRenderEntities.DeleteContents( false ); + staticRenderEntities.SetNum( num ); + for( i = 0; i < num; i++ ) { + renderEnt = new renderEntity_t; + savefile->ReadRenderEntity( *renderEnt ); + gameRenderWorld->AddEntityDef( renderEnt ); + staticRenderEntities[i] = renderEnt; + } + + savefile->ReadInt( num ); + hands.SetNum( num ); + for ( i = 0; i < num; i++ ) { + hands[i].Restore( savefile ); + } +} + +bool hhGameLocal::InhibitEntitySpawn( idDict &spawnArgs ) { + const char *modelName = spawnArgs.GetString( "model" ); + int inlineEnt = spawnArgs.GetInt( "inline" ); + + if( idStr::Icmp( spawnArgs.GetString( "classname", NULL ), "func_static" ) == 0 && // Only deal with func_static objects + !spawnArgs.GetBool( "neverInline", "0" ) && // and we're not flagged neverInline + ( inlineEnt != 0 || // and we're flagged explicit inline + world->spawnArgs.GetBool( "inlineAllStatics" ) ) ) { // or we're inlining all statics + +#if _HH_INLINED_PROC_CLIPMODELS + if (inlineEnt != 3) { //HUMANHEAD rww +#endif + // Handle explicit clip models + idClipModel *model = NULL; + const char *temp = spawnArgs.GetString( "clipmodel", NULL ); + + if( temp ) { + if ( idClipModel::CheckModel( temp ) ) { + model = new idClipModel( temp ); + } + } + + if( model || !spawnArgs.GetBool( "noclipmodel", "0" ) ) { + // Get the origin and axis of the object + idMat3 axis; + // get the rotation matrix in either full form, or single angle form + if ( !spawnArgs.GetMatrix( "rotation", "1 0 0 0 1 0 0 0 1", axis ) ) { + float angle = spawnArgs.GetFloat( "angle" ); + if ( angle != 0.0f ) { + axis = idAngles( 0.0f, angle, 0.0f ).ToMat3(); + } else { + axis.Identity(); + } + } + + idVec3 origin = spawnArgs.GetVector( "origin" ); + + // Create a clip model for the static object and position it correctly + if( !model ) { + model = new idClipModel( spawnArgs.GetString( "model" ) ); + } + if( spawnArgs.GetBool( "bulletsOnly", "0" ) ) { + model->SetContents( CONTENTS_SHOOTABLE|CONTENTS_SHOOTABLEBYARROW ); + } else if( spawnArgs.GetBool( "solid", "1" ) ) { + model->SetContents( CONTENTS_SOLID ); + } else { + model->SetContents( 0 ); + } + model->SetPosition( origin, axis ); + model->SetEntity( world ); + model->Link( clip ); + staticClipModels.Append( model ); + } +#if _HH_INLINED_PROC_CLIPMODELS + } +#endif + + if ( inlineEnt == 1 ) { // This means we must hand the model, too + renderEntity_t *renderEnt = new renderEntity_t; + gameEdit->ParseSpawnArgsToRenderEntity( &spawnArgs, renderEnt ); + HH_ASSERT( renderEnt->hModel && !renderEnt->callback && renderEnt->shaderParms[ SHADERPARM_ANY_DEFORM ] == DEFORMTYPE_NONE ); // Inlined statics can't have a callback + renderEnt->entityNum = 0; // WorldSpawn + gameRenderWorld->AddEntityDef( renderEnt ); + staticRenderEntities.Append( renderEnt ); + } + + // Don't spawn an entity for an inlined static + return true; + } else { + return idGameLocal::InhibitEntitySpawn( spawnArgs ); + } +} + +//HUMANHEAD rww +#if _HH_INLINED_PROC_CLIPMODELS +void hhGameLocal::CreateInlinedProcClip(idEntity *clipOwner) { + assert(world); + assert(inlinedProcClipModels.Num() == 0); //should never have existing models when this is called + int n = collisionModelManager->GetNumInlinedProcClipModels(); + if (n <= 0) { //nothing to work with, leave + return; + } + + for (int i = 0; i < n; i++) { + idClipModel *chunk = new idClipModel(va("%s%i", PROC_CLIPMODEL_STRING_PRFX, i)); + chunk->SetContents(CONTENTS_SOLID); + chunk->SetEntity(clipOwner); + chunk->Link(clip); + inlinedProcClipModels.Append(chunk); + } +} +#endif +//HUMANHEAD END + +// Finds entities matching a specified type. If last is NULL it starts at the beginning of the list. +// If last is valid, starts looking at that entities position in the list +idEntity *hhGameLocal::FindEntityOfType(const idTypeInfo &type, idEntity *last) { + idEntity *ent; + if (last) { + ent = last->activeNode.Next(); + } + else { + ent = activeEntities.Next(); + } + + for( ; ent != NULL; ent = ent->activeNode.Next() ) { + if (ent->IsType(type)) { + return ent; + } + } + return NULL; +} + +float hhGameLocal::GetDDAValue( void ) { + if ( !ddaManager ) { + return 0.5; + } else { + return ddaManager->GetDifficulty(); + } +} + + +void hhGameLocal::ClearStaticData( void ) { + delete dwWorldClipModel; //rww - this needs to be done as well, dw clipmodel expects world as owner. + dwWorldClipModel = NULL; + +#if _HH_INLINED_PROC_CLIPMODELS + inlinedProcClipModels.DeleteContents(true); //HUMANHEAD rww +#endif + + staticClipModels.DeleteContents( true ); // Clear any inlined static clip models + staticRenderEntities.DeleteContents( true ); // Clear any inlined static render entities +} + +float hhGameLocal::TimeBasedRandomFloat(void) { + if (gameLocal.isMultiplayer) { //rand based on time step + timeRandom = (1103515245 * timeRandom + 12345)%(1<<31); + return (timeRandom)/( float )( (1<<31) + 1 ); + } + else { //give sp complete randomness + return random.RandomFloat(); + } +} + +bool hhGameLocal::PlayerIsDeathwalking(void) { + HH_ASSERT( !isMultiplayer ); + hhPlayer *player = static_cast (GetLocalPlayer()); + HH_ASSERT( player ); + return player->IsDeathWalking(); +} + + +// Abstraction to hide the ugly code for text exchange between engine and game +void hhGameLocal::GetTip(const char *binding, idStr &keyMaterialString, idStr &keyString, bool &isWide) { + + char keyMaterial[256]; // No passing idStr between game and engine + char key[256]; // No passing idStr between game and engine + keyMaterial[0] = '\0'; + key[0] = '\0'; + if (binding) { + common->MaterialKeyForBinding(binding, keyMaterial, key, isWide); + keyMaterialString = keyMaterial; + keyString = key; + } +} + +/* + hhGameLocal::SetTip + + gui gui to display tip + binding key binding to display associated key + tip textual tip to display + top optional top image + mtr optional material, overrides binding image + prefix optional prefix for gui state keys +*/ +bool hhGameLocal::SetTip(idUserInterface* gui, const char *binding, const char *tip, const char *topMaterial, const char *overrideMaterial, const char *prefix) { + assert(gui); + + idStr keyMaterial, key; + bool keywide = false; + if ( !spawnArgs.GetString("mtr_override", "", keyMaterial) ) { + if (binding) { + GetTip(binding, keyMaterial, key, keywide); + } + } + + const char *translated = common->GetLanguageDict()->GetString(tip); + + if (prefix != NULL) { + gui->SetStateBool( va("%s_keywide", prefix), keywide ); + gui->SetStateString( va("%s_tip", prefix), translated ? translated : "" ); + gui->SetStateString( va("%s_key", prefix), key.c_str() ); + gui->SetStateString( va("%s_keyMaterial", prefix), keyMaterial.c_str() ); + gui->SetStateString( va("%s_topMaterial", prefix), topMaterial ? topMaterial : "" ); + } + else { + gui->SetStateBool( "keywide", keywide ); + gui->SetStateString( "tip", translated ? translated : "" ); + gui->SetStateString( "key", key.c_str() ); + gui->SetStateString( "keyMaterial", keyMaterial.c_str() ); + gui->SetStateString( "topMaterial", topMaterial ? topMaterial : "" ); + } + + return (keyMaterial.Length() > 0); +} + diff --git a/src/Prey/prey_game.h b/src/Prey/prey_game.h new file mode 100644 index 0000000..99610cc --- /dev/null +++ b/src/Prey/prey_game.h @@ -0,0 +1,116 @@ +#ifndef __PREY_GAME_H +#define __PREY_GAME_H + +class hhReactionHandler; +class hhSunCorona; // CJR +class hhHand; +class hhAIInspector; + +#ifdef GAME_DLL +extern idCVar com_forceGenericSIMD; +#endif + +//HUMANHEAD: aob - needed for networking to send the least amount of bits +extern const int DECL_MAX_TYPES_NUM_BITS; +//HUMANHEAD END + +class hhGameLocal : public idGameLocal { +public: + virtual void Init( void ); + virtual void Shutdown( void ); + void UnregisterEntity( idEntity *ent ); + + virtual void MapShutdown( void ); + virtual void InitFromNewMap( const char *mapName, idRenderWorld *renderWorld, idSoundWorld *soundWorld, bool isServer, bool isClient, int randseed ); + virtual void RadiusDamage( const idVec3 &origin, idEntity *inflictor, idEntity *attacker, idEntity *ignoreDamage, idEntity *ignorePush, const char *damageDefName, float dmgPower = 1.0f );// jrm + virtual void RadiusPush( const idVec3 &origin, const float radius, const float push, const idEntity *inflictor, const idEntity *ignore, float inflictorScale, const bool quake ); + //HUMANHEAD rww + virtual void LogitechLCDUpdate(void); + //HUMANHEAD END + virtual gameReturn_t RunFrame( const usercmd_t *clientCmds ); + virtual void CacheDictionaryMedia( const idDict *dict ); + virtual bool Draw( int clientNum ); + + // added functionality functions + void RegisterTalonTarget( idEntity *ent ); + idEntity* SpawnClientObject( const char* objectName, idDict* additionalArgs ); //rww + idEntity* SpawnObject( const char* objectName, idDict* additionalArgs = NULL ); + + void GetTip(const char *binding, idStr &keyMaterialString, idStr &keyString, bool &isWide); + bool SetTip(idUserInterface* gui, const char *binding, const char *tip, const char *topMaterial = NULL, const char *overrideMaterial = NULL, const char *prefix = NULL); + void SetSunCorona( hhSunCorona *ent ) { sunCorona = ent; } // CJR + hhSunCorona * GetSunCorona( void ) { return sunCorona; } // CJR + idEntity * FindEntityOfType(const idTypeInfo &type, idEntity *last); + + // nla + idDict & GetSpawnArgs() { return( spawnArgs ); }; + hhReactionHandler* GetReactionHandler() { return reactionHandler; } + float GetTimeScale() const; + const idVec3& GetGravityNormal() { gravityNormal = gravity; gravityNormal.Normalize(); return gravityNormal; } + int SimpleMonstersWithinRadius( const idVec3 org, float radius, idAI **monstList, int maxCount = MAX_GENTITIES ) const; + + void SendMessageAI( const idEntity* entity, const idVec3& origin, float radius, const idEventDef& message ); + + const char* MatterTypeToMatterName( surfTypes_t type ) const; + const char* MatterTypeToMatterKey( const char* prefix, surfTypes_t type ) const; + surfTypes_t MatterNameToMatterType( const char* name ) const; + surfTypes_t GetMatterType( const trace_t& trace, const char* descriptor = "impact" ) const; + surfTypes_t GetMatterType( const idEntity *ent, const idMaterial *material, const char* descriptor = "impact" ) const; + + class hhDDAManager* GetDDA() { return ddaManager; } + + bool IsLOTA() { return bIsLOTA; } + void AlertAI( idEntity *ent ); + void AlertAI( idEntity *ent, float radius ); + + // Special case, these must be virtual -mdl + virtual void Save( idSaveGame *savefile ) const; + virtual void Restore( idRestoreGame *savefile ); + + float GetDDAValue( void ); + + void ClearStaticData( void ); + + float TimeBasedRandomFloat(void); //HUMANHEAD rww + +#if _HH_INLINED_PROC_CLIPMODELS + virtual void CreateInlinedProcClip(idEntity *clipOwner); //HUMANHEAD rww +#endif + + virtual bool PlayerIsDeathwalking( void ); + + idList talonTargets; + float lastAIAlertRadius; + + idList< idEntityPtr< hhHand > > hands; + + idEntityPtr inspector; + +protected: +#if DEATHWALK_AUTOLOAD + virtual void SpawnAppendedMapEntities(); +#endif + virtual bool InhibitEntitySpawn( idDict &spawnArgs ); // HUMANHEAD mdl + +protected: + virtual void SpawnMapEntities( void ); // JRM + idClipModel * dwWorldClipModel; +#if _HH_INLINED_PROC_CLIPMODELS + idList inlinedProcClipModels; //HUMANHEAD rww - chopped geometry grabbed from proc and turned into clipmodel data +#endif + + idVec3 gravityNormal; + + hhSunCorona *sunCorona; // CJR + + //HUMANHEAD rww - moved this down, it is using mixed memory inside and outside the managed heap as is, very messy. + //class hhDDAManager* ddaManager; + //HUMANHEAD END + + bool bIsLOTA; // CJR: If true, this map is a LOTA map + + idList staticClipModels; // HUMANHEAD mdl: For inlined static clip models + idList staticRenderEntities; // HUMANHEAD mdl: For inlined static models +}; + +#endif diff --git a/src/Prey/prey_items.cpp b/src/Prey/prey_items.cpp new file mode 100644 index 0000000..6f3641d --- /dev/null +++ b/src/Prey/prey_items.cpp @@ -0,0 +1,639 @@ +//************************************************************************** +//** +//** PREY_ITEMS.CPP +//** +//** Game code for Prey-specific items +//** +//************************************************************************** + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// hhItem +//========================================================================== +idEventDef EV_SetPickupState( "", "dd" ); + +CLASS_DECLARATION( idItem, hhItem ) + EVENT( EV_SetPickupState, hhItem::Event_SetPickupState ) + EVENT( EV_RespawnItem, hhItem::Event_Respawn ) +END_CLASS + +/* +================ +hhItem::Spawn +================ +*/ +void hhItem::Spawn() { + // Logic to allow item cabinets to deny pickups until desired + if( spawnArgs.GetBool("enablePickup", "1") ) { + EnablePickup(); + } else { + DisablePickup(); + } + + // Diversity for the blinking highlight shells + SetShaderParm(SHADERPARM_DIVERSITY, gameLocal.random.RandomFloat()); +} + +/* +================ +hhItem::EnablePickup +================ +*/ +void hhItem::EnablePickup() { + GetPhysics()->EnableClip(); + GetPhysics()->SetContents( CONTENTS_TRIGGER ); + canPickUp = !spawnArgs.GetBool("triggerFirst"); +} + +/* +================ +hhItem::DisablePickup +================ +*/ +void hhItem::DisablePickup() { + GetPhysics()->DisableClip(); + GetPhysics()->SetContents( 0 ); + canPickUp = false; +} + +/* +================ +hhItem::Pickup +================ +*/ +bool hhItem::Pickup( idPlayer *player ) { + if( gameLocal.isMultiplayer ) { + if( gameLocal.IsCooperative() ) { + CoopPickup( player ); + } else { + if (MultiplayerPickup( player )) { + //HUMANHEAD rww - see if the weapon has a dropped energy type on it + const char *droppedEnergy = spawnArgs.GetString("def_droppedEnergyType"); + if (droppedEnergy && droppedEnergy[0]) { //if it does, copy it to the player's inventory + const idDeclEntityDef *energyDecl = gameLocal.FindEntityDef(droppedEnergy, false); + if (energyDecl) { + const idDeclEntityDef *fireDecl = gameLocal.FindEntityDef(energyDecl->dict.GetString("def_fireInfo"), false); + if (fireDecl) { + hhPlayer *hhPl = static_cast(player); + int num = hhPl->GetWeaponNum("weaponobj_soulstripper"); + assert(num); + hhPl->inventory.energyType = droppedEnergy; + hhPl->weaponInfo[ num ].ammoMax = fireDecl->dict.GetInt("ammoAmount"); + hhPl->spawnArgs.SetInt( "max_ammo_energy", fireDecl->dict.GetInt("ammoAmount") ); + hhPl->inventory.ammo[hhPl->inventory.AmmoIndexForAmmoClass("ammo_energy")]=0; + hhPl->Give( "ammo_energy", fireDecl->dict.GetString("ammoAmount") ); + } + } + } + //HUMANHEAD END + } + } + } else { + SinglePlayerPickup( player ); + } + return true; +} + +/* +================ +hhItem::ShouldRespawn +================ +*/ +bool hhItem::ShouldRespawn( float* respawnDelay ) const { + float respawn = spawnArgs.GetFloat( "respawn", "5.0" ); + if (gameLocal.isMultiplayer) { //rww - override default in mp + respawn *= 2.0f; + } + + if( respawnDelay && respawn > 0.0f ) { + *respawnDelay = respawn; + } + + return (respawn > 0.0f) && !spawnArgs.GetBool( "dropped" ) && gameLocal.isMultiplayer; +} + +/* +================ +hhItem::PostRespawn +================ +*/ +void hhItem::PostRespawn( float delay ) { + const char *sfx = spawnArgs.GetString( "fxRespawn" ); + if ( sfx && *sfx ) { + PostEventSec( &EV_RespawnFx, delay - 0.5f ); + } + PostEventSec( &EV_RespawnItem, delay ); +} + +/* +================ +hhItem::DetermineRemoveOrRespawn +================ +*/ +void hhItem::DetermineRemoveOrRespawn( int removeDelay ) { + bool keepThinking = false; + + // clear our contents so the object isn't picked up twice + SetPickupState( 0, false ); + + if (gameLocal.isMultiplayer && !IsType(idMoveableItem::Type)) { //check for a respawning skin in mp + idStr respawningSkin; + if (spawnArgs.GetString("skin_itemRespawning", "", respawningSkin)) { + SetSkin(declManager->FindSkin(respawningSkin.c_str())); + keepThinking = true; + } + } + + if (!keepThinking) { + // hide the model + Hide(); + } + + // add the highlight shell + if ( itemShellHandle != -1 ) { + gameRenderWorld->FreeEntityDef( itemShellHandle ); + itemShellHandle = -1; + } + + float respawnDelay = 0.0f; + if ( ShouldRespawn(&respawnDelay) ) { + PostRespawn( respawnDelay ); + } else { + // give some time for the pickup sound to play + // FIXME: Play on the owner + PostEventMS( &EV_Remove, removeDelay ); + } + + if (!keepThinking) { + BecomeInactive( TH_THINK ); + } +} + +/* +================ +hhItem::AnnouncePickup +================ +*/ +int hhItem::AnnouncePickup( idPlayer* player ) { + ServerSendEvent( EVENT_PICKUP, NULL, false, -1 ); + + // play pickup sound + int soundLength = 0; + StartSound( "snd_acquire", SND_CHANNEL_ITEM, 0, false, &soundLength ); + + // trigger our targets + ActivateTargets( player ); + + return soundLength; +} + +/* +================ +hhItem::SinglePlayerPickup +================ +*/ +void hhItem::SinglePlayerPickup( idPlayer *player ) { + if ( !GiveToPlayer( player ) ) { + return; + } + + DetermineRemoveOrRespawn( AnnouncePickup(player) ); +} + +/* +================ +hhItem::CoopPickup +================ +*/ +void hhItem::CoopPickup( idPlayer* player ) { + const char* weaponDef = spawnArgs.GetString( "def_weapon" ); + if( weaponDef && weaponDef[0] ) { + CoopWeaponPickup( player ); + } else { + CoopItemPickup( player ); + } +} + +/* +================ +hhItem::CoopWeaponPickup +================ +*/ +void hhItem::CoopWeaponPickup( idPlayer *player ) { + float delay = 0.0f; + + if( !GiveToPlayer(player) ) { + return; + } + + AnnouncePickup( player ); + + ShouldRespawn( &delay ); + SetPickupState( 0, false ); + PostEventSec( &EV_SetPickupState, delay, CONTENTS_TRIGGER, true ); +} + +/* +================ +hhItem::CoopItemPickup +================ +*/ +void hhItem::CoopItemPickup( idPlayer *player ) { + idEntity* entity = NULL; + hhPlayer* castPlayer = NULL; + + for( int ix = 0; ix < gameLocal.numClients; ++ix ) { + entity = gameLocal.entities[ix]; + if( !entity || !entity->IsType(hhPlayer::Type) ) { + continue; + } + + castPlayer = static_cast( entity ); + castPlayer->GiveItem( this ); + } + + DetermineRemoveOrRespawn( AnnouncePickup(player) ); +} + +/* +================ +hhItem::MultiplayerPickup +================ +*/ +bool hhItem::MultiplayerPickup( idPlayer *player ) { + if( !GiveToPlayer(player) ) { + return false; + } + + DetermineRemoveOrRespawn( AnnouncePickup(player) ); + return true; +} + +/* +================ +hhItem::SetPickupState +================ +*/ +void hhItem::SetPickupState( int contents, bool allowPickup ) { + GetPhysics()->SetContents( contents ); + canPickUp = allowPickup; +} + +/* +================ +hhItem::Event_SetPickupState +================ +*/ +void hhItem::Event_SetPickupState( int contents, bool allowPickup ) { + SetPickupState( contents, allowPickup ); +} + +/* +================ +hhItem::Event_Respawn +================ +*/ +void hhItem::Event_Respawn( void ) { + if ( !gameLocal.isClient ) { + ServerSendEvent( EVENT_RESPAWN, NULL, false, -1 ); + } + BecomeActive( TH_THINK ); + if (gameLocal.isMultiplayer && renderEntity.customSkin != GetNonRespawnSkin()) { + SetSkin(GetNonRespawnSkin()); //restore skin + } + Show(); + inViewTime = -1000; + lastCycle = -1; + + //HUMANHEAD: aob + SetPickupState( CONTENTS_TRIGGER, true ); + //HUMANHEAD END + + SetOrigin( orgOrigin ); + StartSound( "snd_respawn", SND_CHANNEL_ITEM ); +} + + +//============================================================================= +// hhItemSoul +//============================================================================= + +idEventDef EV_PlaySpiritSound( "playSpiritSound", NULL ); +idEventDef EV_Broadcast_AssignFx_Spirit( "", "e" ); +idEventDef EV_Broadcast_AssignFx_Physical( "", "e" ); + +CLASS_DECLARATION( hhItem, hhItemSoul ) + EVENT( EV_TalonAction, hhItemSoul::Event_TalonAction ) + EVENT( EV_PostSpawn, hhItemSoul::Event_PostSpawn ) + EVENT( EV_PlaySpiritSound, hhItemSoul::Event_PlaySpiritSound ) + EVENT( EV_Broadcast_AssignFx_Spirit, hhItemSoul::Event_AssignFxSoul_Spirit ) + EVENT( EV_Broadcast_AssignFx_Physical, hhItemSoul::Event_AssignFxSoul_Physical ) +END_CLASS + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Spawn +// +//-------------------------------------------------------------------------- +void hhItemSoul::Spawn() { + idDict args; + idVec3 offset; + + fl.networkSync = true; //rww + fl.clientEvents = true; //rww + + spawnArgs.SetBool("dropped", true); //rww - don't respawn + + bFollowTriggered = false; + + // Only allow this item to be spawned if we can spirit walk + if (!gameLocal.isMultiplayer) { + hhPlayer *player = static_cast( gameLocal.GetLocalPlayer() ); + if ( player ) { + if ( !player->inventory.requirements.bCanSpiritWalk ) { + Hide(); + PostEventMS( &EV_Remove, 0 ); // Remove this soul before it can be spawned + return; + } + } + } + + BecomeActive( TH_THINK | TH_PHYSICS ); + + // Remove the lifeforce after 60 seconds if it hasn't been picked up + if (gameLocal.isMultiplayer) { //rww + PostEventSec( &EV_Remove, spawnArgs.GetFloat( "lifeTime_mp", "30" ) ); + } + else { + PostEventSec( &EV_Remove, spawnArgs.GetFloat( "lifeTime", "60" ) ); + } + + // Play the spirit sound, after a 10 second delay + PostEventSec( &EV_PlaySpiritSound, spawnArgs.GetFloat( "spiritSoundDelay", "10" ) ); + + renderEntity.onlyVisibleInSpirit = true; + + velocity = vec3_origin; + acceleration = vec3_origin; + surge = 0.0f; + parentMonster = NULL; + + PostEventMS( &EV_PostSpawn, 0 ); +} + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Event_PostSpawn +// +//-------------------------------------------------------------------------- + +void hhItemSoul::Event_PostSpawn() { + hhFxInfo fxInfo; + + if (!gameLocal.isMultiplayer) { + const char *monsterName = spawnArgs.GetString("monsterSpawnedBy", NULL); + if (monsterName) { + idEntity *ent = gameLocal.FindEntity(monsterName); + if (ent && ent->IsType(idAI::Type)) { + parentMonster = ent; + } + } + } + + // Spawn the spirit-only fx (this fx is only seen when spiritwalking) + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone( false ); + fxInfo.OnlyVisibleInSpirit( true ); + BroadcastFxInfo( spawnArgs.GetString("fx_lifeforce"), GetOrigin(), GetAxis(), &fxInfo, &EV_Broadcast_AssignFx_Spirit, false ); //rww - local + + // Spawn the physical realm fx + fxInfo.SetNormal( GetAxis()[2] ); + fxInfo.RemoveWhenDone( false ); + fxInfo.OnlyVisibleInSpirit( false ); + BroadcastFxInfo( spawnArgs.GetString("fx_lifeforcePhysical"), GetOrigin(), GetAxis(), &fxInfo, &EV_Broadcast_AssignFx_Physical, false ); //rww - local +} + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Event_AssignFxSoul +// +//-------------------------------------------------------------------------- + +void hhItemSoul::Event_AssignFxSoul_Spirit( hhEntityFx* fx ) { + soulFx = fx; +} + +void hhItemSoul::Event_AssignFxSoul_Physical( hhEntityFx* fx ) { + physicalSoulFx = fx; +} + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Event_PlaySpiritSound +// +//-------------------------------------------------------------------------- + +void hhItemSoul::Event_PlaySpiritSound() { + StartSound( "snd_spiritItemNear", SND_CHANNEL_SPIRITWALK, 0, true ); + + // Randomly start the sound over again (random delay between 12 and 22 seconds) + PostEventSec( &EV_PlaySpiritSound, 12.0f + spawnArgs.GetFloat( "spiritSoundDelay", "10.0" ) * gameLocal.random.RandomFloat() ); +} + +//-------------------------------------------------------------------------- +// +// hhItemSoul::~hhItemSoul +// +//-------------------------------------------------------------------------- + +hhItemSoul::~hhItemSoul() { + StopSound( SND_CHANNEL_SPIRITWALK, false ); //stop now, do not broadcast on purpose + SAFE_REMOVE( soulFx ); + SAFE_REMOVE( physicalSoulFx ); +} + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Ticker +// +//-------------------------------------------------------------------------- + +void hhItemSoul::Think() { + idVec3 playerOrigin; + idMat3 playerAxis; + // Move the soul towards the nearest spirit player + //COOP FIXME: Get the NEAREST spirit player for this to support multiplayer and coop. + + hhPlayer *player = NULL; + if (!gameLocal.isMultiplayer) { + player = static_cast( gameLocal.GetLocalPlayer() ); + } + else { + orgOrigin = GetOrigin(); + spin = false; + } + float followSpeed = spawnArgs.GetFloat( "followSpeed", "20" ); + + if ( !player || player->noclip ) { + velocity = vec3_origin; // mdl: If player is noclip, don't move an inch + } else { + player->GetViewPos( playerOrigin, playerAxis ); + + if ( !bFollowTriggered && player && player->IsSpiritWalking() ) { + acceleration = ( playerOrigin - playerAxis[2] * 20 ) - GetOrigin(); + float dot = playerAxis[0] * -acceleration; + + if ( dot > 0.95f ) { // Accelerate only if the player is looking at the soul + bFollowTriggered = true; + lastPlayerOrigin = playerOrigin; + } + } + + if ( bFollowTriggered ) { + //HUMANHEAD PCF mdl 04/28/06 - Changed factor from 0.01 to 0.05 for stationary players + float factor = 0.05f; // Keep acceleration slow enough for the player to see + if ( ( lastPlayerOrigin - playerOrigin ).LengthSqr() > 75.0f ) { + velocity = vec3_origin; // Player has moved, so clear the old velocity + lastPlayerOrigin = playerOrigin; + //HUMANHEAD PCF mdl 04/28/06 - Changed factor from 5 to 3.5 for moving players + factor = 3.5f; // Give acceleration a little boost so the soul doesn't seem slow when the player is moving around. + } + + acceleration = ( playerOrigin - playerAxis[2] * 20 ) - GetOrigin(); + velocity += (factor * acceleration * ( spawnArgs.GetFloat( "acceleration", "0.5" ) * spawnArgs.GetFloat( "surge", "0.01" ) ) * (60.0f * USERCMD_ONE_OVER_HZ)); + + SetOrigin( GetOrigin() + velocity ); + UpdateVisuals(); + + if ( soulFx.IsValid() ) { + soulFx->SetOrigin( GetOrigin() ); + soulFx->UpdateVisuals(); + } + if ( physicalSoulFx.IsValid() ) { + physicalSoulFx->SetOrigin( GetOrigin() ); + physicalSoulFx->UpdateVisuals(); + } + } + } + + // Must be done after this movement for proper relinking + hhItem::Think(); +} + +//rww - netcode +void hhItemSoul::WriteToSnapshot( idBitMsgDelta &msg ) const { + GetPhysics()->WriteToSnapshot(msg); + msg.WriteBits(IsHidden() || !soulFx.IsValid() || !soulFx->IsActive(TH_THINK), 1); +} + +void hhItemSoul::ReadFromSnapshot( const idBitMsgDelta &msg ) { + GetPhysics()->ReadFromSnapshot(msg); + bool hidden = !!msg.ReadBits(1); + if (hidden != IsHidden()) { + if (hidden) { + if( soulFx.IsValid() ) { + soulFx->Nozzle( false ); + } + if( physicalSoulFx.IsValid() ) { + physicalSoulFx->Nozzle( false ); + } + Hide(); + } + else { + Show(); + } + } +} + +void hhItemSoul::ClientPredictionThink( void ) { + Think(); +} + + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Pickup +// +//-------------------------------------------------------------------------- + +bool hhItemSoul::Pickup( idPlayer *player ) { + + CancelEvents( &EV_Remove ); // Remove the 60-second cancel event from when this item was spawned + CancelEvents(&EV_PlaySpiritSound); //rww - also cancel pending spirit sound events + + StopSound( SND_CHANNEL_SPIRITWALK, true ); + + bool result = hhItem::Pickup( player ); + if( soulFx.IsValid() ) { + soulFx->Nozzle( false ); + } + if( physicalSoulFx.IsValid() ) { + physicalSoulFx->Nozzle( false ); + } + + return result; +} + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Event_TalonAction +// +//-------------------------------------------------------------------------- + +void hhItemSoul::Event_TalonAction( idEntity *talon, bool landed ) { + idPlayer *player; + + if( !talon ) { + return; + } + + player = (idPlayer *)(((hhTalon *)talon)->GetOwner()); + Pickup( player ); +} + +//-------------------------------------------------------------------------- +// +// hhItemSoul::Event_Remove +// +//-------------------------------------------------------------------------- +void hhItemSoul::Event_Remove() { + // Now that spirit is gone, start the disposal countdown + if (parentMonster.IsValid()) { + parentMonster->StartDisposeCountdown(); + } + + hhItem::Event_Remove(); +} + +//================ +//hhItemSoul::Save +//================ +void hhItemSoul::Save( idSaveGame *savefile ) const { + soulFx.Save( savefile ); + physicalSoulFx.Save( savefile ); + parentMonster.Save( savefile ); + savefile->WriteVec3( velocity ); + savefile->WriteVec3( acceleration ); + savefile->WriteFloat( surge ); + savefile->WriteBool( bFollowTriggered ); + savefile->WriteVec3( lastPlayerOrigin ); +} + +//================ +//hhItemSoul::Restore +//================ +void hhItemSoul::Restore( idRestoreGame *savefile ) { + soulFx.Restore( savefile ); + physicalSoulFx.Restore( savefile ); + parentMonster.Restore( savefile ); + savefile->ReadVec3( velocity ); + savefile->ReadVec3( acceleration ); + savefile->ReadFloat( surge ); + savefile->ReadBool( bFollowTriggered ); + savefile->ReadVec3( lastPlayerOrigin ); +} + diff --git a/src/Prey/prey_items.h b/src/Prey/prey_items.h new file mode 100644 index 0000000..2c41b0c --- /dev/null +++ b/src/Prey/prey_items.h @@ -0,0 +1,79 @@ +#ifndef __PREY_ITEMS_H__ +#define __PREY_ITEMS_H__ + +// These defined in item.cpp +extern const idEventDef EV_RespawnItem; +extern const idEventDef EV_HideObjective; +extern const idEventDef EV_HideItem; + +class hhItem: public idItem { + CLASS_PROTOTYPE( hhItem ); + + public: + void Spawn(); + + virtual bool Pickup( idPlayer *player ); + + void EnablePickup(); + void DisablePickup(); + + protected: + bool ShouldRespawn( float* respawnDelay = NULL ) const; + void PostRespawn( float delay ); + void DetermineRemoveOrRespawn( int removeDelay ); + int AnnouncePickup( idPlayer* player ); + + void SinglePlayerPickup( idPlayer *player ); + + void CoopPickup( idPlayer* player ); + void CoopWeaponPickup( idPlayer *player ); + void CoopItemPickup( idPlayer *player ); + + bool MultiplayerPickup( idPlayer *player ); + + void SetPickupState( int contents, bool allowPickup ); + + protected: + void Event_SetPickupState( int contents, bool allowPickup ); + void Event_Respawn( void ); +}; + +class hhItemSoul : public hhItem { + CLASS_PROTOTYPE(hhItemSoul); + + public: + void Spawn(); + virtual ~hhItemSoul(); + + virtual void Think(); + + //rww - netcode + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + + virtual bool Pickup( idPlayer *player ); + void Event_TalonAction( idEntity *talon, bool landed ); + void Event_PostSpawn(); + void Event_PlaySpiritSound(); + void Event_AssignFxSoul_Spirit( hhEntityFx* fx ); + void Event_AssignFxSoul_Physical( hhEntityFx* fx ); + + virtual void Event_Remove(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + idEntityPtr soulFx; + idEntityPtr physicalSoulFx; + idEntityPtr parentMonster; + idVec3 velocity; + idVec3 acceleration; + float surge; + bool bFollowTriggered; + idVec3 lastPlayerOrigin; +}; + + +#endif /* __PREY_ITEMS_H__ */ diff --git a/src/Prey/prey_liquid.cpp b/src/Prey/prey_liquid.cpp new file mode 100644 index 0000000..8e55d4b --- /dev/null +++ b/src/Prey/prey_liquid.cpp @@ -0,0 +1,105 @@ +//************************************************************************** +//** +//** PREY_LIQUID.CPP +//** +//** Game code for Prey-specific dynamic liquid +//** +//** Currently using id's liquid code for the rendering +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +//========================================================================== +// +// hhLiquid +// +//========================================================================== +const idEventDef EV_Disturb("disturb", "vff"); + +CLASS_DECLARATION( hhRenderEntity, hhLiquid ) + EVENT( EV_Touch, hhLiquid::Event_Touch ) + EVENT( EV_Disturb, hhLiquid::Event_Disturb ) +END_CLASS + +void hhLiquid::Spawn(void) { + if (renderEntity.hModel) { + renderEntity.hModel->Reset(); + } + + fl.takedamage = true; + GetPhysics()->SetContents( CONTENTS_WATER | CONTENTS_TRIGGER | CONTENTS_RENDERMODEL ); + + factor_movement = spawnArgs.GetFloat("factor_movement"); + factor_collide = spawnArgs.GetFloat("factor_collide"); +} + +void hhLiquid::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( factor_movement ); + savefile->WriteFloat( factor_collide ); +} + +void hhLiquid::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( factor_movement ); + savefile->ReadFloat( factor_collide ); + + if (renderEntity.hModel) { + renderEntity.hModel->Reset(); + } +} + +void hhLiquid::Disturb( const idVec3 &point, const idBounds &bounds, const float magnitude ) { + if (renderEntity.hModel) { + idVec3 relativeToModel = point - GetPhysics()->GetOrigin(); + //FIXME: Take rotation of 'this' into account + +#if 0 + hhUtils::DebugCross(colorRed, point, 5, 1000); + gameLocal.Printf("Disturbance: Bounds={%.0f,%.0f,%.0f)(%.0f,%.0f,%.0f) mag=%f\n", + bounds[0].x, bounds[0].y, bounds[0].z, + bounds[1].x, bounds[1].y, bounds[1].z, + magnitude); +#endif + + // Pass in bounds in model coords + renderEntity.hModel->IntersectBounds( bounds.Translate(relativeToModel), magnitude ); + } +} + +void hhLiquid::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + idBounds bounds(idVec3(-1,-1,-1), idVec3(1,1,1)); + if (inflictor) { + bounds = inflictor->GetPhysics()->GetBounds(); + } + + const idDict *damageDef = gameLocal.FindEntityDefDict( damageDefName ); + if (damageDef) { + float disturbance = damageDef->GetFloat("liquid_disturbance"); + float boundsfactor = damageDef->GetFloat("liquid_bounds_expand"); + Disturb(inflictor->GetPhysics()->GetOrigin(), bounds.Expand(boundsfactor), disturbance); + } +} + +void hhLiquid::Event_Touch(idEntity *other, trace_t *trace) { + + idVec3 vel = other->GetPhysics()->GetLinearVelocity(); + idVec3 pos = other->GetPhysics()->GetOrigin(); + bool bMoving = vel.LengthSqr() > 1.0f; + if (bMoving) { + //TODO: Make scale with velocity + + // Since deforming completely to the bounds is a little too much, average the + // z position of the actor with the surface + pos.z = (pos.z + GetOrigin().z) * 0.5f; + Disturb(pos, other->GetPhysics()->GetBounds(), factor_movement); + } +} + +void hhLiquid::Event_Disturb(const idVec3 &position, float size, float magnitude) { + idBounds bounds(idVec3(-0.5f,-0.5f,-0.5f), idVec3(0.5f,0.5f,0.5f)); + Disturb(position, bounds.Expand(size), magnitude); +} diff --git a/src/Prey/prey_liquid.h b/src/Prey/prey_liquid.h new file mode 100644 index 0000000..051e5d2 --- /dev/null +++ b/src/Prey/prey_liquid.h @@ -0,0 +1,26 @@ + +#ifndef __PREY_SLUDGE_H__ +#define __PREY_SLUDGE_H__ + +class hhLiquid : public hhRenderEntity { +public: + CLASS_PROTOTYPE( hhLiquid ); + + void Spawn(void); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + + void Disturb( const idVec3 &point, const idBounds &bounds, const float magnitude ); + +protected: + virtual void Event_Touch(idEntity *other, trace_t *trace); + void Event_Disturb(const idVec3 &position, float size, float magnitude); + +protected: + float factor_movement; + float factor_collide; +}; + + +#endif /* __PREY_SLUDGE_H__ */ diff --git a/src/Prey/prey_local.cpp b/src/Prey/prey_local.cpp new file mode 100644 index 0000000..6ad9cae --- /dev/null +++ b/src/Prey/prey_local.cpp @@ -0,0 +1,3 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop +#include "prey_local.h" \ No newline at end of file diff --git a/src/Prey/prey_local.h b/src/Prey/prey_local.h new file mode 100644 index 0000000..c900bc8 --- /dev/null +++ b/src/Prey/prey_local.h @@ -0,0 +1,115 @@ +#ifndef __PREY_LOCAL_H +#define __PREY_LOCAL_H + +#include "../Game/game_local.h" +#include "prey_game.h" +#include "sys_preycmds.h" + +#include "force_converge.h" +#include "game_targetproxy.h" +#include "sys_debugger.h" +#include "game_console.h" +#include "game_spring.h" +#include "game_misc.h" +#include "game_moveable.h" +#include "game_barrel.h" +#include "physics_vehicle.h" +#include "game_vehicle.h" +#include "game_shuttle.h" +#include "game_railshuttle.h" +#include "game_dockedgun.h" +#include "game_player.h" +#include "prey_beam.h" +#include "game_trigger.h" +#include "game_tripwire.h" +#include "prey_bonecontroller.h" +#include "game_securityeye.h" +#include "game_cards.h" +#include "game_arcadegame.h" +#include "game_jukebox.h" +#include "game_blackjack.h" +#include "game_poker.h" +#include "game_zone.h" +#include "game_safeDeathVolume.h" +#include "game_energynode.h" +#include "prey_weaponrifle.h" +#include "prey_weaponspiritbow.h" +#include "prey_weaponautocannon.h" +#include "prey_weaponcrawlergrenade.h" +#include "prey_weaponrocketlauncher.h" +#include "prey_weaponsoulstripper.h" +#include "prey_weaponhider.h" +#include "game_jumpzone.h" +#include "game_note.h" +#include "game_portal.h" +#include "game_skybox.h" +#include "game_trackmover.h" +#include "game_sphere.h" +#include "prey_items.h" +#include "game_modeltoggle.h" +#include "game_animator.h" +#include "game_targets.h" +#include "game_slots.h" +#include "game_vomiter.h" +#include "game_modeldoor.h" +#include "game_door.h" +#include "game_cilia.h" +#include "game_animatedgui.h" +#include "game_gibbable.h" +//#include "game_pathNode.h" +#include "game_organtrigger.h" +#include "game_gravityswitch.h" + +#include "ai_passageway.h" +#include "ai_speech.h" +#include "ai_reaction.h" +#include "game_monster_ai.h" +#include "game_entityspawner.h" +#include "ai_centurion.h" +#include "ai_droid.h" +#include "ai_mutilatedhuman.h" +#include "ai_mutate.h" +#include "ai_hunter_simple.h" +#include "ai_creaturex.h" +#include "ai_harvester_simple.h" +#include "ai_jetpack_harvester_simple.h" +#include "ai_keeper_simple.h" +#include "ai_gasbag_simple.h" +#include "ai_spawncase.h" +#include "ai_inspector.h" +#include "ai_possessedTommy.h" +#include "ai_sphereboss.h" +#include "ai_crawler.h" + +#include "game_itemcabinet.h" +#include "game_itemautomatic.h" +#include "game_healthspore.h" +#include "game_healthbasin.h" +#include "game_portalframe.h" +#include "game_afs.h" +#include "game_lightfixture.h" +#include "game_mover.h" +#include "game_gun.h" +#include "game_wraith.h" +#include "game_deathwraith.h" +#include "game_talon.h" +#include "game_debrisspawner.h" +#include "game_bindController.h" +#include "game_rail.h" +#include "game_mountedgun.h" +#include "game_fixedpod.h" +#include "game_mine.h" +#include "game_pod.h" +#include "game_podspawner.h" +#include "game_shuttledock.h" +#include "game_shuttletransport.h" +#include "prey_spiritbridge.h" +#include "prey_spiritsecret.h" +#include "game_forcefield.h" +#include "prey_liquid.h" +#include "game_alarm.h" +#include "game_sunCorona.h" +#include "game_eggspawner.h" +#include "game_proxdoor.h" + +#endif diff --git a/src/Prey/prey_projectile.cpp b/src/Prey/prey_projectile.cpp new file mode 100644 index 0000000..de400c1 --- /dev/null +++ b/src/Prey/prey_projectile.cpp @@ -0,0 +1,1547 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_SpawnDriverLocal( "", "s" ); +const idEventDef EV_SpawnFxFlyLocal( "", "s" ); + +const idEventDef EV_Collision_Flesh( "", "tv", 'd' ); +const idEventDef EV_Collision_Metal( "", "tv", 'd' ); +const idEventDef EV_Collision_AltMetal( "", "tv", 'd' ); +const idEventDef EV_Collision_Wood( "", "tv", 'd' ); +const idEventDef EV_Collision_Stone( "", "tv", 'd' ); +const idEventDef EV_Collision_Glass( "", "tv", 'd' ); +const idEventDef EV_Collision_Liquid( "", "tv", 'd' ); +const idEventDef EV_Collision_Spirit( "", "tv", 'd' ); +const idEventDef EV_Collision_Remove( "", "tv", 'd' ); +const idEventDef EV_Collision_CardBoard( "", "tv", 'd' ); +const idEventDef EV_Collision_Tile( "", "tv", 'd' ); +const idEventDef EV_Collision_Forcefield( "", "tv", 'd' ); +const idEventDef EV_Collision_Chaff( "", "tv", 'd' ); +const idEventDef EV_Collision_Wallwalk( "", "tv", 'd' ); +const idEventDef EV_Collision_Pipe( "", "tv", 'd' ); + + +const idEventDef EV_AllowCollision_Flesh( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Metal( "", "t", 'd' ); +const idEventDef EV_AllowCollision_AltMetal( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Wood( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Stone( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Glass( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Liquid( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Spirit( "", "t", 'd' ); +const idEventDef EV_AllowCollision_CardBoard( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Tile( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Forcefield( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Chaff( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Wallwalk( "", "t", 'd' ); +const idEventDef EV_AllowCollision_Pipe( "", "t", 'd' ); + +hhMatterEventDefPartner matterEventsCollision( "collision" ); +hhMatterEventDefPartner matterEventsAllowCollision( "collision_allow" ); + +CLASS_DECLARATION( idProjectile, hhProjectile ) + EVENT( EV_Explode, hhProjectile::Event_Fuse_Explode ) + EVENT( EV_SpawnDriverLocal, hhProjectile::Event_SpawnDriverLocal ) + EVENT( EV_SpawnFxFlyLocal, hhProjectile::Event_SpawnFxFlyLocal ) + + EVENT( EV_Collision_Flesh, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Metal, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_AltMetal, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Wood, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Stone, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Glass, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Liquid, hhProjectile::Event_Collision_DisturbLiquid ) + EVENT( EV_Collision_CardBoard, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Tile, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Forcefield, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Pipe, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Wallwalk, hhProjectile::Event_Collision_Impact ) + EVENT( EV_Collision_Chaff, hhProjectile::Event_Collision_Impact ) + + EVENT( EV_Collision_Remove, hhProjectile::Event_Collision_Remove ) + + EVENT( EV_AllowCollision_Flesh, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Metal, hhProjectile::Event_AllowCollision_CollideNoProj ) + EVENT( EV_AllowCollision_AltMetal, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Wood, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Stone, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Glass, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Liquid, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_CardBoard, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Tile, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Forcefield, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Pipe, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Wallwalk, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Spirit, hhProjectile::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_Chaff, hhProjectile::Event_AllowCollision_Collide ) //bjk: shield blocks all +END_CLASS + + +/* +================ +hhProjectile::Spawn +================ +*/ +void hhProjectile::Spawn() { + bDDACounted = false; + parentProjectile = NULL; + launchTimestamp = 0; + + collidedPortal = NULL; // cjr + collideLocation = vec3_origin; // cjr + collideVelocity = vec3_origin; // cjr + + weaponNum = -1; // cjr - default the weapon index to -1, to denote non-player weapons + + netSyncPhysics = spawnArgs.GetBool( "net_fullphysics" ); //HUMANHEAD rww + bNoCollideWithCrawlers = spawnArgs.GetBool( "noCollideWithCrawlers", "0" ); + bProjCollide = spawnArgs.GetBool( "proj_collision", "0" ); + flyBySoundDistSq = spawnArgs.GetFloat( "flyby_dist", "0" ); + flyBySoundDistSq *= flyBySoundDistSq; + if ( flyBySoundDistSq > 0 ) { + bPlayFlyBySound = true; + } + BecomeActive( TH_TICKER ); +} + +/* +================ +hhProjectile::~hhProjectile +================ +*/ +hhProjectile::~hhProjectile() { + SAFE_REMOVE(fxFly); //HUMANHEAD rww +} + +void hhProjectile::SetParentProjectile( hhProjectile* in_parent ) { + parentProjectile.Assign( in_parent ); +} + +void hhProjectile::SetCollidedPortal( hhPortal *newPortal, idVec3 newLocation, idVec3 newVelocity ) { + collidedPortal.Assign( newPortal ); + collideLocation = newLocation; + collideVelocity = newVelocity; + BecomeActive(TH_MISC1); + + physicsObj.PutToRest(); +} + +hhProjectile* hhProjectile::GetParentProjectile( void ) { + if( parentProjectile.IsValid() ) { + return parentProjectile.GetEntity(); + } + return NULL; +} + + +/* +================ +hhProjectile::SetOrigin +================ +*/ +void hhProjectile::SetOrigin( const idVec3& origin ) { + idVec3 masterOrigin; + idMat3 masterAxis; + idVec3 localOrigin( origin ); + + if( driver.IsValid() && IsBoundTo(driver.GetEntity()) ) { + GetMasterPosition( masterOrigin, masterAxis ); + localOrigin = (localOrigin - masterOrigin) * masterAxis.Transpose(); + } + + idProjectile::SetOrigin( localOrigin ); +} + +/* +================ +hhProjectile::SetAxis +================ +*/ +void hhProjectile::SetAxis( const idMat3& axis ) { + idVec3 masterOrigin; + idMat3 masterAxis; + idMat3 localAxis( axis ); + + if( driver.IsValid() && IsBoundTo(driver.GetEntity()) ) { + GetMasterPosition( masterOrigin, masterAxis ); + localAxis *= masterAxis.Transpose(); + } + + idProjectile::SetAxis( localAxis ); +} + +/* +================ +hhProjectile::Think +================ +*/ +void hhProjectile::Think( void ) { + + // HUMANHEAD: cjr - if this projectile recently struck a portal, then attempt to portal it + if ( (thinkFlags & TH_MISC1) && collidedPortal.IsValid() ) { + GetPhysics()->SetLinearVelocity( collideVelocity ); + collidedPortal->PortalProjectile( this, collideLocation, collideLocation + collideVelocity ); + collidedPortal = NULL; + collideLocation = vec3_origin; + collideVelocity = vec3_origin; + BecomeInactive(TH_MISC1); + } + // HUMANHEAD END + + // run physics + RunPhysics(); + + if( thinkFlags & TH_THINK ) { + //HUMANHEAD: aob - added thrust_start check + if( thrust && (thrust_start <= gameLocal.GetTime() && gameLocal.GetTime() < thrust_end) ) { + // evaluate force + //HUMANHEAD rww - get rid of the thruster, needless projectile constructor overhead. + //thruster.SetForce( GetAxis()[ 0 ] * thrust ); + //thruster.Evaluate( gameLocal.GetTime() ); + //replaced the logic for the thing here. + idVec3 force = GetAxis()[ 0 ] * thrust; + idVec3 point = physicsObj.GetCenterOfMass(); + idVec3 p = GetPhysics()->GetOrigin() + point * physics->GetAxis(); + GetPhysics()->AddForce( 0, p, force ); + } + } + + //HUMANHEAD: aob + if( thinkFlags & TH_TICKER ) { + Ticker(); + } + //HUMANHEAD END + + Present(); + + if ( thinkFlags & TH_MISC2 ) { + UpdateLight(); + } + + //HUMANHEAD jsh flyby sounds + if( !gameLocal.isMultiplayer && bPlayFlyBySound && gameLocal.GetLocalPlayer() ) { + if ( !owner.IsEqualTo( gameLocal.GetLocalPlayer() ) ) { + if ( (GetOrigin() - gameLocal.GetLocalPlayer()->GetOrigin()).LengthSqr() < flyBySoundDistSq ) { + BroadcastFxPrefixedRandom( "fx_flyby", EV_SpawnFxFlyLocal ); + StartSound( "snd_flyby", SND_CHANNEL_BODY ); + bPlayFlyBySound = false; + } + } + } +} + +/* +===================== +hhProjectile::PlayImpactSound + +custom for projectiles. this needs to broadcast, unless we get predicted projectiles operational. +however predicted projectiles are not reliable since the client will not always collide them before +the server does, and so they will get removed with no fx. not a serious issue but somewhat bothersome +nonetheless. +===================== +*/ +int hhProjectile::PlayImpactSound( const idDict* dict, const idVec3 &origin, surfTypes_t type ) { + const char *snd = gameLocal.MatterTypeToMatterKey( "snd_impact", type ); + if( !snd || !snd[0] || !dict ) { + return -1; + } + + int length = 0; + StartSoundShader( declManager->FindSound(dict->GetString(snd), false), SND_CHANNEL_BODY, 0, true, &length ); + return length; +} + +/* +============== +hhProjectile::UpdateLight +============== +*/ +void hhProjectile::UpdateLight() { + // Attempt to remove light + if( lightStartTime != lightEndTime && gameLocal.GetTime() >= lightEndTime ) { + FreeLightDef(); + BecomeInactive(TH_MISC2); + } + + if( lightDefHandle != -1 ) { + UpdateLightPosition(); + UpdateLightFade(); + gameRenderWorld->UpdateLightDef( lightDefHandle, &renderLight ); + } +} + +/* +============== +hhProjectile::UpdateLightPosition +============== +*/ +void hhProjectile::UpdateLightPosition() { + renderLight.origin = GetOrigin() + GetAxis() * lightOffset; + renderLight.axis = GetAxis(); +} + +/* +============== +hhProjectile::UpdateLightFade +============== +*/ +void hhProjectile::UpdateLightFade() { + idVec3 color( vec3_zero ); + float frac = 0.0f; + + int time = gameLocal.GetTime(); + if( lightStartTime != lightEndTime && time < lightEndTime ) { + frac = MS2SEC(gameLocal.GetTime() - lightStartTime) / MS2SEC(lightEndTime - lightStartTime); + color.Lerp( lightColor, color, frac ); + + for( int ix = 0; ix < 3; ++ix ) { + renderLight.shaderParms[SHADERPARM_RED + ix] = color[ix]; + } + } +} + +/* +============== +hhProjectile::CreateLight +============== +*/ +int hhProjectile::CreateLight( const char* shaderName, const idVec3& size, const idVec3& color, const idVec3& offset, float fadeTime ) { + int fadeDuration = 0; + + if ( size.x <= 0.0f || !g_projectileLights.GetBool() ) { + return 0; + } + + if( size.Compare(vec3_zero, VECTOR_EPSILON) ) { + return 0; + } + + SIMDProcessor->Memset( &renderLight, 0, sizeof(renderLight) ); + FreeLightDef(); + + if( !shaderName || !shaderName[0] ) { + return 0; + } + + UpdateLightPosition(); + + renderLight.shader = declManager->FindMaterial( shaderName, false ); + renderLight.pointLight = true; + renderLight.lightRadius = size; + renderLight.shaderParms[SHADERPARM_RED] = color[0]; + renderLight.shaderParms[SHADERPARM_GREEN] = color[1]; + renderLight.shaderParms[SHADERPARM_BLUE] = color[2]; + renderLight.shaderParms[SHADERPARM_ALPHA] = 1.0f; + renderLight.shaderParms[SHADERPARM_TIMEOFFSET] = -MS2SEC( gameLocal.GetTime() ); + renderLight.noShadows = true; //HUMANHEAD bjk: cheaper + + fadeDuration = SEC2MS( fadeTime ); + + lightOffset = offset; + lightColor = color; + lightStartTime = gameLocal.GetTime(); + lightEndTime = lightStartTime + fadeDuration; + + BecomeActive(TH_MISC2); + + if( lightDefHandle != -1 ) { + gameRenderWorld->UpdateLightDef( lightDefHandle, &renderLight ); + } else { + lightDefHandle = gameRenderWorld->AddLightDef( &renderLight ); + } + + return fadeDuration; +} + +/* +============== +hhProjectile::BounceSplat +============== +*/ +void hhProjectile::BounceSplat( const idVec3& origin, const idVec3& dir ) { + float size = hhMath::Lerp( spawnArgs.GetVec2("decal_bounce_size", "1.8 2.2"), gameLocal.random.RandomFloat() ); + const char* decal = spawnArgs.RandomPrefix( "mtr_bounce_shader", gameLocal.random ); + if( decal && *decal ) { + gameLocal.ProjectDecal( origin, dir, 16.0f, true, size, decal ); + } +} + + +/* +================ +hhProjectile::Create + +HUMANHEAD: AOBMERGE - Overridden +================ +*/ +void hhProjectile::Create( idEntity *owner, const idVec3 &start, const idVec3 &dir ) { + Create( owner, start, dir.ToMat3() ); +} + +/* +================ +hhProjectile::Launch + +HUMANHEAD: AOBMERGE - Overridden +================ +*/ +void hhProjectile::Launch( const idVec3 &start, const idVec3 &dir, const idVec3 &pushVelocity, const float timeSinceFire, const float launchPower, const float dmgPower ) { + Launch( start, dir.ToMat3(), pushVelocity, timeSinceFire, launchPower, dmgPower ); + + launchTimestamp = gameLocal.time; //HUMANHEAD: mdc - record launch time +} + +/* +================ +hhProjectile::Create + +HUMANHEAD: AOBMERGE - Overridden +================ +*/ +void hhProjectile::Create( idEntity *owner, const idVec3 &start, const idMat3 &axis ) { + Unbind(); + + SetOrigin( start ); + SetAxis( axis ); + + this->owner = owner; + + CreateLight( spawnArgs.GetString("mtr_light_shader"), spawnArgs.GetVector("light_size"), spawnArgs.GetVector("light_color", "1 1 1") * spawnArgs.GetFloat("light_intensity", "1.0"), spawnArgs.GetVector("light_offset"), spawnArgs.GetFloat("light_fadetime") ); + + GetPhysics()->SetContents( 0 ); + + state = CREATED; +} + +/* +================= +hhProjectile::Launch + +HUMANHEAD: aob - made second parameter idMat3& +================= +*/ +void hhProjectile::Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire, const float launchPower, const float dmgPower ) { + float fuse; + idVec3 velocity; + idAngles angular_velocity; + float linear_friction; + float angular_friction; + float contact_friction; + float bounce; + float mass; + float gravity; + idVec3 gravVec; + + spawnArgs.GetVector( "velocity", "0 0 0", velocity ); + spawnArgs.GetAngles( "angular_velocity", "0 0 0", angular_velocity ); + + linear_friction = spawnArgs.GetFloat( "linear_friction" ); + angular_friction = spawnArgs.GetFloat( "angular_friction" ); + contact_friction = spawnArgs.GetFloat( "contact_friction" ); + bounce = spawnArgs.GetFloat( "bounce" ); + mass = spawnArgs.GetFloat( "mass" ); + gravity = spawnArgs.GetFloat( "gravity" ); + fuse = spawnArgs.GetFloat( "fuse" ); + + projectileFlags.detonate_on_world = spawnArgs.GetBool( "detonate_on_world" ); + projectileFlags.detonate_on_actor = spawnArgs.GetBool( "detonate_on_actor" ); + projectileFlags.randomShaderSpin = spawnArgs.GetBool( "random_shader_spin" ); + projectileFlags.isLarge = spawnArgs.GetBool( "largeProjectile" ); // HUMANHEAD bjk + + if ( mass <= 0 ) { + gameLocal.Error( "Invalid mass on '%s'\n", GetClassname() ); + } + + thrust = mass * spawnArgs.GetFloat( "thrust" ); + thrust_start = SEC2MS( spawnArgs.GetFloat("thrust_start") ) + gameLocal.GetTime(); + thrust_end = SEC2MS( spawnArgs.GetFloat("thrust_duration") ) + thrust_start; + //HUMANHEAD: aob - if thrust is set then set TH_THINK + if( hhMath::Fabs(thrust) >= VECTOR_EPSILON ) { + BecomeActive( TH_THINK ); + } + //HUMANHEAD END + + if ( health ) { + fl.takedamage = true; + } + + gravVec = gameLocal.GetGravity(); + gravVec.NormalizeFast(); + + Unbind(); + + int contents = DetermineContents(); + int clipMask = DetermineClipmask(); + + //HUMANHEAD rww + launchQuat = axis.ToCQuat(); //save off launch orientation + launchPos = start; //save off launch pos + //HUMANHEAD END + + physicsObj.SetSelf( this ); + physicsObj.SetClipModel( new idClipModel( GetPhysics()->GetClipModel() ), 1.0f ); + //HUMANHEAD: aob - added DetermineClipModelOwner so some projectiles can decide to collide with there owners + physicsObj.GetClipModel()->SetOwner( DetermineClipModelOwner() ); + //HUMANHEAD END + physicsObj.SetMass( mass ); + physicsObj.SetFriction( linear_friction, angular_friction, contact_friction ); + if ( contact_friction == 0.0f ) { + physicsObj.NoContact(); + } + physicsObj.SetBouncyness( bounce ); + physicsObj.SetGravity( gravVec * gravity ); + physicsObj.SetContents( contents ); + physicsObj.SetClipMask( clipMask ); + physicsObj.SetLinearVelocity( axis[ 0 ] * velocity[ 0 ] + axis[ 1 ] * velocity[ 1 ] + axis[ 2 ] * velocity[ 2 ] + pushVelocity ); + /* + if (gameLocal.isClient) { + gameLocal.Printf("C: (%f %f %f) (%f %f %f) (%f %f %f, %f %f %f, %f %f %f)\n", velocity[0], velocity[1], velocity[2], + pushVelocity[0], pushVelocity[1], pushVelocity[2], axis[0][0], axis[0][1], axis[0][2], axis[1][0], axis[1][1], + axis[1][2], axis[2][0], axis[2][1], axis[2][2]); + } + else { + gameLocal.Printf("S: (%f %f %f) (%f %f %f) (%f %f %f, %f %f %f, %f %f %f)\n", velocity[0], velocity[1], velocity[2], + pushVelocity[0], pushVelocity[1], pushVelocity[2], axis[0][0], axis[0][1], axis[0][2], axis[1][0], axis[1][1], + axis[1][2], axis[2][0], axis[2][1], axis[2][2]); + } + */ + + physicsObj.SetAngularVelocity( angular_velocity.ToAngularVelocity() * axis ); + physicsObj.SetOrigin( start ); + physicsObj.SetAxis( axis ); + SetPhysics( &physicsObj ); + + //HUMANHEAD rww - get rid of the thruster, needless projectile constructor overhead + //thruster.SetPosition( &physicsObj, 0, physicsObj.GetCenterOfMass() );//idVec3( GetPhysics()->GetBounds()[ 0 ].x, 0, 0 ) ); + + //HUMANHEAD rww - debug projectile position and axis (for client side projectiles) + /* + extern void Debug_ClearDebugLines(void); + extern void Debug_AddDebugLine(idVec3 &start, idVec3 &end, int color); + + idVec3 p = start; + idVec3 prj; + Debug_ClearDebugLines(); + prj = p + (axis[0]*32.0f); + Debug_AddDebugLine(p, prj, 1); + prj = p + (axis[1]*32.0f); + Debug_AddDebugLine(p, prj, 2); + prj = p + (axis[2]*32.0f); + Debug_AddDebugLine(p, prj, 3); + */ + + if ( !gameLocal.isClient || fl.clientEntity ) //HUMANHEAD rww - if clientEntity + { + if ( fuse <= 0 ) { + // run physics for 1 second + RunPhysics(); + PostEventMS( &EV_Remove, spawnArgs.GetInt( "remove_time", "1500" ) ); + } else if ( spawnArgs.GetBool( "detonate_on_fuse" ) ) { + fuse -= timeSinceFire; + if ( fuse < 0.0f ) { + fuse = 0.0f; + } + PostEventSec( &EV_Explode, fuse ); + } else { + fuse -= timeSinceFire; + if ( fuse < 0.0f ) { + fuse = 0.0f; + } + PostEventSec( &EV_Fizzle, fuse ); + } + } + + StartSound( "snd_fly", SND_CHANNEL_BODY, 0, true ); + + //HUMANHEAD: aob - replaces id's smoke_fly code + // CJR: Changed so that we can randomly choose between multiple fx_fly systems on a single projectile + BroadcastFxPrefixedRandom( "fx_fly", EV_SpawnFxFlyLocal ); + //HUMANHEAD END + + // used for the plasma bolts but may have other uses as well + if ( projectileFlags.randomShaderSpin ) { + float f = gameLocal.random.RandomFloat(); + f *= 0.5f; + renderEntity.shaderParms[SHADERPARM_DIVERSITY] = f; + } + + // HUMANHEAD CJR: if launched by a player, set the projectile's weapon index appropriately + if ( owner.IsValid() && owner->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast( owner.GetEntity() ); + weaponNum = player->GetCurrentWeapon(); + } else { // Otherwise, default the weaponNum to denote a non-player weapon + weaponNum = -1; + }// HUMANHEAD END + + state = LAUNCHED; + + if (gameLocal.isClient) { //HUMANHEAD rww + launchTimestamp = gameLocal.time; //HUMANHEAD: mdc - record launch time + return; + } + + // Notify the AI about this launch + gameLocal.SendMessageAI( this, GetOrigin(), spawnArgs.GetFloat("ai_notify_launch", "0"), MA_OnProjectileLaunch ); + BroadcastEntityDef( spawnArgs.GetString("def_driver"), EV_SpawnDriverLocal ); + + launchTimestamp = gameLocal.time; //HUMANHEAD: mdc - record launch time +} + +/* +================ +hhProjectile::DetermineClipModelOwner + +HUMANHEAD: aob +================ +*/ +idEntity* hhProjectile::DetermineClipModelOwner() { + return (spawnArgs.GetBool("collideWithOwner")) ? this : owner.GetEntity(); +} + +/* +================ +hhProjectile::Collide +================ +*/ +extern const int RB_VELOCITY_EXPONENT_BITS; //HUMANHEAD rww +extern const int RB_VELOCITY_MANTISSA_BITS; //HUMANHEAD rww +bool hhProjectile::Collide( const trace_t& collision, const idVec3& velocity ) { + if ( state == EXPLODED || state == FIZZLED || state == COLLIDED ) { //HUMANHEAD bjk + return false; + } + + // HUMANHEAD CJR: Don't allow the collision if the projectile has recently hit a portal + if ( collidedPortal.IsValid() ) { + return false; + } // HUMANHEAD END + + if ( gameLocal.isClient ) { + //HUMANHEAD rww - our projectile stuff is pretty different from id's at this point, so i'm just making some new prediction code. + if (state == EXPLODED || state == FIZZLED || state == COLLIDED) { //HUMANHEAD bjk + ProcessCollision(&collision, velocity); + return false; + } + //HUMANHEAD END + } + + //HUMANHEAD rww - commented out to allow projectiles to collide with owner after portalling + /* + if (gameLocal.isClient) { //HUMANHEAD rww + idEntity* entHit = gameLocal.GetTraceEntity(collision); + if ( entHit == owner.GetEntity() ) { + return true; + } + } + else { + // get the entity the projectile collided with + idEntity* entHit = gameLocal.GetTraceEntity(collision); + if ( entHit == owner.GetEntity() ) { + assert( 0 ); + return false; + } + } + */ + + // remove projectile when a 'noimpact' surface is hit + if ( collision.c.material && ( collision.c.material->GetSurfaceFlags() & SURF_NOIMPACT ) ) { + common->DPrintf("removing projectile that hit noimpact surface: mat=[%s]\n", collision.c.material->GetName() ); + RemoveProjectile( 0 ); + return false; + } + + //HUMANHEAD rww - send events for collisions on the server for pseudo-sync'd projectiles + if (gameLocal.isServer && fl.networkSync && !netSyncPhysics) { + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + msg.Init( msgBuf, sizeof( msgBuf ) ); + msg.BeginWriting(); + + msg.WriteBits(collision.c.contents, 32); + msg.WriteFloat(collision.c.dist); + msg.WriteBits(collision.c.entityNum, GENTITYNUM_BITS); + msg.WriteShort(collision.c.id); + if (!collision.c.material) { + msg.WriteShort(-1); + } + else { + msg.WriteShort(collision.c.material->Index()); + } + msg.WriteBits(collision.c.modelFeature, 32); + //ensure it is normalized properly first + idVec3 normal = collision.c.normal; + normal.Normalize(); + msg.WriteDir(normal, 24); + msg.WriteFloat(collision.c.point.x); + msg.WriteFloat(collision.c.point.y); + msg.WriteFloat(collision.c.point.z); + msg.WriteBits(collision.c.trmFeature, 32); + msg.WriteBits(collision.c.type, 4); + msg.WriteFloat(collision.fraction); + + //unfortunately, this is needed for proper decal projections + msg.WriteFloat(velocity.x, RB_VELOCITY_EXPONENT_BITS, RB_VELOCITY_MANTISSA_BITS); + msg.WriteFloat(velocity.y, RB_VELOCITY_EXPONENT_BITS, RB_VELOCITY_MANTISSA_BITS); + msg.WriteFloat(velocity.z, RB_VELOCITY_EXPONENT_BITS, RB_VELOCITY_MANTISSA_BITS); + + msg.WriteDir(collision.endAxis[0], 24); + + ServerSendPVSEvent(EVENT_PROJECTILE_EXPLOSION, &msg, collision.endpos); + } + //HUMANHEAD END + + //HUMANHEAD rww - our projectile stuff is pretty different from id's at this point, so i'm just making some new prediction code. + if (gameLocal.isClient) { + //ProcessCollision(&collision, velocity); + if (fl.networkSync && !netSyncPhysics) { //if this projectile is psuedo-sync'd, we will be expecting impact results from the server. + ClientHideProjectile(); + return 0; + } + return ProcessCollisionEvent( &collision, velocity ); + } + //HUMANHEAD END + + if ( !gameLocal.isMultiplayer && spawnArgs.GetString( "hit_notify" ) && owner.IsValid() && owner->IsType( hhMonsterAI::Type ) ) { + gameLocal.SendMessageAI( this, GetOrigin(), spawnArgs.GetFloat("ai_notify_yes", "0"), MA_OnProjectileHit ); + } + + return ProcessCollisionEvent( &collision, velocity ); +} + +/* +================ +hhProjectile::ProcessCollisionEvent +================ +*/ +bool hhProjectile::ProcessCollisionEvent( const trace_t* collision, const idVec3& velocity ) { + assert( collision ); + + idEntity* ent = gameLocal.entities[ collision->c.entityNum ]; + const idEventDef* eventDef = matterEventsCollision.GetPartner( ent, collision->c.material ); + assert( eventDef ); + + ProcessEvent( eventDef, collision, velocity ); + return gameLocal.program.GetReturnedBool(); +} + +/* +================ +hhProjectile::ProcessCollision + +HUMANHEAD: aob +================ +*/ +int hhProjectile::ProcessCollision( const trace_t* collision, const idVec3& velocity ) { + PROFILE_SCOPE("ProjectileCollision", PROFMASK_PHYSICS|PROFMASK_COMBAT); + idEntity* entHit = gameLocal.entities[ collision->c.entityNum ]; + + SetOrigin( collision->endpos ); + SetAxis( collision->endAxis ); + + if( fxFly.IsValid() ) { + fxFly->Stop(); + } + SAFE_REMOVE( fxFly ); + FreeLightDef(); + CancelEvents( &EV_Fizzle ); + + if (entHit) { //rww - may be null on client. + if (!gameLocal.isClient || (fl.networkSync && !netSyncPhysics)) { //don't do this on the client, unless this is a sync'd projectile + DamageEntityHit( collision, velocity, entHit ); + } + } + + fl.takedamage = false; + physicsObj.SetContents( 0 ); + physicsObj.PutToRest(); + + surfTypes_t matterType = gameLocal.GetMatterType( entHit, collision->c.material, "hhProjectile::ProcessCollision" ); + return PlayImpactSound( gameLocal.FindEntityDefDict(spawnArgs.GetString("def_damage")), collision->endpos, matterType ); +} + +/* +================ +hhProjectile::DamageEntityHit + +HUMANHEAD: aob +================ +*/ +void hhProjectile::DamageEntityHit( const trace_t* collision, const idVec3& velocity, idEntity* entHit ) { + PROFILE_SCOPE("DamageEntityHit", PROFMASK_COMBAT); + if (GERMAN_VERSION || g_nogore.GetBool()) { + if (entHit->IsType(idActor::Type)) { + idActor *actor = reinterpret_cast (entHit); + if ( !actor->fl.takedamage || ( actor->IsActiveAF() && !actor->spawnArgs.GetBool( "not_gory", "0" ) ) ) { + // Don't process hits on ragdolls + return; + } + } + } + + float push = 0.0f; + float damageScale = 1.0f; + const char *damage = NULL; + if (gameLocal.isMultiplayer) { //rww - check for special mp damage def + damage = spawnArgs.GetString( "def_damage_mp" ); + } + if (!damage || !damage[0]) { + damage = spawnArgs.GetString( "def_damage" ); + } + hhPlayer* playerHit = (entHit->IsType(hhPlayer::Type)) ? static_cast(entHit) : NULL; + idAFEntity_Base* afHit = (entHit->IsType(idAFEntity_Base::Type)) ? static_cast(entHit) : NULL; + + idVec3 dir = velocity.ToNormal(); + + // non-radius damage defs can also apply an additional impulse to the rigid body physics impulse + const idDeclEntityDef *def = gameLocal.FindEntityDef( damage, false ); + if ( def ) { + if (entHit->IsType(hhProjectile::Type)) { + push = 0.0f; // mdl: Don't let projectiles push each other + } else if (afHit && afHit->IsActiveAF() ) { + push = def->dict.GetFloat( "push_ragdoll" ); + } else { + push = def->dict.GetFloat( "push" ); + } + } + + if (!gameLocal.isClient) { //rww + if( playerHit ) { + // pdm: save collision location in case we want to project a blob there + playerHit->playerView.SetDamageLoc( collision->endpos ); + } + + if( DamageIsValid(collision, damageScale) && entHit->fl.takedamage ) { + UpdateBalanceInfo( collision, entHit ); + + if( damage && damage[0] ) { + idEntity *killer = owner.GetEntity(); + if (killer && killer->IsType(hhVehicle::Type)) { //rww - handle vehicle projectiles killing people + hhVehicle *veh = static_cast(killer); + if (veh->GetPilot()) { + killer = veh->GetPilot(); + } + } + + entHit->Damage( this, killer, dir, damage, damageScale, CLIPMODEL_ID_TO_JOINT_HANDLE(collision->c.id) ); + + if ( playerHit && def->dict.GetInt( "freeze_duration" ) > 0 ) { + playerHit->Freeze( def->dict.GetInt( "freeze_duration" ) ); + } + } + } + + // HUMANHEAD bjk: moved to after damage so impulse can be applied to ragdoll + if ( push > 0.0f ) { + if (g_debugImpulse.GetBool()) { + gameRenderWorld->DebugArrow(colorYellow, collision->c.point, collision->c.point + (push*dir), 25, 2000); + } + + entHit->ApplyImpulse( this, collision->c.id, collision->c.point, push * dir ); + } + } + + if ( entHit->fl.applyDamageEffects ) { + ApplyDamageEffect( entHit, collision, velocity, damage ); + } +} + +/* +================ +hhProjectile::DamageIsValid + +HUMANHEAD: aob +================ +*/ +bool hhProjectile::DamageIsValid( const trace_t* collision, float& damageScale ) { + damageScale = DetermineDamageScale( collision ); + + return true; +} + +/* +================ +hhProjectile::ApplyDamageEffect + +HUMANHEAD: aob +================ +*/ +void hhProjectile::ApplyDamageEffect( idEntity* hitEnt, const trace_t* collision, const idVec3& velocity, const char* damageDefName ) { + if( hitEnt ) { + hitEnt->AddDamageEffect( *collision, velocity, damageDefName, (!fl.networkSync || netSyncPhysics) ); + } +} + +int hhProjectile::DetermineContents() { + return (spawnArgs.GetBool("proj_collision")) ? CONTENTS_PROJECTILE | CONTENTS_SHOOTABLE : CONTENTS_PROJECTILE; +} + +int hhProjectile::DetermineClipmask() { + return MASK_SHOT_RENDERMODEL; +} + +/* +================ +hhProjectile::UpdateBalanceInfo + +HUMANHEAD: aob +================ +*/ +void hhProjectile::UpdateBalanceInfo( const trace_t* collision, const idEntity* hitEnt ) { + hhPlayer* player = (owner.IsValid() && owner->IsType(hhPlayer::Type)) ? static_cast(owner.GetEntity()) : NULL; + + if( hitEnt->IsType(idActor::Type) && player ) { + player->SetLastHitTime( gameLocal.GetTime() ); + player->AddProjectileHits( 1 ); + } +} + +/* +================ +hhProjectile::SpawnProjectile + +HUMANHEAD: aob +================ +*/ +hhProjectile* hhProjectile::SpawnProjectile( const idDict* args ) {//FIXME: Broadcast + assert( args ); + assert(!gameLocal.isClient); + + idEntity* ent = NULL; + + gameLocal.SpawnEntityDef( *args, &ent ); + HH_ASSERT( ent && ent->IsType(hhProjectile::Type) ); + + return static_cast(ent); +} + +/* +================ +hhProjectile::SpawnClientProjectile + +HUMANHEAD: rww +================ +*/ +hhProjectile* hhProjectile::SpawnClientProjectile( const idDict* args ) {//FIXME: Broadcast + assert( args ); + + idEntity* ent = NULL; + + gameLocal.SpawnEntityDef( *args, &ent, true, gameLocal.isClient ); + HH_ASSERT( ent && ent->IsType(hhProjectile::Type) ); + + ent->fl.networkSync = false; + ent->fl.clientEvents = true; + + return static_cast(ent); +} + +/* +================ +hhProjectile::Killed +================ +*/ +void hhProjectile::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + //HUMANHEAD: aob - added collision so we can get explosion to show up when killed + trace_t collision; + + if ( spawnArgs.GetBool( "detonate_on_death" ) ) { + memset( &collision, 0, sizeof( trace_t ) ); + collision.fraction = 0.0f; + collision.endpos = GetOrigin(); + collision.endAxis = GetAxis(); + collision.c.entityNum = attacker->entityNumber; + collision.c.normal = inflictor->GetOrigin() - GetOrigin(); + collision.c.normal.Normalize(); + Explode( &collision, GetPhysics()->GetLinearVelocity(), 0 ); + } else { + Fizzle(); + } +} + +/* +================ +hhProjectile::Fizzle +================ +*/ +void hhProjectile::Fizzle( void ) { + if ( state == EXPLODED || state == FIZZLED || state == COLLIDED ) { //HUMANHEAD bjk + return; + } + + int removeTime = StartSound( "snd_fizzle", SND_CHANNEL_BODY, 0, true ); + + if (!gameLocal.isClient) + { + // fizzle FX + hhFxInfo fxInfo; + fxInfo.SetNormal( -GetAxis()[0] ); + fxInfo.RemoveWhenDone( true ); + BroadcastFxInfoPrefixed( "fx_fuse", GetOrigin(), GetAxis(), &fxInfo ); + } + + SAFE_REMOVE( fxFly ); + + fl.takedamage = false; + physicsObj.SetContents( 0 ); + physicsObj.GetClipModel()->Unlink(); + physicsObj.PutToRest(); + + FreeLightDef(); + + state = FIZZLED; + + RemoveProjectile( removeTime ); +} + +/* +================= +hhProjectile::RemoveProjectile +================= +*/ +void hhProjectile::RemoveProjectile( const int removeDelay ) { + Hide(); + BecomeInactive( TH_TICKER|TH_THINK ); + RemoveBinds();//Remove any fx we have because they aren't hidden + PostEventMS( &EV_Remove, removeDelay ); +} + +/* +================ +hhProjectile::Explode +================ +*/ +void hhProjectile::Explode( const trace_t* collision, const idVec3& velocity, int removeDelay ) { + const char *fxname = NULL; + int length = 0; + + if ( state == EXPLODED || state == FIZZLED || state == COLLIDED ) { //HUMANHEAD bjk + return; + } + + if( !collision ) { + return; + } + + //HUMANHEAD: aob + StartSound( "snd_explode", SND_CHANNEL_BODY, 0, true, &length ); + removeDelay = hhMath::hhMax( length, removeDelay ); + + // explosion light + //FIXME: may need to be Broadcast + removeDelay = hhMath::hhMax( removeDelay, CreateLight(spawnArgs.GetString("mtr_explode_light_shader"), spawnArgs.GetVector("explode_light_size"), spawnArgs.GetVector("explode_light_color", "1 1 1") * spawnArgs.GetFloat("explode_light_intensity", "1.0"), spawnArgs.GetVector("explode_light_offset"), spawnArgs.GetFloat("explode_light_fadetime")) ); + + //HUMANHEAD: aob - moved logic to helper function + SpawnExplosionFx( collision ); + + SpawnDebris( collision->c.normal, velocity.ToNormal() ); + //HUMANHEAD END + + state = EXPLODED; + + //HUMANHEAD rww + if (!gameLocal.isClient) { + idEntity *killer = owner.GetEntity(); + if (killer && killer->IsType(hhVehicle::Type)) { //rww - handle vehicle projectiles killing people + hhVehicle *veh = static_cast(killer); + if (veh->GetPilot()) { + killer = veh->GetPilot(); + } + } + + // splash damage + if (!gameLocal.isClient) { + //SplashDamage( collision->endpos, killer, this, this, spawnArgs.GetString("def_splash_damage") ); + SplashDamage( GetOrigin(), killer, this, this, spawnArgs.GetString("def_splash_damage") ); + } + } + //HUMANHEAD END + + //HUMANHEAD: aob - moved logic to helper function + RemoveProjectile( removeDelay ); + //HUMANHEAD END +} + +/* +================ +hhProjectile::SplashDamage + +HUMANHEAD: aob +================ +*/ +void hhProjectile::SplashDamage( const idVec3& origin, idEntity* attacker, idEntity* ignoreDamage, idEntity* ignorePush, const char* splashDamageDefName ) { + if( splashDamageDefName && splashDamageDefName[0] ) { + gameLocal.RadiusDamage( origin, this, attacker, ignoreDamage, ignorePush, splashDamageDefName ); + } +} + +/* +================ +hhProjectile::SpawnExplosionFx + +HUMANHEAD: aob +================ +*/ +void hhProjectile::SpawnExplosionFx( const trace_t* collision ) { + idEntity* hitEnt = NULL; + hhFxInfo fxInfo; + const idDict* dict = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_detonateFx"), false ); + + if( !dict || !collision || collision->fraction >= 1.0f ) { + return; + } + + hitEnt = gameLocal.entities[ collision->c.entityNum ]; + + fxInfo.SetNormal( collision->c.normal ); + fxInfo.SetIncomingVector( GetAxis()[0] ); + fxInfo.SetBounceVector( hhProjectile::GetBounceDirection( physicsObj.GetLinearVelocity(), + collision->c.normal, + this, hitEnt + ) ); + fxInfo.RemoveWhenDone( true ); + surfTypes_t matterType = gameLocal.GetMatterType( hitEnt, collision->c.material, "hhProjectile::SpawnExplosionFx" ); + const char* fxKey = gameLocal.MatterTypeToMatterKey( "fx", matterType ); + BroadcastFxInfo( dict->RandomPrefix(fxKey, gameLocal.random), GetOrigin(), GetAxis(), &fxInfo, NULL, false ); //rww - no broadcast, only locally. + //FIXME: Once BroadcastFxInfo() is gone and we are spawning the entityfx directly, mark it as fl.neverdormant here +} + +/* +================ +hhProjectile::SpawnDebris + +HUMANHEAD: aob +================ +*/ +void hhProjectile::SpawnDebris( const idVec3& collisionNormal, const idVec3& collisionDir ) { + static char debrisKey[] = "def_debris"; + static char countKey[] = "debris_count"; + static char spreadKey[] = "debris_spread"; + + idDebris *debris = NULL; + idEntity *ent = NULL; + idStr indexStr; + int amount = 0; + const idDict *dict = NULL; + const idKeyValue* defKV = NULL; + for( defKV = spawnArgs.MatchPrefix(debrisKey, NULL); defKV; defKV = spawnArgs.MatchPrefix(debrisKey, defKV) ) { + + if( !defKV->GetValue().Length() ) { + continue; + } + + dict = gameLocal.FindEntityDefDict( defKV->GetValue().c_str(), false ); + if( !dict ) { + continue; + } + + indexStr = defKV->GetKey(); + indexStr.Strip( debrisKey ); + + if (gameLocal.isMultiplayer) { //rww - for decreasing the count for things in mp on a design basis + amount = hhMath::Lerp( spawnArgs.GetVec2(va("%s_mp%s", countKey, indexStr.c_str())), gameLocal.random.RandomFloat() ); + if (!amount) { + amount = hhMath::Lerp( spawnArgs.GetVec2(va("%s%s", countKey, indexStr.c_str())), gameLocal.random.RandomFloat() ); + } + } + else { + amount = hhMath::Lerp( spawnArgs.GetVec2(va("%s%s", countKey, indexStr.c_str())), gameLocal.random.RandomFloat() ); + } + for ( int i = 0; i < amount; i++ ) { + //HUMANHEAD: aob + idVec3 dir = hhUtils::RandomSpreadDir( collisionNormal.ToMat3(), DEG2RAD(spawnArgs.GetFloat(va("%s%s", spreadKey, indexStr.c_str()))) ); + //HUMAMHEAD END + + gameLocal.SpawnEntityDef( *dict, &ent, true, gameLocal.isClient ); //HUMANHEAD rww - make them local non-broadcast entities. + if ( !ent || !ent->IsType( idDebris::Type ) ) { + gameLocal.Error( "hhProjectile: 'projectile_debris' is not an idDebris" ); + } + + debris = static_cast(ent); + debris->Create( owner.GetEntity(), GetOrigin() + collisionNormal*10, dir.ToMat3() ); // HUMANHEAD bjk: displace out of surface + debris->Launch(); + debris->fl.networkSync = false; //HUMANHEAD rww + debris->fl.clientEvents = true; //HUMANHEAD rww + } + } +} + +/* +================ +hhProjectile::SetGravity +================ +*/ +void hhProjectile::SetGravity( const idVec3 &newGravity ) { + float relativeMagnitude = spawnArgs.GetFloat( "gravity" ); + idVec3 newGravityVector( vec3_zero ); + + if( GetGravity().Compare(newGravity, VECTOR_EPSILON) ) { + return; + } + + if( relativeMagnitude > 0.0f ) { + newGravityVector = newGravity; + relativeMagnitude *= newGravityVector.Normalize() / gameLocal.GetGravity().Length(); + newGravityVector *= relativeMagnitude; + } + + GetPhysics()->SetGravity( newGravityVector ); +} + +/* +================ +hhProjectile::ProcessAllowCollisionEvent +================ +*/ +bool hhProjectile::ProcessAllowCollisionEvent( const trace_t* collision ) { + assert( collision ); + + idEntity* ent = gameLocal.entities[ collision->c.entityNum ]; + const idEventDef* eventDef = matterEventsAllowCollision.GetPartner( ent, collision->c.material ); + assert( eventDef ); + + ProcessEvent( eventDef, collision ); + return gameLocal.program.GetReturnedBool(); +} + +//============================================================================= +// +// hhProjectile::Portalled +// +// The projectile was just portalled. Update the fx info to the bound fly fx system +// HUMANHEAD CJR +//============================================================================= + +void hhProjectile::Portalled(idEntity *portal) { + if ( fxFly.IsValid() ) { + hhFxInfo fxInfo; + fxInfo.SetNormal( -GetAxis()[0] ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( false ); + + fxFly->SetFxInfo( fxInfo ); + + // Reset the fx system + fxFly->Stop(); + fxFly->Start( gameLocal.time ); + } + if (physicsObj.GetClipModel()) { //HUMANHEAD rww - allow projectiles to collide with owner after portalling + physicsObj.GetClipModel()->SetOwner(this); + } +} // END HUMANHEAD + +//============================================================================= +// +// hhProjectile::AllowCollision +// +// Determines if the projectile can strike a given entity. Here, all +// projectiles will pass-through Wraiths (other than arrows, which is handled +// in the arrow code) +//============================================================================= +bool hhProjectile::AllowCollision( const trace_t& collision ) { + return ProcessAllowCollisionEvent( &collision ); +} + +/* +================ +hhProjectile::Event_Fuse_Explode +================ +*/ +void hhProjectile::Event_Fuse_Explode() { + trace_t collision; + + SIMDProcessor->Memset( &collision, 0, sizeof(trace_t) ); + collision.endpos = GetOrigin(); + collision.endAxis = GetAxis(); + collision.c.normal = -gameLocal.GetGravityNormal(); + Explode( &collision, GetPhysics()->GetLinearVelocity(), 0 ); +} + +/* +================ +hhProjectile::Event_Collision_Explode +================ +*/ +void hhProjectile::Event_Collision_Explode( const trace_t* collision, const idVec3& velocity ) { + Explode( collision, velocity, ProcessCollision(collision, velocity) ); + idThread::ReturnInt( 1 ); +} + +/* +================ +hhProjectile::Event_Collision_Impact +================ +*/ +void hhProjectile::Event_Collision_Impact( const trace_t* collision, const idVec3& velocity ) { + CancelEvents( &EV_Explode ); + RemoveProjectile( ProcessCollision(collision, velocity) ); + state = COLLIDED; + idThread::ReturnInt( 1 ); +} + +/* +================ +hhProjectile::Event_Collision_DisturbLiquid +================ +*/ +void hhProjectile::Event_Collision_DisturbLiquid( const trace_t* collision, const idVec3& velocity ) { + CancelEvents( &EV_Explode ); + RemoveProjectile( ProcessCollision(collision, velocity) ); + idThread::ReturnInt( 1 ); +} + +/* +================ +hhProjectile::Event_Collision_Remove +================ +*/ +void hhProjectile::Event_Collision_Remove( const trace_t* collision, const idVec3& velocity ) { + RemoveProjectile( 0 ); + idThread::ReturnInt( 1 ); +} + +/* +================ +hhProjectile::Event_AllowCollision_CollideNoProj +================ +*/ +void hhProjectile::Event_AllowCollision_CollideNoProj( const trace_t* collision ) { + idEntity* ent = gameLocal.entities[ collision->c.entityNum ]; + if( ent->IsType( hhProjectile::Type ) ) { + if ( static_cast(ent)->ProjCollide() ) { + idThread::ReturnInt( 1 ); + return; + } + } + + if( ent->IsType( hhProjectile::Type ) && // If we're colliding with another projectile + ( !bNoCollideWithCrawlers || // AND we're not set to collide with crawlers + !( ent->IsType( hhProjectileRocketLauncher::Type ) && // OR we're not a rocket launcher projectile + ent->IsType( hhProjectileCrawlerGrenade::Type ) ) ) ) { // AND we're not a crawler projectile + idThread::ReturnInt( 0 ); // Pass through + return; + } + + hhProjectile::Event_AllowCollision_Collide( collision ); +} + +/* +================ +hhProjectile::Event_AllowCollision_Collide +================ +*/ +void hhProjectile::Event_AllowCollision_Collide( const trace_t* collision ) { + idThread::ReturnInt( 1 ); +} + +/* +================ +hhProjectile::Event_AllowCollision_PassThru +================ +*/ +void hhProjectile::Event_AllowCollision_PassThru( const trace_t* collision ) { + idThread::ReturnInt( 0 ); +} + +/* +================ +hhProjectile::Event_SpawnDriverLocal + +HUMANHEAD: aob +================ +*/ +void hhProjectile::Event_SpawnDriverLocal( const char* defName ) { + if( !defName || !defName[ 0 ] ) { + return; + } + + driver = static_cast( gameLocal.SpawnObject(defName) ); + driver->SetPassenger( this ); + + //Not sure if this should be pulled out of the def_driver dict or not + float roll = hhMath::Lerp( spawnArgs.GetVec2("driver_rollRange"), gameLocal.random.RandomFloat() ); + driver->SetAxis( idAngles( 0.0f, 0.0f, roll ).ToMat3() * driver->GetAxis() ); +} + +/* +================ +hhProjectile::Event_SpawnFxFlyLocal + +HUMANHEAD: aob +================ +*/ +void hhProjectile::Event_SpawnFxFlyLocal( const char* defName ) { + if( !defName || !defName[0] ) { + return; + } + + SAFE_REMOVE(fxFly); + hhFxInfo fxInfo; + + fxInfo.SetNormal( -GetAxis()[0] ); + fxInfo.SetEntity( this ); + fxInfo.RemoveWhenDone( false ); + fxFly = SpawnFxLocal( defName, GetOrigin(), GetAxis(), &fxInfo, true ); //rww - client (local) entity. + if (fxFly.IsValid()) { + fxFly->fl.neverDormant = true; + + //rww + fxFly->fl.networkSync = false; + fxFly->fl.clientEvents = true; + } +} + +//================ +//hhProjectile::Save +//================ +void hhProjectile::Save( idSaveGame *savefile ) const { + driver.Save( savefile ); + fxFly.Save( savefile ); + savefile->WriteInt( thrust_start ); + savefile->WriteBool( bDDACounted ); + parentProjectile.Save( savefile ); + savefile->WriteInt( launchTimestamp ); + savefile->WriteInt( weaponNum ); + savefile->WriteBool( bPlayFlyBySound ); + savefile->WriteFloat( flyBySoundDistSq ); + + collidedPortal.Save( savefile ); + savefile->WriteVec3( collideLocation ); + savefile->WriteVec3( collideVelocity ); +} + +//================ +//hhProjectile::Restore +//================ +void hhProjectile::Restore( idRestoreGame *savefile ) { + driver.Restore( savefile ); + fxFly.Restore( savefile ); + savefile->ReadInt( thrust_start ); + savefile->ReadBool( bDDACounted ); + parentProjectile.Restore( savefile ); + savefile->ReadInt( launchTimestamp ); + savefile->ReadInt( weaponNum ); + savefile->ReadBool( bPlayFlyBySound ); + savefile->ReadFloat( flyBySoundDistSq ); + + bNoCollideWithCrawlers = spawnArgs.GetBool( "noCollideWithCrawlers", "0" ); + bProjCollide = spawnArgs.GetBool( "proj_collision", "0" ); + + collidedPortal.Restore( savefile ); + savefile->ReadVec3( collideLocation ); + savefile->ReadVec3( collideVelocity ); +} + +/* +================ +hhProjectile::WriteToSnapshot +================ +*/ +void hhProjectile::WriteToSnapshot( idBitMsgDelta &msg ) const { + //rww - we capture the launch orientation/pos for predicting the projectile launch, and (usually) don't sync physics at all + if (fabsf(launchQuat.ToAngles().roll) > 0.001f) { //is it going to translate to a direction happily, or do we need a real orientation? + msg.WriteBits(1, 1); + msg.WriteFloat(launchQuat.x); + msg.WriteFloat(launchQuat.y); + msg.WriteFloat(launchQuat.z); + } + else { + msg.WriteBits(0, 1); + msg.WriteDir(launchQuat.ToMat3()[0], 24); + } + msg.WriteFloat(launchPos.x); + msg.WriteFloat(launchPos.y); + msg.WriteFloat(launchPos.z); + + idProjectile::WriteToSnapshot(msg); +} + +/* +================ +hhProjectile::ReadFromSnapshot +================ +*/ +void hhProjectile::ReadFromSnapshot( const idBitMsgDelta &msg ) { + //rww - we capture the launch orientation for predicting the projectile launch, and (usually) don't sync physics at all + bool fullOrientation = !!msg.ReadBits(1); + if (fullOrientation) { + launchQuat.x = msg.ReadFloat(); + launchQuat.y = msg.ReadFloat(); + launchQuat.z = msg.ReadFloat(); + } + else { + idVec3 dir = msg.ReadDir(24); + launchQuat = dir.ToMat3().ToCQuat(); + } + launchPos.x = msg.ReadFloat(); + launchPos.y = msg.ReadFloat(); + launchPos.z = msg.ReadFloat(); + + idProjectile::ReadFromSnapshot(msg); +} + +/* +================ +hhProjectile::ClientHideProjectile +================ +*/ +void hhProjectile::ClientHideProjectile(void) { + Hide(); + FreeLightDef(); + BecomeInactive(TH_THINK|TH_PHYSICS); + + GetPhysics()->PutToRest(); + + if (fxFly.IsValid()) { + fxFly->Stop(); + SAFE_REMOVE( fxFly ); + } +} + +/* +=============== +hhProjectile::GetBounceDirection +=============== +*/ + +idVec3 hhProjectile::GetBounceDirection( const idVec3 &incoming, + const idVec3 &surface_normal, + const idEntity *incoming_entity, + const idEntity *surface_entity ) { + idVec3 bounceDir; + idVec3 tanget; + idVec3 normal; + float dot = 0.0f; + float eProjectile = 1.0f; // Elasticity constants + float eTarget = 1.0f; + float fProjectile = 0.0f; // Friction constants + float fTarget = 0.0f; + + if ( incoming_entity ) { + eProjectile = incoming_entity->spawnArgs.GetFloat("bounce", "1"); + fProjectile = incoming_entity->spawnArgs.GetFloat("contact_friction", "0"); + } + + if ( surface_entity ) { + eTarget = surface_entity->spawnArgs.GetFloat("bounce", "1"); + fTarget = surface_entity->spawnArgs.GetFloat("contact_friction", "0"); + } + + dot = incoming * surface_normal; + normal = surface_normal * dot; + tanget = incoming - normal; + + bounceDir = tanget * (1.0f - fProjectile) * (1.0f - fTarget) - + normal * (eProjectile * eTarget); + + //? Should this all be seperated out? Ie, what if they don't want it normalized? + if ( bounceDir.Length() < .0001f ) { + bounceDir = surface_normal; + } + + //HUMANHEAD: nla/aob - removed normalize to give return value more dependence on inputs. + + return( bounceDir ); +} + diff --git a/src/Prey/prey_projectile.h b/src/Prey/prey_projectile.h new file mode 100644 index 0000000..a5b0421 --- /dev/null +++ b/src/Prey/prey_projectile.h @@ -0,0 +1,176 @@ +#ifndef __PREY_PROJECTILE_H__ +#define __PREY_PROJECTILE_H__ + +class hhBeamSystem; + +extern const idEventDef EV_SpawnDriverLocal; +extern const idEventDef EV_SpawnFxFlyLocal; + +extern const idEventDef EV_Collision_Flesh; +extern const idEventDef EV_Collision_Metal; +extern const idEventDef EV_Collision_AltMetal; +extern const idEventDef EV_Collision_Wood; +extern const idEventDef EV_Collision_Stone; +extern const idEventDef EV_Collision_Glass; +extern const idEventDef EV_Collision_Liquid; +extern const idEventDef EV_Collision_Spirit; +extern const idEventDef EV_Collision_Remove; +extern const idEventDef EV_Collision_CardBoard; +extern const idEventDef EV_Collision_Tile; +extern const idEventDef EV_Collision_Forcefield; +extern const idEventDef EV_Collision_Wallwalk; +extern const idEventDef EV_Collision_Chaff; +extern const idEventDef EV_Collision_Pipe; + + +extern const idEventDef EV_AllowCollision_Flesh; +extern const idEventDef EV_AllowCollision_Metal; +extern const idEventDef EV_AllowCollision_AltMetal; +extern const idEventDef EV_AllowCollision_Wood; +extern const idEventDef EV_AllowCollision_Stone; +extern const idEventDef EV_AllowCollision_Glass; +extern const idEventDef EV_AllowCollision_Liquid; +extern const idEventDef EV_AllowCollision_Spirit; +extern const idEventDef EV_AllowCollision_CardBoard; +extern const idEventDef EV_AllowCollision_Tile; +extern const idEventDef EV_AllowCollision_Forcefield; +extern const idEventDef EV_AllowCollision_Wallwalk; +extern const idEventDef EV_AllowCollision_Chaff; +extern const idEventDef EV_AllowCollision_Pipe; + + +/*********************************************************************** + + hhProjectile + +***********************************************************************/ +class hhPortal; // cjr + +class hhProjectile : public idProjectile { + CLASS_PROTOTYPE( hhProjectile ); + + public: + void Spawn(); + virtual ~hhProjectile(); + virtual void Create( idEntity *owner, const idVec3 &start, const idVec3 &dir ); + virtual void Launch( const idVec3 &start, const idVec3 &dir, const idVec3 &pushVelocity, const float timeSinceFire = 0.0f, const float launchPower = 1.0f, const float dmgPower = 1.0f ); + + virtual void Create( idEntity *owner, const idVec3 &start, const idMat3 &axis ); + virtual void Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire = 0.0f, const float launchPower = 1.0f, const float dmgPower = 1.0f ); + + virtual void SetOrigin( const idVec3& origin ); + virtual void SetAxis( const idMat3& axis ); + + virtual void Think(); + + virtual void RemoveProjectile( const int removeDelay ); + virtual bool Collide( const trace_t &collision, const idVec3 &velocity ); + virtual bool ProcessCollisionEvent( const trace_t* collision, const idVec3& velocity ); + virtual void Explode( const trace_t* collision, const idVec3& velocity, int removeDelay ); + virtual void SplashDamage( const idVec3& origin, idEntity* attacker, idEntity* ignoreDamage, idEntity* ignorePush, const char* splashDamageDefName ); + virtual void BounceSplat( const idVec3& origin, const idVec3& dir ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + virtual void Fizzle( void ); + + virtual void SetGravity( const idVec3 &newGravity ); + virtual const idVec3& GetGravity() const { return idEntity::GetGravity(); } + + virtual bool GetDDACounted() const { return bDDACounted; } + virtual void SetDDACounted() { bDDACounted = true; } + virtual void SetParentProjectile( hhProjectile* in_parent ); + virtual hhProjectile* GetParentProjectile( void ); + virtual int GetLaunchTimestamp() const { return launchTimestamp; } + + virtual void Portalled(idEntity *portal); + virtual bool AllowCollision( const trace_t &collision ); + + virtual void UpdateBalanceInfo( const trace_t* collision, const idEntity* hitEnt ); + + static hhProjectile* SpawnProjectile( const idDict* args ); + static hhProjectile* SpawnClientProjectile( const idDict* args ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientHideProjectile(void); + + virtual const int GetWeaponNum( void ) const { return weaponNum; } // HUMANHEAD CJR: weapon-specific index on each projectile + bool ProjCollide() { return bProjCollide; } + + virtual int ProcessCollision( const trace_t* collision, const idVec3& velocity ); //rww - made public + + void SetCollidedPortal( hhPortal *newPortal, idVec3 newLocation, idVec3 newVelocity ); // cjr + + static idVec3 GetBounceDirection( const idVec3 &incoming_vector, + const idVec3 &surface_normal, + const idEntity *incoming_entity = NULL, + const idEntity *surface_entity = NULL ); + + idCQuat launchQuat; //rww - for saving off launch orientation + idVec3 launchPos; //rww - for saving off launch pos + + protected: + virtual float DetermineDamageScale( const trace_t* collision ) const { return 1.0f; } + virtual bool DamageIsValid( const trace_t* collision, float& damageScale ); + virtual int DetermineContents(); + virtual int DetermineClipmask(); + + virtual idEntity* DetermineClipModelOwner(); + + virtual int PlayImpactSound( const idDict* dict, const idVec3 &origin, surfTypes_t type ); + + virtual void UpdateLight(); + virtual void UpdateLightPosition(); + virtual void UpdateLightFade(); + virtual int CreateLight( const char* shaderName, const idVec3& size, const idVec3& color, const idVec3& offset, float fadeTime ); + + virtual void ApplyDamageEffect( idEntity* entHit, const trace_t* collision, const idVec3& velocity, const char* damageDefName ); + + virtual void SpawnExplosionFx( const trace_t* collision ); + virtual void SpawnDebris( const idVec3& collisionNormal, const idVec3& collisionDir ); + + virtual void DamageEntityHit( const trace_t* collision, const idVec3& velocity, idEntity* entHit ); + + virtual bool ProcessAllowCollisionEvent( const trace_t* collision ); + + protected: + void Event_Collision_Explode( const trace_t* collision, const idVec3& velocity ); + void Event_Collision_Impact( const trace_t* collision, const idVec3& velocity ); + void Event_Collision_DisturbLiquid( const trace_t* collision, const idVec3& velocity ); + void Event_Collision_Remove( const trace_t* collision, const idVec3& velocity ); + void Event_AllowCollision_CollideNoProj( const trace_t* collision ); + + void Event_AllowCollision_Collide( const trace_t* collision ); + void Event_AllowCollision_PassThru( const trace_t* collision ); + + void Event_Fuse_Explode(); + + void Event_SpawnDriverLocal( const char* defName ); + void Event_SpawnFxFlyLocal( const char* defName ); + + + protected: + idEntityPtr driver; + idEntityPtr fxFly; + int thrust_start; + bool bDDACounted; //already counted by dda as hitting something + idEntityPtr parentProjectile; //projectile that spawned me + int launchTimestamp; + + int weaponNum; // cjr - weapon index that spawned this projectile (-1) for non-player weapons + + bool bNoCollideWithCrawlers; // mdl: Defines whether or not we collide with crawlers/rockets + bool bProjCollide; + + // jsh flyby sounds + float flyBySoundDistSq; + bool bPlayFlyBySound; + + idEntityPtr collidedPortal; // cjr: This projectile struck a portal, so it should get portalled before thinking + idVec3 collideLocation; // cjr: This projectile struck a portal, so it should get portalled before thinking + idVec3 collideVelocity; // cjr: This projectile struck a portal, so it should get portalled before thinking +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectileautocannon.cpp b/src/Prey/prey_projectileautocannon.cpp new file mode 100644 index 0000000..0ab2401 --- /dev/null +++ b/src/Prey/prey_projectileautocannon.cpp @@ -0,0 +1,21 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhProjectile, hhProjectileAutoCannonGrenade ) + EVENT( EV_Collision_Flesh, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_AltMetal, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Wood, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Stone, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Glass, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Liquid, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_CardBoard, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Tile, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Forcefield, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Pipe, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Wallwalk, hhProjectileAutoCannonGrenade::Event_Collision_Explode ) + + EVENT( EV_AllowCollision_Chaff, hhProjectileAutoCannonGrenade::Event_AllowCollision_Collide ) +END_CLASS \ No newline at end of file diff --git a/src/Prey/prey_projectileautocannon.h b/src/Prey/prey_projectileautocannon.h new file mode 100644 index 0000000..026a362 --- /dev/null +++ b/src/Prey/prey_projectileautocannon.h @@ -0,0 +1,8 @@ +#ifndef __HH_PROJECTILE_AUTOCANNON_GRENADE_H +#define __HH_PROJECTILE_AUTOCANNON_GRENADE_H + +class hhProjectileAutoCannonGrenade : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileAutoCannonGrenade ) +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectilebounce.cpp b/src/Prey/prey_projectilebounce.cpp new file mode 100644 index 0000000..82f7e84 --- /dev/null +++ b/src/Prey/prey_projectilebounce.cpp @@ -0,0 +1,34 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#include "prey_local.h" + +CLASS_DECLARATION( hhProjectile, hhProjectileBounce ) + EVENT( EV_Collision_Flesh, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Metal, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_AltMetal, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Wood, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Stone, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Glass, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_CardBoard, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Tile, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Forcefield, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Pipe, hhProjectileBounce::Event_Collision_Bounce ) + EVENT( EV_Collision_Wallwalk, hhProjectileBounce::Event_Collision_Bounce ) + +END_CLASS + +void hhProjectileBounce::Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ) { + if( !collision || collision->fraction == 1.0f ) { + return; + } + StartSound( "snd_bounce", SND_CHANNEL_BODY, 0, true, NULL ); + float dot = velocity * collision->c.normal; + idVec3 normal = collision->c.normal * dot; + idVec3 tangent = (velocity - normal).ToNormal() - normal.ToNormal(); + idVec3 newVelocity = tangent.ToNormal() * velocity.Length(); + physicsObj.SetLinearVelocity( newVelocity ); + idThread::ReturnInt( 0 ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilebounce.h b/src/Prey/prey_projectilebounce.h new file mode 100644 index 0000000..191c186 --- /dev/null +++ b/src/Prey/prey_projectilebounce.h @@ -0,0 +1,11 @@ +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#ifndef __HH_PROJECTILE_BOUNCE_H +#define __HH_PROJECTILE_BOUNCE_H + +class hhProjectileBounce : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileBounce ); + void Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ); +}; + +#endif +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilebug.cpp b/src/Prey/prey_projectilebug.cpp new file mode 100644 index 0000000..a94c4d3 --- /dev/null +++ b/src/Prey/prey_projectilebug.cpp @@ -0,0 +1,135 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#include "prey_local.h" + +const idEventDef EV_Guide( "" ); + +CLASS_DECLARATION( hhProjectileTracking, hhProjectileBug ) + EVENT( EV_Collision_Flesh, hhProjectileBug::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_AltMetal, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Wood, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Stone, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Glass, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Liquid, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_CardBoard, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Tile, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Forcefield, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Chaff, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Wallwalk, hhProjectileBug::Event_Collision_Bounce ) + EVENT( EV_Collision_Pipe, hhProjectileBug::Event_Collision_Bounce ) + + EVENT( EV_Guide, hhProjectileBug::Event_TrackTarget ) +END_CLASS + +void hhProjectileBug::Spawn() { + enemyRadius = spawnArgs.GetFloat( "enemy_radius", "200" ); +} + +idEntity* hhProjectileBug::DetermineEnemy() { + idEntity * entityList[ MAX_GENTITIES ]; + idEntity* possibleEnemy = NULL; + float currentEnemyDot = 0.0f; + float currentEnemyDist = CM_MAX_TRACE_DIST; + idEntity* localEnemy = NULL; + + //find player if within certain radius + for( int i = 0; i < gameLocal.numClients; i++ ) { + if ( gameLocal.entities[ i ] ) { + possibleEnemy = gameLocal.entities[i]; + if ( !possibleEnemy || (possibleEnemy->GetOrigin() - GetOrigin()).Length() > enemyRadius ) { + continue; + } + localEnemy = WhosClosest( possibleEnemy, localEnemy, currentEnemyDot, currentEnemyDist ); + } + } + if ( localEnemy ) { + return localEnemy; + } + + //otherwise look for bug triggers + float bestDist = 9999999; + int bestIndex = -1; + idBounds bounds = idBounds( GetOrigin() ).Expand( spawnArgs.GetFloat( "enemy_check_radius" )); + int numListedEntities = gameLocal.clip.EntitiesTouchingBounds( bounds, -1, entityList, MAX_GENTITIES ); + for ( int i=0; ispawnArgs.GetInt( "bug_trigger", "0" ) ) { + continue; + } + float dist = ( GetOrigin() - possibleEnemy->GetOrigin() ).Length(); + if ( dist < bestDist ) { + bestIndex = i; + bestDist = dist; + } + } + + if ( bestIndex >= 0 ) { + if ( entityList[bestIndex]->IsType( idActor::Type ) ) { + physicsObj.SetContents( 0 ); + } else { + physicsObj.SetContents( CONTENTS_PROJECTILE ); + } + return entityList[bestIndex]; + } + + return NULL; +} + +idVec3 hhProjectileBug::DetermineEnemyPosition( const idEntity* ent ) const { + float randomOffset = spawnArgs.GetFloat( "offset_max", "70" ) * gameLocal.random.RandomFloat(); + if ( ent && ent->IsType( idActor::Type ) ) { + const idActor *entActor = static_cast(ent); + return entActor->GetEyePosition() + idVec3(0,0,randomOffset); + } + + return ent->GetOrigin() + idVec3(0,0,randomOffset); +} + +void hhProjectileBug::Event_TrackTarget() { + idEntity *newEnemy = DetermineEnemy(); + if( !newEnemy ) { + physicsObj.SetLinearVelocity( GetAxis()[ 0 ] * velocity[ 0 ] + GetAxis()[ 1 ] * velocity[ 1 ] + GetAxis()[ 2 ] * velocity[ 2 ] ); + physicsObj.SetAngularVelocity( angularVelocity.ToAngularVelocity() * GetAxis() ); + return; + } + enemy = newEnemy; + + idVec3 enemyDir = DetermineEnemyDir( enemy.GetEntity() ); + idVec3 currentDir = GetAxis()[0]; + idVec3 newDir = currentDir*(1-turnFactor) + enemyDir*turnFactor; + newDir.Normalize(); + if ( driver.IsValid() ) { + driver->SetAxis(newDir.ToMat3()); + } else { + SetAxis(newDir.ToMat3()); + } + + physicsObj.SetLinearVelocity( GetAxis()[ 0 ] * velocity[ 0 ] + GetAxis()[ 1 ] * velocity[ 1 ] + GetAxis()[ 2 ] * velocity[ 2 ] ); + physicsObj.SetAngularVelocity( angularVelocity.ToAngularVelocity() * GetAxis() ); + + PostEventMS( &EV_Guide, updateRate ); +} + +void hhProjectileBug::Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ) { + physicsObj.SetLinearVelocity( hhProjectile::GetBounceDirection( physicsObj.GetLinearVelocity(), collision->c.normal ) ); + idThread::ReturnInt( 0 ); +} + +//================ +//hhProjectileBug::Save +//================ +void hhProjectileBug::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( enemyRadius ); +} + +//================ +//hhProjectileBug::Restore +//================ +void hhProjectileBug::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( enemyRadius ); +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilebug.h b/src/Prey/prey_projectilebug.h new file mode 100644 index 0000000..5696804 --- /dev/null +++ b/src/Prey/prey_projectilebug.h @@ -0,0 +1,20 @@ +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#ifndef __HH_PROJECTILE_BUG_H +#define __HH_PROJECTILE_BUG_H + +class hhProjectileBug: public hhProjectileTracking { +public: + CLASS_PROTOTYPE( hhProjectileBug ) + idEntity* DetermineEnemy(); + void Event_TrackTarget(); + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + idVec3 DetermineEnemyPosition( const idEntity* ent ) const; + void Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ); +protected: + float enemyRadius; +}; + +#endif +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilebugtrigger.cpp b/src/Prey/prey_projectilebugtrigger.cpp new file mode 100644 index 0000000..50df589 --- /dev/null +++ b/src/Prey/prey_projectilebugtrigger.cpp @@ -0,0 +1,40 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#include "prey_local.h" + +CLASS_DECLARATION( hhProjectile, hhProjectileBugTrigger ) + EVENT( EV_Collision_Flesh, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Metal, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_AltMetal, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Wood, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Stone, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Glass, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Liquid, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_CardBoard, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Tile, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Forcefield, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Chaff, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Pipe, hhProjectileBugTrigger::Event_Collision_Impact ) + EVENT( EV_Collision_Wallwalk, hhProjectileBugTrigger::Event_Collision_Impact ) + + EVENT( EV_AllowCollision_Chaff, hhProjectileBugTrigger::Event_AllowCollision_Collide ) + EVENT( EV_Touch, hhProjectileBugTrigger::Event_Touch ) +END_CLASS + +void hhProjectileBugTrigger::Event_Collision_Impact( const trace_t* collision, const idVec3& velocity ) { + ProcessCollision(collision, velocity); + RemoveProjectile( spawnArgs.GetInt( "remove_time", "1500" ) ); + GetPhysics()->SetContents( CONTENTS_TRIGGER ); + idThread::ReturnInt( 1 ); +} + +void hhProjectileBugTrigger::Event_Touch( idEntity *other, trace_t *trace ) { + idAI *ownerAI = static_cast(owner.GetEntity()); + if ( other && ownerAI && ownerAI->GetEnemy() == other ) { + other->Damage( this, owner.GetEntity(), idVec3( 0,0,1 ), spawnArgs.GetString( "def_damage" ), 1.0, 0 ); + GetPhysics()->SetContents( 0 ); + } +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilebugtrigger.h b/src/Prey/prey_projectilebugtrigger.h new file mode 100644 index 0000000..9f8b334 --- /dev/null +++ b/src/Prey/prey_projectilebugtrigger.h @@ -0,0 +1,13 @@ +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#ifndef __HH_PROJECTILE_BUGTRIGGER_H +#define __HH_PROJECTILE_BUGTRIGGER_H + +class hhProjectileBugTrigger: public hhProjectile { +public: + CLASS_PROTOTYPE( hhProjectileBugTrigger ) + void Event_Collision_Impact( const trace_t* collision, const idVec3& velocity ); + void Event_Touch( idEntity *other, trace_t *trace ); +}; + +#endif +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilecocoon.cpp b/src/Prey/prey_projectilecocoon.cpp new file mode 100644 index 0000000..48ea2b5 --- /dev/null +++ b/src/Prey/prey_projectilecocoon.cpp @@ -0,0 +1,105 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#include "prey_local.h" + +const idEventDef EV_Guide( "" ); + +CLASS_DECLARATION( hhProjectileTracking, hhProjectileCocoon ) + EVENT( EV_Collision_Flesh, hhProjectileCocoon::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileCocoon::Event_Collision_Proj ) + EVENT( EV_Collision_AltMetal, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Wood, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Stone, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Glass, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Liquid, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_CardBoard, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Tile, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Forcefield, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Chaff, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Wallwalk, hhProjectileCocoon::Event_Collision_Bounce ) + EVENT( EV_Collision_Pipe, hhProjectileCocoon::Event_Collision_Bounce ) + + EVENT( EV_Guide, hhProjectileCocoon::Event_TrackTarget ) + EVENT( EV_AllowCollision_Flesh, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Metal, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_AltMetal, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Wood, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Stone, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Glass, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Liquid, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_CardBoard, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Tile, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Forcefield, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Pipe, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Wallwalk, hhProjectileCocoon::Event_AllowCollision ) + EVENT( EV_AllowCollision_Spirit, hhProjectileCocoon::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_Chaff, hhProjectileCocoon::Event_AllowCollision_Collide ) + +END_CLASS + +void hhProjectileCocoon::Spawn() { + nextBounceTime = 0; +} + +void hhProjectileCocoon::Event_TrackTarget() { + idVec3 newVelocity; + if( !enemy.IsValid() ) { + physicsObj.SetLinearVelocity( GetAxis()[ 0 ] * velocity[ 0 ] + GetAxis()[ 1 ] * velocity[ 1 ] + GetAxis()[ 2 ] * velocity[ 2 ] ); + physicsObj.SetAngularVelocity( angularVelocity.ToAngularVelocity() * GetAxis() ); + return; + } + + idVec3 enemyDir = DetermineEnemyDir( enemy.GetEntity() ); + idVec3 currentDir = GetAxis()[0]; + idVec3 newDir = currentDir*(1-turnFactor) + enemyDir*turnFactor; + newDir.Normalize(); + if ( driver.IsValid() ) { + driver->SetAxis(newDir.ToMat3()); + } else { + SetAxis(newDir.ToMat3()); + } + newVelocity = GetAxis()[ 0 ] * velocity[ 0 ] + GetAxis()[ 1 ] * velocity[ 1 ] + GetAxis()[ 2 ] * velocity[ 2 ]; + newVelocity *= spawnArgs.GetFloat( "bounce", "1.0" ); + physicsObj.SetLinearVelocity( newVelocity ); + physicsObj.SetAngularVelocity( angularVelocity.ToAngularVelocity() * GetAxis() ); +} + +void hhProjectileCocoon::Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ) { + if ( gameLocal.time >= nextBounceTime ) { + nextBounceTime = gameLocal.time + spawnArgs.GetFloat( "bounce_freq", "200" ); + + StopSound( SND_CHANNEL_BODY, true ); + StartSound( "snd_bounce", SND_CHANNEL_BODY, 0, true, NULL ); + idEntity *ent = gameLocal.entities[ collision->c.entityNum ]; + Event_TrackTarget(); + BounceSplat( GetOrigin(), -collision->c.normal ); + } + idThread::ReturnInt( 0 ); +} + +void hhProjectileCocoon::Event_Collision_Proj( const trace_t* collision, const idVec3 &velocity ) { + idEntity *ent = gameLocal.entities[ collision->c.entityNum ]; + if ( ent && ent->IsType( idProjectile::Type ) ) { + Explode( collision, velocity, 0 ); + } else { + Event_Collision_Bounce( collision, velocity ); + } + + idThread::ReturnInt( 0 ); +} + +void hhProjectileCocoon::Event_AllowCollision( const trace_t* collision ) { + idEntity *ent = gameLocal.entities[ collision->c.entityNum ]; + if ( ent && ent->IsType( idProjectile::Type ) ) { + int foo = 0; + } + + idThread::ReturnInt( 1 ); +} + +void hhProjectileCocoon::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + hhProjectileTracking::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilecocoon.h b/src/Prey/prey_projectilecocoon.h new file mode 100644 index 0000000..2dc1d88 --- /dev/null +++ b/src/Prey/prey_projectilecocoon.h @@ -0,0 +1,19 @@ +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#ifndef __HH_PROJECTILE_COCOON_H +#define __HH_PROJECTILE_COCOON_H + +class hhProjectileCocoon: public hhProjectileTracking { +public: + CLASS_PROTOTYPE( hhProjectileCocoon ) + void Spawn(); + void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); +protected: + void Event_Collision_Proj( const trace_t* collision, const idVec3 &velocity ); + void Event_TrackTarget(); + void Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ); + void Event_AllowCollision( const trace_t* collision ); + int nextBounceTime; +}; + +#endif +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilecrawlergrenade.cpp b/src/Prey/prey_projectilecrawlergrenade.cpp new file mode 100644 index 0000000..5161fee --- /dev/null +++ b/src/Prey/prey_projectilecrawlergrenade.cpp @@ -0,0 +1,557 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileCrawlerGrenade + +***********************************************************************/ +const idEventDef EV_ApplyExpandWound( "" ); + +const idEventDef EV_DyingState( "" ); +const idEventDef EV_DeadState( "" ); + +CLASS_DECLARATION( hhProjectile, hhProjectileCrawlerGrenade ) + EVENT( EV_ApplyExpandWound, hhProjectileCrawlerGrenade::Event_ApplyExpandWound ) + EVENT( EV_DyingState, hhProjectileCrawlerGrenade::EnterDyingState ) + EVENT( EV_DeadState, hhProjectileCrawlerGrenade::EnterDeadState ) + + EVENT( EV_Collision_Flesh, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Metal, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_AltMetal, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Wood, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Stone, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Glass, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_CardBoard, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Tile, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Forcefield, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Pipe, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + EVENT( EV_Collision_Wallwalk, hhProjectileCrawlerGrenade::Event_Collision_Bounce ) + //EVENT( EV_Collision_Chaff, hhProjectileCrawlerGrenade::Event_Collision_Explode ) + EVENT( EV_Collision_Liquid, hhProjectileCrawlerGrenade::Event_Collision_DisturbLiquid ) + + EVENT( EV_AllowCollision_Chaff, hhProjectileCrawlerGrenade::Event_AllowCollision_Collide ) +END_CLASS + +/* +================= +hhProjectileCrawlerGrenade::Spawn +================= +*/ +void hhProjectileCrawlerGrenade::Spawn() { + modelScale.Init( gameLocal.time, 0, 1.0f, 1.0f ); + modelProxy = NULL; + + InitCollisionInfo(); + + //doesn't matter for single player, only for network logic -rww + modelProxyCopyDone = false; + + if( !gameLocal.isClient ) { + SpawnModelProxy(); + } + + BecomeActive( TH_TICKER ); + + if (gameLocal.isClient) + { //rww - do this right away on the client + //Get rid of our model. The modelProxy is our model now. + SetModel( "" ); + } + + //rww - allow events on client + fl.clientEvents = true; +} + +/* +================= +hhProjectileCrawlerGrenade::~hhProjectileCrawlerGrenade +================= +*/ +hhProjectileCrawlerGrenade::~hhProjectileCrawlerGrenade() { + SAFE_REMOVE( modelProxy ); +} + +/* +================= +hhProjectileCrawlerGrenade::InitCollisionInfo +================= +*/ +void hhProjectileCrawlerGrenade::InitCollisionInfo() { + memset( &collisionInfo, 0, sizeof(trace_t) ); + collisionInfo.fraction = 1.0f; +} + +/* +================= +hhProjectileCrawlerGrenade::CopyToModelProxy +================= +*/ +void hhProjectileCrawlerGrenade::CopyToModelProxy() +{ + //rww - do this the right way with an inheriting entityDef + //idDict args = spawnArgs; + //args.Delete( "spawnclass" ); + //args.Delete( "name" ); + + idDict args; + idDict *setArgs; + idStr str; + + if (gameLocal.isClient) + { + setArgs = &modelProxy->spawnArgs; + } + else + { + args.Clear(); + setArgs = &args; + } + + setArgs->Set( "owner", GetName() ); + setArgs->SetVector( "origin", GetOrigin() ); + setArgs->SetMatrix( "rotation", GetAxis() ); + + //copy the model over + if (spawnArgs.GetString("model", "", str)) + { + setArgs->Set("model", str.c_str()); + if (gameLocal.isClient && modelProxy.IsValid()) + { + modelProxy->SetModel(str.c_str()); + } + } + + //these are now taken care of in the ent def. + /* + setArgs->SetBool( "useCombatModel", true ); + setArgs->SetBool( "transferDamage", false ); + setArgs->SetBool( "solid", false ); + */ + + if (!gameLocal.isClient) + { + modelProxy = gameLocal.SpawnObject("projectile_crawler_proxy", &args); + } + + if( modelProxy.IsValid() ) { + modelProxy->Bind( this, true ); + modelProxy->CycleAnim( "idle", ANIMCHANNEL_ALL ); + } + + //for debugging + //spawnArgs.CompareArgs(modelProxy->spawnArgs); + + modelProxyCopyDone = true; +} + +/* +================= +hhProjectileCrawlerGrenade::SpawnModelProxy +================= +*/ +void hhProjectileCrawlerGrenade::SpawnModelProxy() { + if (!gameLocal.isClient) + { + CopyToModelProxy(); + } + + //Get rid of our model. The modelProxy is our model now. + SetModel( "" ); +} + +/* +================= +hhProjectileCrawlerGrenade::Ticker +================= +*/ +void hhProjectileCrawlerGrenade::Ticker() { + if( state == StateDying ) { + if( modelProxy.IsValid() ) { + modelProxy->SetDeformation(DEFORMTYPE_SCALE, modelScale.GetCurrentValue(gameLocal.time)); + } + } +} + +/* +================ +hhProjectileCrawlerGrenade::Launch +================ +*/ +void hhProjectileCrawlerGrenade::Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire, const float launchPower, const float dmgPower ) { + hhProjectile::Launch( start, axis, pushVelocity, timeSinceFire, launchPower, dmgPower ); + + if( modelProxy.IsValid() ) { + modelProxy->CycleAnim( "flight", ANIMCHANNEL_ALL ); + } + + float delayBeforeDying = spawnArgs.GetFloat( "delayBeforeDying" ); + float fuse = spawnArgs.GetFloat( "fuse" ); + + inflateDuration = SEC2MS( hhMath::ClampFloat(0.0f, fuse, fuse - delayBeforeDying) ); + + PostEventSec( &EV_DyingState, delayBeforeDying ); + PostEventSec( &EV_DeadState, fuse ); +} + +/* +================ +hhProjectileCrawlerGrenade::SpawnFlyFx +================ +*/ +void hhProjectileCrawlerGrenade::SpawnFlyFx() { + if( modelProxy.IsValid() ) { + modelProxy->BroadcastFxInfoAlongBonePrefix( &spawnArgs, "fx_fly", "joint_legStub" ); + } +} + +/* +================= +hhProjectileCrawlerGrenade::Hide +================= +*/ +void hhProjectileCrawlerGrenade::Hide() { + hhProjectile::Hide(); + + if( modelProxy.IsValid() ) { + modelProxy->Hide(); + } +} + +/* +================= +hhProjectileCrawlerGrenade::Show +================= +*/ +void hhProjectileCrawlerGrenade::Show() { + hhProjectile::Show(); + + if( modelProxy.IsValid() ) { + modelProxy->Show(); + } +} + +/* +================= +hhProjectileCrawlerGrenade::RemoveProjectile +================= +*/ +void hhProjectileCrawlerGrenade::RemoveProjectile( const int removeDelay ) { + hhProjectile::RemoveProjectile( removeDelay ); + + if( modelProxy.IsValid() ) { + modelProxy->PostEventMS( &EV_Remove, removeDelay ); + } +} + +/* +================= +hhProjectileCrawlerGrenade::Event_ApplyExpandWound +================= +*/ +void hhProjectileCrawlerGrenade::Event_ApplyExpandWound() { + trace_t trace; + + if( !modelProxy.IsValid() || !modelProxy->GetCombatModel() ) { + return; + } + + idBounds clipBounds( modelProxy->GetRenderEntity()->bounds ); + idVec3 traceEnd = GetOrigin(); + idVec3 traceStart = traceEnd + hhUtils::RandomPointInShell( clipBounds.Expand(1.0f).GetRadius(), clipBounds.Expand(2.0f).GetRadius() ); + idVec3 jointOrigin, localOrigin, localNormal; + idMat3 jointAxis, axisTranspose; + jointHandle_t jointHandle = INVALID_JOINT; + + CancelEvents( &EV_ApplyExpandWound ); + PostEventSec( &EV_ApplyExpandWound, spawnArgs.GetFloat("expandWoundDelay") ); + + if( !gameLocal.clip.TracePoint(trace, traceStart, traceEnd, modelProxy->GetCombatModel()->GetContents(), NULL) ) { + return; + } + + if( trace.c.entityNum != entityNumber ) {//Make sure we hit ourselves + return; + } + + modelProxy->AddDamageEffect( trace, vec3_zero, spawnArgs.GetString("def_expandDamage"), (!fl.networkSync || netSyncPhysics) ); +} + +/* +================= +hhProjectileCrawlerGrenade::Event_Collision_Bounce +================= +*/ +void hhProjectileCrawlerGrenade::Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ) { + static const float minCollisionVelocity = 20.0f; + static const float maxCollisionVelocity = 90.0f; + + StopSound( SND_CHANNEL_BODY, true ); + + // Velocity in normal direction + float len = velocity * -collision->c.normal; + + if( collision->fraction < VECTOR_EPSILON || len < minCollisionVelocity ) { + idThread::ReturnInt( 0 ); + return; + } + + StartSound( "snd_bounce", SND_CHANNEL_BODY, 0, true, NULL ); + float volume = hhUtils::CalculateSoundVolume( len, minCollisionVelocity, maxCollisionVelocity ); + HH_SetSoundVolume( volume, SND_CHANNEL_BODY ); + + BounceSplat( GetOrigin(), -collision->c.normal ); + + SIMDProcessor->Memcpy( &collisionInfo, collision, sizeof(trace_t) ); + collisionInfo.fraction = 0.0f;//Sometimes fraction == 1.0f + + physicsObj.SetAngularVelocity( 0.5f*physicsObj.GetAngularVelocity() ); + + idThread::ReturnInt( 0 ); +} + +/* +================= +hhProjectileCrawlerGrenade::Event_Collision_DisturbLiquid +================= +*/ +void hhProjectileCrawlerGrenade::Event_Collision_DisturbLiquid( const trace_t* collision, const idVec3 &velocity ) { + CancelActivates(); + EnterDeadState(); + + hhProjectile::Event_Collision_DisturbLiquid( collision, velocity ); +} + +//================ +//hhProjectileCrawlerGrenade::Save +//================ +void hhProjectileCrawlerGrenade::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( modelScale.GetStartTime() ); // idInterpolate + savefile->WriteFloat( modelScale.GetDuration() ); + savefile->WriteFloat( modelScale.GetStartValue() ); + savefile->WriteFloat( modelScale.GetEndValue() ); + savefile->WriteInt( inflateDuration ); + savefile->WriteInt( state ); + savefile->WriteTrace( collisionInfo ); + modelProxy.Save( savefile ); +} + +//================ +//hhProjectileCrawlerGrenade::Restore +//================ +void hhProjectileCrawlerGrenade::Restore( idRestoreGame *savefile ) { + float set; + savefile->ReadFloat( set ); // idInterpolate + modelScale.SetStartTime( set ); + savefile->ReadFloat( set ); + modelScale.SetDuration( set ); + savefile->ReadFloat( set ); + modelScale.SetStartValue( set ); + savefile->ReadFloat( set ); + modelScale.SetEndValue( set ); + + savefile->ReadInt( inflateDuration ); + savefile->ReadInt( reinterpret_cast ( state ) ); + + savefile->ReadTrace( collisionInfo ); + modelProxy.Restore( savefile ); +} + +/* +================= +hhProjectileCrawlerGrenade::WriteToSnapshot +================= +*/ +void hhProjectileCrawlerGrenade::WriteToSnapshot( idBitMsgDelta &msg ) const +{ + msg.WriteBits(modelProxyCopyDone, 1); + msg.WriteBits(modelProxy.GetSpawnId(), 32); + + hhProjectile::WriteToSnapshot(msg); +} + +/* +================= +hhProjectileCrawlerGrenade::ReadFromSnapshot +================= +*/ +void hhProjectileCrawlerGrenade::ReadFromSnapshot( const idBitMsgDelta &msg ) +{ + bool newModelProxyCopyDone = !!msg.ReadBits(1); + if (modelProxy.SetSpawnId(msg.ReadBits(32))) + { + if (modelProxyCopyDone != newModelProxyCopyDone && + modelProxy.IsValid() && + modelProxy->IsType(hhGenericAnimatedPart::Type)) + { + modelProxyCopyDone = newModelProxyCopyDone; + CopyToModelProxy(); + } + } + + hhProjectile::ReadFromSnapshot(msg); +} + +/* +================= +hhProjectileCrawlerGrenade::EnterDyingState +================= +*/ +void hhProjectileCrawlerGrenade::EnterDyingState() { + state = StateDying; + modelScale.Init( gameLocal.GetTime(), inflateDuration, modelScale.GetCurrentValue(gameLocal.GetTime()), spawnArgs.GetFloat("inflateScale") ); + + StartSound( "snd_expand_screech", SND_CHANNEL_VOICE, 0, true, NULL ); + ProcessEvent( &EV_ApplyExpandWound ); +} + +/* +================= +hhProjectileCrawlerGrenade::EnterDeadState +================= +*/ +void hhProjectileCrawlerGrenade::EnterDeadState() { + state = StateDead; + CancelEvents( &EV_ApplyExpandWound ); + StopSound( SND_CHANNEL_VOICE, true ); +} + +/* +================= +hhProjectileCrawlerGrenade::CancelActivates +================= +*/ +void hhProjectileCrawlerGrenade::CancelActivates() { + CancelEvents( &EV_DyingState ); + CancelEvents( &EV_DeadState ); +} + +/*********************************************************************** + + hhProjectileStickyCrawlerGrenade + +***********************************************************************/ + +CLASS_DECLARATION( hhProjectileCrawlerGrenade, hhProjectileStickyCrawlerGrenade ) + EVENT( EV_Collision_Flesh, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Metal, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_AltMetal, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Wood, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Stone, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Glass, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_CardBoard, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Tile, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Forcefield, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Pipe, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + EVENT( EV_Collision_Wallwalk, hhProjectileStickyCrawlerGrenade::Event_Collision_Stick ) + + EVENT( EV_Activate, hhProjectileStickyCrawlerGrenade::Event_Activate ) +END_CLASS + +int hhProjectileStickyCrawlerGrenade::ProcessCollision( const trace_t* collision, const idVec3& velocity ) { + idEntity* entHit = gameLocal.entities[ collision->c.entityNum ]; + + //SAFE_REMOVE( fxFly ); + FreeLightDef(); + CancelEvents( &EV_Fizzle ); + + //physicsObj.SetContents( 0 ); + physicsObj.PutToRest(); + + surfTypes_t matterType = gameLocal.GetMatterType( entHit, collision->c.material, "hhProjectile::ProcessCollision" ); + return PlayImpactSound( gameLocal.FindEntityDefDict(spawnArgs.GetString("def_damage")), collision->endpos, matterType ); +} + +idMat3 hhProjectileStickyCrawlerGrenade::DetermineCollisionAxis( const idMat3& collisionAxis ) { + return collisionAxis; +} + +void hhProjectileStickyCrawlerGrenade::BindToCollisionObject( const trace_t* collision ) { + if( !collision || collision->fraction > 1.0f ) { + return; + } + + //HUMANHEAD PCF rww 05/18/06 - wait until we receive bind info from the server + if (gameLocal.isClient) { + return; + } + //HUMANHEAD END + + idEntity* pEntity = gameLocal.entities[collision->c.entityNum]; + HH_ASSERT( pEntity ); + + // HUMANHEAD PCF pdm 05-20-06: Check for some degenerate cases to combat the server hangs happening + if (pEntity == this || this->IsBound()) { + assert(0); // Report any of these + return; + } + // HUMANHEAD END + + jointHandle_t jointHandle = CLIPMODEL_ID_TO_JOINT_HANDLE( collision->c.id ); + if ( jointHandle != INVALID_JOINT ) { + SetOrigin( collision->endpos ); + SetAxis( DetermineCollisionAxis( (-collision->c.normal).ToMat3()) ); + BindToJoint( pEntity, jointHandle, true ); + } else { + SetOrigin( collision->endpos ); + SetAxis( DetermineCollisionAxis( (-collision->c.normal).ToMat3()) ); + Bind( pEntity, true ); + } +} + +void hhProjectileStickyCrawlerGrenade::Event_Collision_Stick( const trace_t* collision, const idVec3 &velocity ) { + if (proximityDetonateTrigger.GetEntity()) { //rww - don't allow this to be called more than once in a crawler grenade's lifetime + return; + } + ProcessCollision( collision, velocity ); + + BindToCollisionObject( collision ); + + fl.ignoreGravityZones = true; + SetGravity( idVec3(0.f, 0.f, 0.f) ); + spawnArgs.SetVector("gravity", idVec3(0.f, 0.f, 0.f) ); + + BounceSplat( GetOrigin(), -collision->c.normal ); + + idDict dict; + + dict.SetVector( "origin", GetOrigin() ); + //dict.SetMatrix( "rotation", GetAxis() ); + dict.Set( "target", name.c_str() ); + + dict.SetVector( "mins", spawnArgs.GetVector("detonationMins", "-10 -10 -10") ); + dict.SetVector( "maxs", spawnArgs.GetVector("detonationMaxs", "10 10 10") ); + if (!gameLocal.isClient) { + proximityDetonateTrigger = gameLocal.SpawnObject( spawnArgs.GetString("def_trigger"), &dict ); + proximityDetonateTrigger->Bind( this, true ); + if ( proximityDetonateTrigger->IsType( hhTrigger::Type ) ) { + hhTrigger *trigger = static_cast(proximityDetonateTrigger.GetEntity()); + if ( trigger && trigger->IsEncroached() ) { + proximityDetonateTrigger->PostEventMS( &EV_Activate, 0, this ); + } + } + } + + if( modelProxy.IsValid() ) { + modelProxy->CycleAnim( "idle", ANIMCHANNEL_ALL ); + } + + // CJR: Added this from the normal crawler collision bounce code + SIMDProcessor->Memcpy( &collisionInfo, collision, sizeof(trace_t) ); + collisionInfo.fraction = 0.0f;//Sometimes fraction == 1.0f + + idThread::ReturnInt( 1 ); +} + +void hhProjectileStickyCrawlerGrenade::Event_Activate( idEntity *pActivator ) { + StartSound( "snd_expand_screech", SND_CHANNEL_VOICE, 0, true, NULL ); + PostEventSec( &EV_Explode, spawnArgs.GetFloat( "explodeDelay", "1.0" ) ); +} + +void hhProjectileStickyCrawlerGrenade::Explode( const trace_t* collision, const idVec3& velocity, int removeDelay ) { + SAFE_REMOVE( fxFly ); + hhProjectile::Explode( &collisionInfo, velocity, removeDelay ); +} + diff --git a/src/Prey/prey_projectilecrawlergrenade.h b/src/Prey/prey_projectilecrawlergrenade.h new file mode 100644 index 0000000..2903b31 --- /dev/null +++ b/src/Prey/prey_projectilecrawlergrenade.h @@ -0,0 +1,86 @@ +#ifndef __HH_PROJECTILE_CRAWLER_GRENADE_H +#define __HH_PROJECTILE_CRAWLER_GRENADE_H + +extern const idEventDef EV_ApplyExpandWound; + +/*********************************************************************** + + hhProjectileCrawlerGrenade + +***********************************************************************/ +class hhProjectileCrawlerGrenade : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileCrawlerGrenade ); + + public: + void Spawn(); + virtual ~hhProjectileCrawlerGrenade(); + + virtual void Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire = 0.0f, const float launchPower = 1.0f, const float dmgPower = 1.0f ); + + virtual void Hide(); + virtual void Show(); + virtual void RemoveProjectile( const int removeDelay ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - networking + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + protected: + virtual void Ticker(); + + virtual void SpawnFlyFx(); + virtual void CopyToModelProxy(); + virtual void SpawnModelProxy(); + virtual void InitCollisionInfo(); + + protected: + void Event_ApplyExpandWound(); + + void Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ); + void Event_Collision_DisturbLiquid( const trace_t* collision, const idVec3 &velocity ); + void EnterDyingState(); + void EnterDeadState(); + void CancelActivates(); + + protected: + enum States { + StateAlive = 0, + StateDying, + StateDead + } state; + + idInterpolate modelScale; + int inflateDuration; + + trace_t collisionInfo; + + idEntityPtr modelProxy; + + bool modelProxyCopyDone; +}; + +/*********************************************************************** + + hhProjectileStickyCrawlerGrenade + +***********************************************************************/ +class hhProjectileStickyCrawlerGrenade : public hhProjectileCrawlerGrenade { + CLASS_PROTOTYPE( hhProjectileStickyCrawlerGrenade ); + + public: + int ProcessCollision( const trace_t* collision, const idVec3& velocity ); + idMat3 DetermineCollisionAxis( const idMat3& collisionAxis ); + void Event_Activate( idEntity *pActivator ); + + protected: + virtual void BindToCollisionObject( const trace_t* collision ); + void Event_Collision_Stick( const trace_t* collision, const idVec3 &velocity ); + virtual void Explode( const trace_t* collision, const idVec3& velocity, int removeDelay ); + + idEntityPtr proximityDetonateTrigger; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectilefreezer.cpp b/src/Prey/prey_projectilefreezer.cpp new file mode 100644 index 0000000..eb98835 --- /dev/null +++ b/src/Prey/prey_projectilefreezer.cpp @@ -0,0 +1,115 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileFreezer + +***********************************************************************/ +CLASS_DECLARATION( hhProjectile, hhProjectileFreezer ) + //EVENT( EV_Touch, hhProjectileFreezer::Event_Touch ) + + EVENT( EV_Collision_Flesh, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Metal, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_AltMetal, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Wood, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Stone, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Glass, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_CardBoard, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Tile, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Forcefield, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Pipe, hhProjectileFreezer::Event_Collision_Bounce ) + EVENT( EV_Collision_Wallwalk, hhProjectileFreezer::Event_Collision_Bounce ) +END_CLASS + +void hhProjectileFreezer::Spawn() { + decelStart = SEC2MS( spawnArgs.GetFloat("decelStart") ) + gameLocal.GetTime(); + decelEnd = SEC2MS( spawnArgs.GetFloat("decelDuration") ) + decelStart; + + collided=false; + + BecomeActive( TH_TICKER ); +} + +void hhProjectileFreezer::Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire, const float launchPower, const float dmgPower ) { + hhProjectile::Launch( start, axis, pushVelocity, timeSinceFire, launchPower, dmgPower ); + + cachedVelocity = GetPhysics()->GetLinearVelocity(); + + //fl.takedamage = false; + //physicsObj.DisableImpact(); +} + +void hhProjectileFreezer::Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ) { + idEntity *entityHit = gameLocal.entities[ collision->c.entityNum ]; + if ( entityHit->IsType(idAI::Type) || entityHit->IsType(idAFEntity_Base::Type) ) { + Event_Collision_Explode(collision, velocity); + return; + } + + ProcessCollision(collision, velocity); + collided=true; + idThread::ReturnInt( 1 ); +} + +bool hhProjectileFreezer::Collide( const trace_t& collision, const idVec3& velocity ) { + if(!collided) + return hhProjectile::Collide( collision, velocity ); + else + return false; +} + + +int hhProjectileFreezer::ProcessCollision( const trace_t* collision, const idVec3& velocity ) { + idEntity* entHit = gameLocal.entities[ collision->c.entityNum ]; + + SetOrigin( collision->endpos ); + SetAxis( collision->endAxis ); + + if (entHit) { //rww - may be null on client. + DamageEntityHit( collision, velocity, entHit ); + } + + fl.takedamage = false; + physicsObj.SetContents( 0 ); + physicsObj.PutToRest(); + + surfTypes_t matterType = gameLocal.GetMatterType( entHit, collision->c.material, "hhProjectile::ProcessCollision" ); + return PlayImpactSound( gameLocal.FindEntityDefDict(spawnArgs.GetString("def_damage")), collision->endpos, matterType ); +} + +void hhProjectileFreezer::Ticker() { + float scale = 0.0f; + if( gameLocal.GetTime() > decelStart && gameLocal.GetTime() < decelEnd ) { + scale = hhMath::Sin( DEG2RAD(hhMath::MidPointLerp( 0.0f, 30.0f, 90.0f, 1.0f - hhUtils::CalculateScale(gameLocal.GetTime(), decelStart, decelEnd))) ); + + GetPhysics()->SetLinearVelocity( cachedVelocity * hhMath::ClampFloat(0.05f, 1.0f, scale) ); + } +} + +void hhProjectileFreezer::Event_Touch( idEntity *other, trace_t *trace ) { + //Supposed to be empty + +} + +void hhProjectileFreezer::Save( idSaveGame *savefile ) const { + savefile->WriteInt( decelStart ); + savefile->WriteInt( decelEnd ); + savefile->WriteVec3( cachedVelocity ); + savefile->WriteBool( collided ); +} + +void hhProjectileFreezer::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( decelStart ); + savefile->ReadInt( decelEnd ); + savefile->ReadVec3( cachedVelocity ); + savefile->ReadBool( collided ); +} + +void hhProjectileFreezer::ApplyDamageEffect( idEntity* hitEnt, const trace_t* collision, const idVec3& velocity, const char* damageDefName ) { + if( hitEnt && gameLocal.random.RandomFloat() > 0.6f ) { + hitEnt->AddDamageEffect( *collision, velocity, damageDefName, (!fl.networkSync || netSyncPhysics) ); + } +} \ No newline at end of file diff --git a/src/Prey/prey_projectilefreezer.h b/src/Prey/prey_projectilefreezer.h new file mode 100644 index 0000000..b0aa427 --- /dev/null +++ b/src/Prey/prey_projectilefreezer.h @@ -0,0 +1,35 @@ +#ifndef __HH_PROJECTILE_FREEZER_H +#define __HH_PROJECTILE_FREEZER_H + +class hhProjectileFreezer : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileFreezer ); + + public: + void Spawn(); + + virtual void Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire = 0.0f, const float launchPower = 1.0f, const float dmgPower = 1.0f ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool Collide( const trace_t& collision, const idVec3& velocity ); + virtual int ProcessCollision( const trace_t* collision, const idVec3& velocity ); + + protected: + virtual void Ticker(); + + protected: + void Event_Touch( idEntity *other, trace_t *trace ); + void Event_Collision_Bounce( const trace_t* collision, const idVec3 &velocity ); + + void ApplyDamageEffect( idEntity* hitEnt, const trace_t* collision, const idVec3& velocity, const char* damageDefName ); + + protected: + int decelStart; + int decelEnd; + + idVec3 cachedVelocity; + bool collided; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectilegasbagpod.cpp b/src/Prey/prey_projectilegasbagpod.cpp new file mode 100644 index 0000000..d635ade --- /dev/null +++ b/src/Prey/prey_projectilegasbagpod.cpp @@ -0,0 +1,89 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION(hhProjectile, hhProjectileGasbagPod) + EVENT(EV_Collision_Flesh, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Metal, hhProjectileGasbagPod::Event_Collision_Proj) + EVENT(EV_Collision_AltMetal, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Wood, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Stone, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Glass, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Liquid, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_CardBoard, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Tile, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Forcefield, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Chaff, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Wallwalk, hhProjectileGasbagPod::Event_Collision_SpawnPod) + EVENT(EV_Collision_Pipe, hhProjectileGasbagPod::Event_Collision_SpawnPod) + + EVENT(EV_AllowCollision_Flesh, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Metal, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_AltMetal, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Wood, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Stone, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Glass, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Liquid, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_CardBoard, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Tile, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Forcefield, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Pipe, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Wallwalk, hhProjectileGasbagPod::Event_AllowCollision_Collide) + EVENT(EV_AllowCollision_Spirit, hhProjectileGasbagPod::Event_AllowCollision_PassThru) + EVENT(EV_AllowCollision_Chaff, hhProjectileGasbagPod::Event_AllowCollision_Collide) //bjk: shield blocks all +END_CLASS + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +void hhProjectileGasbagPod::Spawn(void) { + BecomeActive(TH_TICKER); +} + +void hhProjectileGasbagPod::Ticker(void) { + renderEntity.shaderParms[SHADERPARM_ANY_DEFORM_PARM2] += .1f; + if (renderEntity.shaderParms[SHADERPARM_ANY_DEFORM_PARM2] > 1.0f) { + renderEntity.shaderParms[SHADERPARM_ANY_DEFORM_PARM2] = 1.0f; + BecomeInactive(TH_TICKER); + } +} + +void hhProjectileGasbagPod::Event_Collision_SpawnPod(const trace_t* collision, const idVec3 &velocity) { + idVec3 vel = GetPhysics()->GetLinearVelocity(); + idVec3 avel = GetPhysics()->GetAngularVelocity(); + + physicsObj.PutToRest(); + physicsObj.SetContents(0); + PostEventMS(&EV_Remove, 0); + + idDict args; + args.Clear(); + args.Set("origin", (GetPhysics()->GetOrigin()).ToString()); + args.Set("axis", (GetPhysics()->GetAxis()).ToString()); + args.Set("nodrop", "1"); + + idEntity *ent = gameLocal.SpawnObject("object_pod", &args); + + if (ent) { + ent->GetPhysics()->SetLinearVelocity(vel); + ent->GetPhysics()->SetAngularVelocity(avel); + + if (owner.IsValid()) { + owner->PostEventMS(&EV_NewPod, 0, ent); + } + } + + idThread::ReturnInt(1); +} + +void hhProjectileGasbagPod::Event_Collision_Proj(const trace_t* collision, const idVec3 &velocity) { + idEntity *ent = gameLocal.entities[collision->c.entityNum]; + if (ent && ent->IsType(idProjectile::Type)) { + Explode(collision, velocity, 0); + } else { + Event_Collision_SpawnPod(collision, velocity); + } + + idThread::ReturnInt(0); +} + +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilegasbagpod.h b/src/Prey/prey_projectilegasbagpod.h new file mode 100644 index 0000000..83fffbb --- /dev/null +++ b/src/Prey/prey_projectilegasbagpod.h @@ -0,0 +1,20 @@ +#ifndef __HH_PROJECTILE_GASBAGPOD_H +#define __HH_PROJECTILE_GASBAGPOD_H + +class hhProjectileGasbagPod : public hhProjectile { + CLASS_PROTOTYPE(hhProjectileGasbagPod); +#ifdef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build + void Event_Collision_SpawnPod(const trace_t* collision, const idVec3 &velocity) {}; + void Event_Collision_Proj(const trace_t* collision, const idVec3 &velocity) {}; +#else +public: + void Spawn(void); + virtual void Ticker(void); + +protected: + void Event_Collision_SpawnPod(const trace_t* collision, const idVec3 &velocity); + void Event_Collision_Proj(const trace_t* collision, const idVec3 &velocity); +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +}; + +#endif diff --git a/src/Prey/prey_projectilehiderweapon.cpp b/src/Prey/prey_projectilehiderweapon.cpp new file mode 100644 index 0000000..24df8e3 --- /dev/null +++ b/src/Prey/prey_projectilehiderweapon.cpp @@ -0,0 +1,136 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileHider + +***********************************************************************/ +CLASS_DECLARATION( hhProjectile, hhProjectileHider ) +END_CLASS + +void hhProjectileHider::ApplyDamageEffect( idEntity* hitEnt, const trace_t& collision, const idVec3& velocity, const char* damageDefName ) { + //This is used to allow the hider weapon shots to streak when colliding on the ground + if( hitEnt ) { + hitEnt->AddDamageEffect( collision, velocity.ToNormal(), damageDefName, (!fl.networkSync || netSyncPhysics) ); + } +} + +/*********************************************************************** + + hhProjectileHiderCanister + +***********************************************************************/ +const idEventDef EV_CollidedWithChaff( "", "tv", 'd' ); +CLASS_DECLARATION( hhProjectileHider, hhProjectileHiderCanister ) + EVENT( EV_Collision_Flesh, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_AltMetal, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Wood, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Stone, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Glass, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_CardBoard, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Forcefield, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Pipe, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Wallwalk, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_Collision_Tile, hhProjectileHiderCanister::Event_Collision_Explode ) + EVENT( EV_CollidedWithChaff, hhProjectileHiderCanister::Event_Collision_ExplodeChaff ) +END_CLASS + +void hhProjectileHiderCanister::Spawn() { + numSubProjectiles = spawnArgs.GetInt( "numSubProjectiles" ); + const idDict *dict = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_subProjectile"), false ); + if (!dict) { + gameLocal.Error( "No def_subProjectile defined for entity '%s'.\n", GetName() ); + } + subProjectileDict = *dict; + subSpread = DEG2RAD( spawnArgs.GetFloat("spread") ); + subBounce = spawnArgs.GetFloat( "subBounce", "1" ); + bScatter = spawnArgs.GetBool( "subScatter", "0" ); +} + +void hhProjectileHiderCanister::SpawnRicochetSpray( const idVec3& bounceVector ) { + hhProjectile* projectile = NULL; + idVec3 dir; + idMat3 projAxis; + idAngles projAngle; + idMat3 bounceAxis; + + if( !bScatter ) { + bounceAxis = bounceVector.ToNormal().ToMat3(); + subProjectileDict.SetVector( "velocity", idVec3(bounceVector.Length() * subBounce, 0.0f, 0.0f) ); + } + + for( int iIndex = 0; iIndex < numSubProjectiles; ++iIndex ) { + if( bScatter ) { + bounceAxis = hhUtils::RandomVector().ToNormal().ToMat3(); + subProjectileDict.SetVector( "velocity", idVec3(bounceVector.Length() * subBounce, 0.0f, 0.0f) ); + } + dir = hhUtils::RandomSpreadDir( bounceAxis, subSpread ); + projAngle = dir.ToAngles(); + projAngle[2] = GetAxis().ToAngles()[2]; + projAxis = projAngle.ToMat3(); + + //HUMANHEAD rww - now local + //projectile = hhProjectile::SpawnProjectile( &subProjectileDict ); + projectile = hhProjectile::SpawnClientProjectile( &subProjectileDict ); + projectile->spawnArgs.Set( "weapontype", spawnArgs.GetString("weapontype", "NONE1") ); + projectile->Create( owner.GetEntity(), GetOrigin(), bounceAxis ); + projectile->Launch( GetOrigin(), projAxis, vec3_zero ); + projectile->SetParentProjectile( this ); + } +} + +void hhProjectileHiderCanister::SpawnDebris( const idVec3& collisionNormal, const idVec3& collisionDir ) { + //Spawn debris along ground with respect to our current direction + idVec3 dir = collisionDir; + dir.ProjectOntoPlane( -GetPhysics()->GetGravityNormal() ); + dir.Normalize(); + hhProjectileHider::SpawnDebris( dir, collisionDir ); +} + +void hhProjectileHiderCanister::Event_Collision_Explode( const trace_t* collision, const idVec3& velocity ) { + hhProjectile::Event_Collision_Explode( collision, velocity ); + + idEntity *entityHit = gameLocal.entities[ collision->c.entityNum ]; + if (entityHit->IsType(idAI::Type) || entityHit->IsType(idPlayer::Type)) + numSubProjectiles = 0; + + SetOrigin(collision->endpos+collision->c.normal*8.0f); + + //rww - these are purely local now, because they cause bandwidth murder. + SpawnRicochetSpray( hhProjectile::GetBounceDirection(velocity, collision->c.normal, this, NULL) ); + + idThread::ReturnInt( 1 ); +} + +void hhProjectileHiderCanister::Event_Collision_ExplodeChaff( const trace_t* collision, const idVec3& velocity ) { + fl.takedamage = false; + hhProjectile::Event_Collision_Explode( collision, velocity ); + idThread::ReturnInt( 1 ); +} + + +void hhProjectileHiderCanister::Save( idSaveGame *savefile ) const { + savefile->WriteInt( numSubProjectiles ); + savefile->WriteDict( &subProjectileDict ); + savefile->WriteFloat( subSpread ); + savefile->WriteBool( bScatter ); +} + +void hhProjectileHiderCanister::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( numSubProjectiles ); + savefile->ReadDict( &subProjectileDict ); + savefile->ReadFloat( subSpread ); + savefile->ReadBool( bScatter ); + + subBounce = spawnArgs.GetFloat( "subBounce", "1" ); +} + +void hhProjectileHiderCanister::Killed( idEntity *inflicter, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + hhProjectileHider::Killed( inflicter, attacker, damage, dir, location ); + //bScatter = true; + SpawnRicochetSpray( dir * (damage * 10.0f) ); +} diff --git a/src/Prey/prey_projectilehiderweapon.h b/src/Prey/prey_projectilehiderweapon.h new file mode 100644 index 0000000..b9751c1 --- /dev/null +++ b/src/Prey/prey_projectilehiderweapon.h @@ -0,0 +1,45 @@ +#ifndef __HH_PROJECTILE_HIDER_WEAPON_H +#define __HH_PROJECTILE_HIDER_WEAPON_H + +/*********************************************************************** + + hhProjectileHider + +***********************************************************************/ +class hhProjectileHider : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileHider ); + + protected: + virtual void ApplyDamageEffect( idEntity* entHit, const trace_t& collision, const idVec3& velocity, const char* damageDefName ); +}; + +/*********************************************************************** + + hhProjectileHiderCanister + +***********************************************************************/ +class hhProjectileHiderCanister : public hhProjectileHider { + CLASS_PROTOTYPE( hhProjectileHiderCanister ); + + public: + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + virtual void SpawnRicochetSpray( const idVec3& bounceVelocity ); + virtual void SpawnDebris( const idVec3& collisionNormal, const idVec3& collisionDir ); + virtual void Killed( idEntity *inflicter, idEntity *attacker, int damage, const idVec3 &dir, int location ); + + void Event_Collision_Explode( const trace_t* collision, const idVec3& velocity ); + void Event_Collision_ExplodeChaff( const trace_t* collision, const idVec3& velocity ); + + protected: + int numSubProjectiles; + idDict subProjectileDict; + float subSpread; + float subBounce; + bool bScatter; +}; + +#endif diff --git a/src/Prey/prey_projectilemine.cpp b/src/Prey/prey_projectilemine.cpp new file mode 100644 index 0000000..ff88a5a --- /dev/null +++ b/src/Prey/prey_projectilemine.cpp @@ -0,0 +1,505 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhHarvesterMine + +***********************************************************************/ +const idEventDef EV_ApplyAttractionTowards( "applyAttractionTowards", "e" ); + +CLASS_DECLARATION( hhRenderEntity, hhHarvesterMine ) + EVENT( EV_Activate, hhHarvesterMine::Event_Detonate ) + EVENT( EV_ApplyAttractionTowards, hhHarvesterMine::Event_ApplyAttractionTowards ) + EVENT( EV_Explode, hhHarvesterMine::Event_Explode ) +END_CLASS + +/* +================ +hhHarvesterMine::hhHarvesterMine +================ +*/ +hhHarvesterMine::hhHarvesterMine() { + proximityDetonateTrigger = NULL; + proximityAttractionTrigger = NULL; +} + +/* +================ +hhHarvesterMine::~hhHarvesterMine +================ +*/ +hhHarvesterMine::~hhHarvesterMine() { + SAFE_REMOVE( proximityDetonateTrigger ); + SAFE_REMOVE( proximityAttractionTrigger ); +} + +/* +================ +hhHarvesterMine::Spawn +================ +*/ +void hhHarvesterMine::Spawn() { + SpawnTriggers(); +} + +/* +================ +hhHarvesterMine::InitPhysics +================ +*/ +void hhHarvesterMine::InitPhysics( const idVec3& start, const idMat3& axis, const idVec3& pushVelocity ) { + float speed; + float linear_friction; + float contact_friction; + float bounce; + float mass; + float gravity; + idVec3 gravVec; + + speed = spawnArgs.GetVector( "velocity", "0 0 0" ).Length(); + + linear_friction = spawnArgs.GetFloat( "linear_friction" ); + contact_friction = spawnArgs.GetFloat( "contact_friction" ); + bounce = spawnArgs.GetFloat( "bounce" ); + mass = spawnArgs.GetFloat( "mass" ); + gravity = spawnArgs.GetFloat( "gravity" ); + + if ( mass <= 0 ) { + gameLocal.Error( "Invalid mass on '%s'\n", GetClassname() ); + } + + gravVec = gameLocal.GetGravity(); + gravVec.NormalizeFast(); + + physicsObj.SetSelf( this ); + physicsObj.SetClipModel( new idClipModel( GetPhysics()->GetClipModel() ), 1.0f ); + physicsObj.GetClipModel()->SetOwner( DetermineClipModelOwner() ); + physicsObj.SetMass( mass ); + physicsObj.SetFriction( linear_friction, 1.0f, contact_friction ); + + physicsObj.SetBouncyness( bounce ); + physicsObj.SetGravity( gravVec * gravity ); + physicsObj.SetContents( DetermineContents() ); + + physicsObj.SetClipMask( MASK_SHOT_RENDERMODEL ); + physicsObj.SetLinearVelocity( axis[ 0 ] * speed + pushVelocity ); + + physicsObj.SetOrigin( start ); + physicsObj.SetAxis( axis ); + SetPhysics( &physicsObj ); +} + +/* +================ +hhHarvesterMine::SpawnTriggers +================ +*/ +void hhHarvesterMine::SpawnTriggers() { + idDict dict; + + dict.SetVector( "origin", GetOrigin() ); + dict.SetMatrix( "rotation", GetAxis() ); + dict.Set( "target", name.c_str() ); + dict.SetInt( "triggerBehavior", TB_FRIENDLIES_ONLY ); + + dict.SetVector( "mins", spawnArgs.GetVector("detonationMins", "-10 -10 -10") ); + dict.SetVector( "maxs", spawnArgs.GetVector("detonationMaxs", "10 10 10") ); + proximityDetonateTrigger = gameLocal.SpawnObject( spawnArgs.GetString("def_detonateTrigger"), &dict ); + proximityDetonateTrigger->Bind( this, true ); + + dict.SetVector( "mins", spawnArgs.GetVector("attractionMins", "-20 -20 -20") ); + dict.SetVector( "maxs", spawnArgs.GetVector("attractionMaxs", "20 20 20") ); + dict.SetFloat( "refire", spawnArgs.GetFloat("attractionUpdateFrequency") ); + dict.Set( "eventDef", "applyAttractionTowards" ); + proximityAttractionTrigger = gameLocal.SpawnObject( spawnArgs.GetString("def_attractionTrigger"), &dict ); + proximityAttractionTrigger->Bind( this, true ); +} + +/* +================ +hhHarvesterMine::Create +================ +*/ +void hhHarvesterMine::Create( idEntity *owner, const idVec3 &start, const idMat3 &axis ) { + idStr shaderName; + idVec3 light_color; + idVec3 light_offset; + + Unbind(); + + SetOrigin( start ); + SetAxis( axis ); + + this->owner = owner; + + SIMDProcessor->Memset( &renderLight, 0, sizeof( renderLight ) ); + shaderName = spawnArgs.GetString( "mtr_light_shader" ); + if ( *shaderName ) { + renderLight.shader = declManager->FindMaterial( shaderName, false ); + renderLight.pointLight = true; + renderLight.lightRadius = spawnArgs.GetVector( "light_size" ); + spawnArgs.GetVector( "light_color", "1 1 1", light_color ); + renderLight.shaderParms[0] = light_color[0]; + renderLight.shaderParms[1] = light_color[1]; + renderLight.shaderParms[2] = light_color[2]; + renderLight.shaderParms[3] = 1.0f; + } + + spawnArgs.GetVector( "light_offset", "0 0 0", lightOffset ); + + GetPhysics()->SetContents( 0 ); + + state = CREATED; +} + +/* +================= +hhHarvesterMine::Launch +================= +*/ +void hhHarvesterMine::Launch( const idVec3 &start, const idMat3& axis, const idVec3 &pushVelocity ) { + int anim = 0; + + if ( health ) { + fl.takedamage = true; + } + + Unbind(); + + //HUMANHEAD: aob - moved logic to helper function + InitPhysics( start, axis, pushVelocity ); + //HUMANHEAD END + + if ( !gameLocal.isClient ) { + PostEventSec( &EV_Explode, hhMath::hhMax(0.0f, spawnArgs.GetFloat("fuse")) ); + } + + StartSound( "snd_fly", SND_CHANNEL_BODY, 0, true, NULL ); + + state = LAUNCHED; +} + +/* +================ +hhHarvesterMine::Killed +================ +*/ +void hhHarvesterMine::Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ) { + //HUMANHEAD: aob - added collision so we can get explosion to show up when killed + trace_t collision; + + collision.fraction = 0.0f; + collision.endpos = GetOrigin(); + collision.endAxis = GetAxis(); + collision.c.entityNum = attacker->entityNumber; + collision.c.normal = inflictor->GetOrigin() - GetOrigin(); + collision.c.normal.Normalize(); + Explode( &collision ); +} + +/* +================ +hhHarvesterMine::Collide +================ +*/ +bool hhHarvesterMine::Collide( const trace_t& collision, const idVec3& velocity ) { + return false;//Never stop because of collision, just bounce +} + +/* +================ +hhHarvesterMine::DetermineContents +================ +*/ +int hhHarvesterMine::DetermineContents() { + return CONTENTS_SOLID; +} + +/* +================ +hhHarvesterMine::DetermineClipModelOwner +================ +*/ +idEntity* hhHarvesterMine::DetermineClipModelOwner() { + return (spawnArgs.GetBool("collideWithOwner")) ? this : owner.GetEntity(); +} + +/* +================= +hhHarvesterMine::RemoveProjectile +================= +*/ +void hhHarvesterMine::RemoveProjectile( const int removeDelay ) { + Hide(); + RemoveBinds();//Remove any fx we have because they aren't hidden + PostEventMS( &EV_Remove, removeDelay ); +} + +/* +================ +hhHarvesterMine::Explode +================ +*/ +void hhHarvesterMine::Explode( const trace_t *collision ) { + const char *light_shader; + float light_fadetime; + int removeDelay; + trace_t collisionInfo; + + if ( state == EXPLODED || state == FIZZLED ) { + return; + } + + //HUMANHEAD: aob + if( collision && collision->fraction < 1.0f ) { + memcpy( &collisionInfo, collision, sizeof(trace_t) ); + } else { + collisionInfo.fraction = 0.0f; + collisionInfo.endpos = GetOrigin(); + collisionInfo.endAxis = GetAxis(); + collisionInfo.c.entityNum = gameLocal.world->entityNumber; + collisionInfo.c.normal = idVec3(0.0f,0.0f,1.0f); + } + + removeDelay = spawnArgs.GetFloat( "remove_time", "200" ); + //HUMANHEAD END + + // play sound + //HUMANHEAD: aob - in case the sound length is longer than removeTime + int length = 0; + StartSound( "snd_explode", SND_CHANNEL_BODY, 0, true, &length ); + removeDelay = hhMath::hhMax( length, removeDelay ); + //HUMANHEAD END + + FreeLightDef(); + + // explosion light + light_shader = spawnArgs.GetString( "mtr_explode_light_shader" ); + if ( *light_shader ) { + renderLight.shader = declManager->FindMaterial( light_shader, false ); + renderLight.pointLight = true; + renderLight.lightRadius = spawnArgs.GetVector( "explode_light_size" ); + spawnArgs.GetVector( "explode_light_color", "1 1 1", lightColor ); + renderLight.shaderParms[SHADERPARM_RED] = lightColor[0]; + renderLight.shaderParms[SHADERPARM_GREEN] = lightColor[1]; + renderLight.shaderParms[SHADERPARM_BLUE] = lightColor[2]; + renderLight.shaderParms[SHADERPARM_ALPHA] = 1.0f; + renderLight.shaderParms[SHADERPARM_TIMEOFFSET] = -MS2SEC( gameLocal.time ); + light_fadetime = spawnArgs.GetFloat( "explode_light_fadetime" ); + lightStartTime = gameLocal.time; + lightEndTime = gameLocal.time + SEC2MS( light_fadetime ); + BecomeActive( TH_THINK ); + } + + if( !gameLocal.isClient ) { + SpawnCollisionFX( &collisionInfo, "fx_detonate" ); + SpawnDebris( collisionInfo.c.normal, physicsObj.GetLinearVelocity().ToMat3()[0] ); + } + + fl.takedamage = false; + physicsObj.SetContents( 0 ); + physicsObj.PutToRest(); + + state = EXPLODED; + + if ( gameLocal.isClient ) { + return; + } + + // splash damage + idStr splash_damage = spawnArgs.GetString( "def_splash_damage" ); + if ( splash_damage.Length() ) { + gameLocal.RadiusDamage( collisionInfo.endpos, this, owner.GetEntity(), this, this, splash_damage ); + } + + //HUMANHEAD: aob - moved logic to helper function + RemoveProjectile( removeDelay ); + //HUMANHEAD END +} + +/* +================ +hhHarvesterMine::SpawnCollisionFX +================ +*/ +void hhHarvesterMine::SpawnCollisionFX( const trace_t* collision, const char* fxKey ) { + hhFxInfo fxInfo; + + if( !collision || collision->fraction >= 1.0f ) { + return; + } + + fxInfo.SetNormal( collision->c.normal ); + fxInfo.RemoveWhenDone( true ); + + BroadcastFxInfoPrefixedRandom( fxKey, GetOrigin(), GetAxis(), &fxInfo ); +} + +/* +================ +hhHarvesterMine::SpawnDebris +================ +*/ +void hhHarvesterMine::SpawnDebris( const idVec3& collisionNormal, const idVec3& collisionDir ) { + int fxdebris = spawnArgs.GetInt( "debris_count" ); + if( !fxdebris ) { + return; + } + + idDebris *debris = NULL; + idEntity *ent = NULL; + int amount = 0; + const idDict *dict = NULL; + for( const idKeyValue* kv = spawnArgs.MatchPrefix("def_debris", NULL); kv; kv = spawnArgs.MatchPrefix("def_debris", kv) ) { + if( !kv->GetValue().Length() ) { + continue; + } + + dict = gameLocal.FindEntityDefDict( kv->GetValue().c_str(), false ); + if( !dict ) { + continue; + } + + amount = gameLocal.random.RandomInt( fxdebris ); + for ( int i = 0; i < amount; i++ ) { + //HUMANHEAD: aob + idVec3 dir = hhUtils::RandomSpreadDir( collisionNormal.ToMat3(), DEG2RAD(spawnArgs.GetFloat("spread_debris", "10")) ); + //HUMAMHEAD END + + gameLocal.SpawnEntityDef( *dict, &ent ); + if ( !ent || !ent->IsType( idDebris::Type ) ) { + gameLocal.Error( "hhProjectile: 'projectile_debris' is not an idDebris" ); + } + + debris = static_cast(ent); + debris->Create( owner.GetEntity(), GetOrigin(), dir.ToMat3() ); + debris->Launch(); + } + } +} + +/* +================ +hhHarvesterMine::SetGravity +================ +*/ +void hhHarvesterMine::SetGravity( const idVec3 &newGravity ) { + float relativeMagnitude = spawnArgs.GetFloat( "gravity" ); + idVec3 newGravityVector( vec3_zero ); + + if( GetGravity().Compare(newGravity, VECTOR_EPSILON) ) { + return; + } + + if( relativeMagnitude > 0.0f ) { + newGravityVector = newGravity; + relativeMagnitude *= newGravityVector.Normalize() / gameLocal.GetGravity().Length(); + newGravityVector *= relativeMagnitude; + } + + GetPhysics()->SetGravity( newGravityVector ); +} + +/* +================ +hhHarvesterMine::Event_Explode +================ +*/ +void hhHarvesterMine::Event_Explode( void ) { + trace_t collision; + + SIMDProcessor->Memset( &collision, 0, sizeof(trace_t) ); + collision.endpos = GetOrigin(); + collision.endAxis = GetAxis(); + collision.c.entityNum = ENTITYNUM_WORLD; + collision.c.normal = idVec3(0.0f, 0.0f, 1.0f); + Explode( &collision ); +} + +/* +================ +hhHarvesterMine::Event_Detonate +================ +*/ +void hhHarvesterMine::Event_Detonate( idEntity *activator ) { + trace_t collision; + + //Monsters and other harvesters are culled out by the trigger behavior + if( owner != activator ) { + SIMDProcessor->Memset( &collision, 0, sizeof(trace_t) ); + collision.endpos = GetOrigin(); + collision.endAxis = GetAxis(); + if(!activator) { + collision.c.entityNum = ENTITYNUM_WORLD; + collision.c.normal = idVec3(0.0f, 0.0f, 1.0f); + } else { + collision.c.entityNum = activator->entityNumber; + collision.c.normal = GetOrigin() - activator->GetOrigin(); + collision.c.normal.Normalize(); + } + + Explode( &collision ); + } +} + +/* +================ +hhHarvesterMine::Event_ApplyAttractionTowards +================ +*/ +void hhHarvesterMine::Event_ApplyAttractionTowards( idEntity *activator ) { + if( !activator || owner == activator ) { + return; + } + + idVec3 dirToTarget = ((activator->IsType(idActor::Type)) ? static_cast(activator)->GetEyePosition() : activator->GetOrigin()) - GetOrigin(); + dirToTarget.Normalize(); + + ApplyImpulse( this, 0, GetOrigin(), dirToTarget * spawnArgs.GetFloat("attractionMagnitude") ); +} + +/* +================ +hhHarvesterMine::Save +================ +*/ +void hhHarvesterMine::Save( idSaveGame *savefile ) const { + proximityDetonateTrigger.Save( savefile ); + proximityAttractionTrigger.Save( savefile ); + + savefile->WriteStaticObject( physicsObj ); + + owner.Save( savefile ); + + savefile->WriteRenderLight( renderLight ); + savefile->WriteVec3( lightOffset ); + savefile->WriteInt( lightStartTime ); + savefile->WriteInt( lightEndTime ); + savefile->WriteVec3( lightColor ); + savefile->WriteInt( state ); +} + +/* +================ +hhHarvesterMine::Restore +================ +*/ +void hhHarvesterMine::Restore( idRestoreGame *savefile ) { + proximityDetonateTrigger.Restore( savefile ); + proximityAttractionTrigger.Restore( savefile ); + + savefile->ReadStaticObject( physicsObj ); + RestorePhysics( &physicsObj ); + + owner.Restore( savefile ); + + savefile->ReadRenderLight( renderLight ); + savefile->ReadVec3( lightOffset ); + savefile->ReadInt( lightStartTime ); + savefile->ReadInt( lightEndTime ); + savefile->ReadVec3( lightColor ); + savefile->ReadInt( reinterpret_cast ( state ) ); +} diff --git a/src/Prey/prey_projectilemine.h b/src/Prey/prey_projectilemine.h new file mode 100644 index 0000000..690a87c --- /dev/null +++ b/src/Prey/prey_projectilemine.h @@ -0,0 +1,70 @@ +#ifndef __HH_PROJECTILE_MINE_H +#define __HH_PROJECTILE_MINE_H + +/*********************************************************************** + + hhProjectileMine + +***********************************************************************/ +class hhHarvesterMine : public hhRenderEntity { + CLASS_PROTOTYPE( hhHarvesterMine ); + + public: + hhHarvesterMine(); + virtual ~hhHarvesterMine(); + void Spawn(); + + virtual void Create( idEntity *owner, const idVec3 &start, const idMat3 &axis ); + virtual void Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity ); + virtual bool Collide( const trace_t& collision, const idVec3& velocity ); + + virtual void SetGravity( const idVec3 &newGravity ); + + protected: + virtual void SpawnDebris( const idVec3& collisionNormal, const idVec3& collisionDir ); + virtual void RemoveProjectile( const int removeDelay ); + virtual idEntity* DetermineClipModelOwner(); + virtual void SpawnCollisionFX( const trace_t* collision, const char* fxKey ); + + virtual void Explode( const trace_t *collision ); + virtual void Killed( idEntity *inflictor, idEntity *attacker, int damage, const idVec3 &dir, int location ); + + virtual int DetermineContents(); + virtual void InitPhysics( const idVec3& start, const idMat3& axis, const idVec3& pushVelocity ); + void SpawnTriggers(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + void Event_Detonate( idEntity *activator ); + void Event_ApplyAttractionTowards( idEntity *activator ); + void Event_Explode(); + + protected: + idEntityPtr proximityDetonateTrigger; + idEntityPtr proximityAttractionTrigger; + + hhPhysics_RigidBodySimple physicsObj; + + idEntityPtr owner; + + renderLight_t renderLight; + //qhandle_t lightDefHandle; // handle to renderer light def + idVec3 lightOffset; + int lightStartTime; + int lightEndTime; + idVec3 lightColor; + + typedef enum { + SPAWNED = 0, + CREATED = 1, + LAUNCHED = 2, + FIZZLED = 3, + EXPLODED = 4 + } mineState_t; + + mineState_t state; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectilerifle.cpp b/src/Prey/prey_projectilerifle.cpp new file mode 100644 index 0000000..69f63a0 --- /dev/null +++ b/src/Prey/prey_projectilerifle.cpp @@ -0,0 +1,226 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileRifleSniper + +***********************************************************************/ +const idEventDef EV_AttemptFinalExitEventDef( "" ); + +CLASS_DECLARATION( hhProjectile, hhProjectileRifleSniper ) + EVENT( EV_AttemptFinalExitEventDef, hhProjectileRifleSniper::Event_AttemptFinalExitEventDef ) + EVENT( EV_AllowCollision_Flesh, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_Wood, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_Glass, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_Pipe, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_Metal, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_AltMetal, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_Tile, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) + EVENT( EV_AllowCollision_CardBoard, hhProjectileRifleSniper::Event_AllowCollision_PassThru ) +END_CLASS + +/* +================= +hhProjectileRifleSniper::Spawn +================= +*/ +void hhProjectileRifleSniper::Spawn() { + numPassThroughs = 0.0f; + maxPassThroughs = hhMath::hhMax( 1.0f, spawnArgs.GetFloat("maxPassThroughs") ); +} + +/* +================= +hhProjectileRifleSniper::Event_AllowCollision_PassThru +================= +*/ +void hhProjectileRifleSniper::Event_AllowCollision_PassThru( const trace_t* collision ) { + assert( collision ); + + idEntity *entityHit = gameLocal.entities[ collision->c.entityNum ]; + + if (!entityHit->IsType(idAI::Type)) { + //gameLocal.Printf("STOP: [%s]\n", entityHit->GetName()); + hhProjectile::Event_AllowCollision_Collide( collision ); + return; + } + + if( lastDamagedEntity != entityHit ) { + lastDamagedEntity = entityHit; + + if( numPassThroughs < maxPassThroughs ) { + idVec3 myVel = GetPhysics()->GetLinearVelocity(); + idVec3 otherVel = entityHit->GetPhysics()->GetLinearVelocity(); + idVec3 vel = myVel - otherVel; + DamageEntityHit( collision, vel, entityHit ); + + numPassThroughs = hhMath::hhMin( numPassThroughs + 1.0f, maxPassThroughs ); + if( numPassThroughs >= maxPassThroughs ) { + ProcessEvent( &EV_AttemptFinalExitEventDef ); + } + } + } + + //gameLocal.Printf("PASS: [%s]\n", entityHit->GetName()); + hhProjectile::Event_AllowCollision_PassThru( collision ); +} + +/* +================= +hhProjectileRifleSniper::DetermineDamageScale +================= +*/ +float hhProjectileRifleSniper::DetermineDamageScale( const trace_t* collision ) const { + float scale = 1.0f - (numPassThroughs / maxPassThroughs); + + return scale; +} + +/* +================= +hhProjectileRifleSniper::DamageIsValid +================= +*/ +bool hhProjectileRifleSniper::DamageIsValid( const trace_t* collision, float& damageScale ) { + if( numPassThroughs < maxPassThroughs && hhProjectile::DamageIsValid(collision, damageScale) ) { + return true; + } + + RemoveProjectile( 0 ); + return false; +} + +void hhProjectileRifleSniper::Think() { + hhProjectile::Think(); + + // Still check for projectile outside of world, since sometimes that can make it + // through, at which point they stop simulating, but still play sound, have fx_fly, etc. + if( !gameLocal.clip.GetWorldBounds().ContainsPoint(GetOrigin()) ) { + RemoveProjectile( 0 ); + BecomeInactive( TH_ALL ); + } +} + +/* +================= +hhProjectileRifleSniper::Event_AttemptFinalExitEventDef +================= +*/ +void hhProjectileRifleSniper::Event_AttemptFinalExitEventDef() { + //Allow the projectile to do something behind final object hit + + CancelEvents( &EV_AttemptFinalExitEventDef ); + if( lastDamagedEntity.IsValid() && !lastDamagedEntity->GetPhysics()->GetAbsBounds().IntersectsBounds(GetPhysics()->GetAbsBounds()) ) { + CancelEvents( &EV_Remove ); + PostEventMS( GetInvalidDamageEventDef(), 0 ); + return; + } + + PostEventMS( &EV_AttemptFinalExitEventDef, USERCMD_MSEC ); +} + +/* +================= +hhProjectileRifleSniper::DamageEntityHit +================= +*/ +void hhProjectileRifleSniper::DamageEntityHit( const trace_t* collision, const idVec3& velocity, idEntity* entHit ) { + float push = 0.0f; + float damageScale = 1.0f; + const char *damage = spawnArgs.GetString( "def_damage" ); + hhPlayer* playerHit = (entHit->IsType(hhPlayer::Type)) ? static_cast(entHit) : NULL; + idAFEntity_Base* afHit = (entHit->IsType(idAFEntity_Base::Type)) ? static_cast(entHit) : NULL; + + idVec3 dir = velocity.ToNormal(); + + // non-radius damage defs can also apply an additional impulse to the rigid body physics impulse + const idDeclEntityDef *def = gameLocal.FindEntityDef( damage, false ); + if ( def ) { + if (entHit->IsType(hhProjectile::Type)) { + push = 0.0f; // mdl: Don't let projectiles push each other + } else if (afHit && afHit->IsActiveAF() ) { + push = def->dict.GetFloat( "push_ragdoll" ); + } else { + push = def->dict.GetFloat( "push" ); + } + } + + if (!gameLocal.isClient) { //rww + if( playerHit ) { + // pdm: save collision location in case we want to project a blob there + playerHit->playerView.SetDamageLoc( collision->endpos ); + } + + if ( entHit && entHit->IsType( idAI::Type ) ) { + idAI *aiHit = static_cast(entHit); + if ( aiHit && aiHit->InVehicle() ) { + idEntity *killer = owner.GetEntity(); + hhVehicle *vehicle = aiHit->GetVehicleInterface()->GetVehicle(); + if ( vehicle ) { + //use a different damagedef, since we cant affect the damage amount from here directly + damage = spawnArgs.GetString( "def_pilotdamage" ); + vehicle->Damage( this, killer, dir, damage, damageScale, CLIPMODEL_ID_TO_JOINT_HANDLE(collision->c.id) ); + } + } + } + + if( DamageIsValid(collision, damageScale) && entHit->fl.takedamage ) { + UpdateBalanceInfo( collision, entHit ); + + if( damage && damage[0] ) { + idEntity *killer = owner.GetEntity(); + if (killer && killer->IsType(hhVehicle::Type)) { //rww - handle vehicle projectiles killing people + hhVehicle *veh = static_cast(killer); + if (veh->GetPilot()) { + killer = veh->GetPilot(); + } + } + + entHit->Damage( this, killer, dir, damage, damageScale, CLIPMODEL_ID_TO_JOINT_HANDLE(collision->c.id) ); + + if ( playerHit && def->dict.GetInt( "freeze_duration" ) > 0 ) { + playerHit->Freeze( def->dict.GetInt( "freeze_duration" ) ); + } + } + } + + // HUMANHEAD bjk: moved to after damage so impulse can be applied to ragdoll + if ( push > 0.0f ) { + if (g_debugImpulse.GetBool()) { + gameRenderWorld->DebugArrow(colorYellow, collision->c.point, collision->c.point + (push*dir), 25, 2000); + } + + entHit->ApplyImpulse( this, collision->c.id, collision->c.point, push * dir ); + } + } + + if ( entHit->fl.applyDamageEffects ) { + ApplyDamageEffect( entHit, collision, velocity, damage ); + } +} + +/* +================ +hhProjectileRifleSniper::Save +================ +*/ +void hhProjectileRifleSniper::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( numPassThroughs ); + savefile->WriteFloat( maxPassThroughs ); + lastDamagedEntity.Save( savefile ); +} + +/* +================ +hhProjectileRifleSniper::Restore +================ +*/ +void hhProjectileRifleSniper::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( numPassThroughs ); + savefile->ReadFloat( maxPassThroughs ); + lastDamagedEntity.Restore( savefile ); +} + diff --git a/src/Prey/prey_projectilerifle.h b/src/Prey/prey_projectilerifle.h new file mode 100644 index 0000000..ad924a9 --- /dev/null +++ b/src/Prey/prey_projectilerifle.h @@ -0,0 +1,41 @@ +#ifndef __HH_PROJECTILE_RIFLE_SNIPER_H +#define __HH_PROJECTILE_RIFLE_SNIPER_H + +/*********************************************************************** + + hhProjectileRifleSniper + +***********************************************************************/ +extern const idEventDef EV_ApplyExitWound; +extern const idEventDef EV_AttemptFinalExitEventDef; + +class hhProjectileRifleSniper : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileRifleSniper ); + + public: + void Spawn(); + virtual void Think(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + virtual float DetermineDamageScale( const trace_t* collision ) const; + virtual bool DamageIsValid( const trace_t* collision, float& damageScale ); + + virtual const idEventDef* GetInvalidDamageEventDef() const { return &EV_Fizzle; } + void DamageEntityHit( const trace_t* collision, const idVec3& velocity, idEntity* entHit ); + + protected: + void Event_AttemptFinalExitEventDef(); + + void Event_AllowCollision_PassThru( const trace_t* collision ); + + protected: + //Used floats so we can divide without casting + float numPassThroughs; + float maxPassThroughs; + idEntityPtr lastDamagedEntity; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectilerocketlauncher.cpp b/src/Prey/prey_projectilerocketlauncher.cpp new file mode 100644 index 0000000..4cf4e80 --- /dev/null +++ b/src/Prey/prey_projectilerocketlauncher.cpp @@ -0,0 +1,323 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileRocketLauncher + +***********************************************************************/ +const idEventDef EV_SpawnModelProxyLocal( "" ); + +CLASS_DECLARATION( hhProjectile, hhProjectileRocketLauncher ) + EVENT( EV_SpawnModelProxyLocal, hhProjectileRocketLauncher::Event_SpawnModelProxyLocal ) + EVENT( EV_SpawnFxFlyLocal, hhProjectileRocketLauncher::Event_SpawnFxFlyLocal ) + + EVENT( EV_Collision_Flesh, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_AltMetal, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Wood, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Stone, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Glass, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Liquid, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_CardBoard, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Tile, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Forcefield, hhProjectileRocketLauncher::Event_Collision_Explode ) + //EVENT( EV_Collision_Chaff, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Pipe, hhProjectileRocketLauncher::Event_Collision_Explode ) + EVENT( EV_Collision_Wallwalk, hhProjectileRocketLauncher::Event_Collision_Explode ) + + EVENT( EV_AllowCollision_Chaff, hhProjectileRocketLauncher::Event_AllowCollision_Collide ) +END_CLASS + +/* +================ +hhProjectileRocketLauncher::Spawn +================ +*/ +void hhProjectileRocketLauncher::Spawn() { +} + +/* +================ +hhProjectileRocketLauncher::~hhProjectileRocketLauncher +================ +*/ +hhProjectileRocketLauncher::~hhProjectileRocketLauncher() { + SAFE_REMOVE( modelProxy ); +} + +/* +================= +hhProjectileRocketLauncher::Hide +================= +*/ +void hhProjectileRocketLauncher::Hide() { + hhProjectile::Hide(); + + if( modelProxy.IsValid() ) { + modelProxy->Hide(); + } +} + +/* +================= +hhProjectileRocketLauncher::Show +================= +*/ +void hhProjectileRocketLauncher::Show() { + hhProjectile::Show(); + + if( modelProxy.IsValid() ) { + modelProxy->Show(); + } +} + +/* +================= +hhProjectileRocketLauncher::RemoveProjectile +================= +*/ +void hhProjectileRocketLauncher::RemoveProjectile( const int removeDelay ) { + hhProjectile::RemoveProjectile( removeDelay ); + + if( modelProxy.IsValid() ) { + modelProxy->PostEventMS( &EV_Remove, removeDelay ); + } +} + +/* +================ +hhProjectileRocketLauncher::Launch +================ +*/ +void hhProjectileRocketLauncher::Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire, const float launchPower, const float dmgPower ) { + hhFxInfo fxInfo; + + ProcessEvent( &EV_SpawnModelProxyLocal ); //rww - was using BroadcastEventDef + + hhProjectile::Launch( start, axis, pushVelocity, timeSinceFire, launchPower, dmgPower ); + + if( modelProxy.IsValid() ) { + fxInfo.SetEntity( this ); + modelProxy->BroadcastFxInfoAlongBonePrefix( &spawnArgs, "fx_blood", "joint_bloodFx", false ); //rww - don't broadcast + } +} + +/* +================ +hhProjectileRocketLauncher::Event_SpawnFxFlyLocal +================ +*/ +void hhProjectileRocketLauncher::Event_SpawnFxFlyLocal( const char* defName ) { + hhFxInfo fxInfo; + + if( modelProxy.IsValid() ) { + fxInfo.SetEntity( this ); + modelProxy->SpawnFxAlongBonePrefixLocal( &spawnArgs, "fx_fly", "joint_flyFx", &fxInfo ); + } +} + +/* +================= +hhProjectileRocketLauncher::Event_SpawnModelProxyLocal +================= +*/ +void hhProjectileRocketLauncher::Event_SpawnModelProxyLocal() { + idDict args = spawnArgs; + + static const idMat3 pitchedOverAxis( idAngles(-90.0f, 0.0f, 0.0f).ToMat3() ); + + args.Delete( "spawnclass" ); + args.Delete( "name" ); + args.Delete( "spawn_entnum" ); //HUMANHEAD rww - yeah, might not be smart to try to spawn in the same entity slot. + + args.Set( "owner", GetName() ); + args.SetVector( "origin", GetOrigin() ); + args.SetMatrix( "rotation", pitchedOverAxis * GetAxis() ); + args.SetBool( "transferDamage", false ); + args.SetBool( "solid", false ); + if (!modelProxy.IsValid()) { + modelProxy = gameLocal.SpawnEntityTypeClient( hhGenericAnimatedPart::Type, &args ); //rww - proxy now localized + if( modelProxy.IsValid() ) { + modelProxy->fl.networkSync = false; + modelProxy->Bind( this, true ); + modelProxy->CycleAnim( "idle", ANIMCHANNEL_ALL ); + } + } + + //Get rid of our model. The modelProxy is our model now. + if (!gameLocal.isClient) { + SetModel( "" ); + } +} + +/* +================ +hhProjectileRocketLauncher::Save +================ +*/ +void hhProjectileRocketLauncher::Save( idSaveGame *savefile ) const { + modelProxy.Save( savefile ); +} + +/* +================ +hhProjectileRocketLauncher::Restore +================ +*/ +void hhProjectileRocketLauncher::Restore( idRestoreGame *savefile ) { + modelProxy.Restore( savefile ); +} + +void hhProjectileRocketLauncher::ClientPredictionThink( void ) { + if (!gameLocal.isNewFrame) { //HUMANHEAD rww + return; + } + + //rww - this code is duplicated here since the rocket projectile on the client is a little special-cased + // HUMANHEAD: cjr - if this projectile recently struck a portal, then attempt to portal it + if ( (thinkFlags & TH_MISC1) && collidedPortal.IsValid() ) { + GetPhysics()->SetLinearVelocity( collideVelocity ); + collidedPortal->PortalProjectile( this, collideLocation, collideLocation + collideVelocity ); + collidedPortal = NULL; + collideLocation = vec3_origin; + collideVelocity = vec3_origin; + BecomeInactive(TH_MISC1); + } + // HUMANHEAD END + + RunPhysics(); + + if ( thinkFlags & TH_MISC2 ) { + UpdateLight(); + } +} + +/* +================ +hhProjectileRocketLauncher::Event_AllowCollision_Collide +================ +*/ +void hhProjectileRocketLauncher::Event_AllowCollision_Collide( const trace_t* collision ) { + idThread::ReturnInt( 1 ); +} + +/*********************************************************************** + + hhProjectileChaff + +***********************************************************************/ +const idEventDef EV_CollidedWithChaff( "", "tv", 'd' ); +CLASS_DECLARATION( hhProjectile, hhProjectileChaff ) + EVENT( EV_Touch, hhProjectileChaff::Event_Touch ) + //FIXME: null out all of the events +END_CLASS + +/* +================ +hhProjectileChaff::Spawn +================ +*/ +void hhProjectileChaff::Spawn() { + decelStart = SEC2MS( spawnArgs.GetFloat("decelStart") ) + gameLocal.GetTime(); + decelEnd = SEC2MS( spawnArgs.GetFloat("decelDuration") ) + decelStart; + + BecomeActive( TH_TICKER ); +} + +/* +================ +hhProjectileChaff::Launch +================ +*/ +void hhProjectileChaff::Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire, const float launchPower, const float dmgPower ) { + hhProjectile::Launch( start, axis, pushVelocity, timeSinceFire, launchPower, dmgPower ); + + cachedVelocity = GetPhysics()->GetLinearVelocity(); + + fl.takedamage = false; + physicsObj.DisableImpact(); +} + +/* +================ +hhProjectileChaff::Collide +================ +*/ +bool hhProjectileChaff::Collide( const trace_t& collision, const idVec3& velocity ) { + // Let the target know + idEntity *ent = gameLocal.GetTraceEntity( collision ); + if ( ent && ent->RespondsTo( EV_CollidedWithChaff ) ) { + ent->PostEventMS( &EV_CollidedWithChaff, 0, &collision, velocity ); + } + + return true;//Always stop after collision +} + +/* +================ +hhProjectileChaff::DetermineContents +================ +*/ +int hhProjectileChaff::DetermineContents() { + // Removed PROJECTILE + return CONTENTS_BLOCK_RADIUSDAMAGE | CONTENTS_OWNER_TO_OWNER | CONTENTS_SHOOTABLE; +} + +/* +================ +hhProjectileChaff::DetermineClipmask +================ +*/ +int hhProjectileChaff::DetermineClipmask() { + // Removed SHOOTABLE, added PROJECTILE + return CONTENTS_PROJECTILE|CONTENTS_SOLID|CONTENTS_RENDERMODEL|CONTENTS_CORPSE|CONTENTS_WATER|CONTENTS_FORCEFIELD; +} + + +/* +================ +hhProjectileChaff::Ticker +================ +*/ +void hhProjectileChaff::Ticker() { + float scale = 0.0f; + if( gameLocal.GetTime() > decelStart && gameLocal.GetTime() < decelEnd ) { + scale = hhMath::Sin( DEG2RAD(hhMath::MidPointLerp( 0.0f, 30.0f, 90.0f, 1.0f - hhUtils::CalculateScale(gameLocal.GetTime(), decelStart, decelEnd))) ); + + GetPhysics()->SetLinearVelocity( cachedVelocity * hhMath::ClampFloat(0.05f, 1.0f, scale) ); + } +} + +/* +================ +hhProjectileChaff::Event_Touch +================ +*/ +void hhProjectileChaff::Event_Touch( idEntity *other, trace_t *trace ) { + //Supposed to be empty +} + +/* +================ +hhProjectileChaff::Save +================ +*/ +void hhProjectileChaff::Save( idSaveGame *savefile ) const { + savefile->WriteInt( decelStart ); + savefile->WriteInt( decelEnd ); + savefile->WriteVec3( cachedVelocity ); +} + +/* +================ +hhProjectileChaff::Restore +================ +*/ +void hhProjectileChaff::Restore( idRestoreGame *savefile ) { + savefile->ReadInt( decelStart ); + savefile->ReadInt( decelEnd ); + savefile->ReadVec3( cachedVelocity ); +} diff --git a/src/Prey/prey_projectilerocketlauncher.h b/src/Prey/prey_projectilerocketlauncher.h new file mode 100644 index 0000000..70a6a9b --- /dev/null +++ b/src/Prey/prey_projectilerocketlauncher.h @@ -0,0 +1,70 @@ +#ifndef __HH_PROJECTILE_ROCKET_LAUNCHER_H +#define __HH_PROJECTILE_ROCKET_LAUNCHER_H + +/*********************************************************************** + + hhProjectileRocketLauncher + +***********************************************************************/ +class hhProjectileRocketLauncher : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileRocketLauncher ); + + public: + void Spawn(); + virtual ~hhProjectileRocketLauncher(); + + virtual void Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire = 0.0f, const float launchPower = 1.0f, const float dmgPower = 1.0f ); + + virtual void Hide(); + virtual void Show(); + virtual void RemoveProjectile( const int removeDelay ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual void ClientPredictionThink( void ); + + protected: + void Event_SpawnModelProxyLocal(); + void Event_SpawnFxFlyLocal( const char* defName ); + + void Event_AllowCollision_Collide( const trace_t* collision ); + + protected: + idEntityPtr modelProxy; +}; + +/*********************************************************************** + + hhProjectileChaff + +***********************************************************************/ +class hhProjectileChaff : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileChaff ); + + public: + void Spawn(); + + virtual void Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire = 0.0f, const float launchPower = 1.0f, const float dmgPower = 1.0f ); + virtual bool Collide( const trace_t& collision, const idVec3& velocity ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + virtual void Ticker(); + + virtual int DetermineContents(); + virtual int DetermineClipmask(); + + protected: + void Event_Touch( idEntity *other, trace_t *trace ); + + protected: + int decelStart; + int decelEnd; + + idVec3 cachedVelocity; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectileshuttle.cpp b/src/Prey/prey_projectileshuttle.cpp new file mode 100644 index 0000000..3e51272 --- /dev/null +++ b/src/Prey/prey_projectileshuttle.cpp @@ -0,0 +1,22 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhProjectile, hhProjectileShuttle ) + EVENT( EV_Collision_Flesh, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_AltMetal, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Wood, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Stone, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Glass, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Liquid, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_CardBoard, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Tile, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Forcefield, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Chaff, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Pipe, hhProjectileShuttle::Event_Collision_Explode ) + EVENT( EV_Collision_Wallwalk, hhProjectileShuttle::Event_Collision_Explode ) + + EVENT( EV_AllowCollision_Chaff, hhProjectileShuttle::Event_AllowCollision_Collide ) +END_CLASS \ No newline at end of file diff --git a/src/Prey/prey_projectileshuttle.h b/src/Prey/prey_projectileshuttle.h new file mode 100644 index 0000000..4165fad --- /dev/null +++ b/src/Prey/prey_projectileshuttle.h @@ -0,0 +1,8 @@ +#ifndef __HH_PROJECTILE_SHUTTLE_H +#define __HH_PROJECTILE_SHUTTLE_H + +class hhProjectileShuttle : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileShuttle ) +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectilesoulcannon.cpp b/src/Prey/prey_projectilesoulcannon.cpp new file mode 100644 index 0000000..5f96598 --- /dev/null +++ b/src/Prey/prey_projectilesoulcannon.cpp @@ -0,0 +1,143 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileSoulCannon + +***********************************************************************/ + +const idEventDef EV_FindSoulEnemy( "", NULL ); + +CLASS_DECLARATION( hhProjectile, hhProjectileSoulCannon ) + EVENT( EV_FindSoulEnemy, hhProjectileSoulCannon::Event_FindEnemy ) +END_CLASS + +//============================================================================= +// +// hhProjectileSoulCannon::Spawn +// +//============================================================================= + +void hhProjectileSoulCannon::Spawn() { + hhProjectile::Spawn(); + + BecomeActive( TH_THINK ); + + PostEventSec( &EV_FindSoulEnemy, 0.5f ); // Find an enemy shortly after being launched + + maxVelocity = spawnArgs.GetFloat( "maxVelocity", "400" ); + maxEnemyDist = spawnArgs.GetFloat( "maxEnemyDist", "4096" ); + + thrustDir = vec3_origin; +} + +//============================================================================= +// +// hhProjectileSoulCannon::Think +// +//============================================================================= + +void hhProjectileSoulCannon::Think( void ) { + // run physics + RunPhysics(); + + // Thrust toward enemy + if ( thinkFlags & TH_THINK && thrustDir != vec3_origin ) { + idVec3 vel = GetPhysics()->GetLinearVelocity(); + vel += thrustDir * spawnArgs.GetFloat( "soulThrust", "5.0" ); + + if ( vel.Length() > maxVelocity ) { // Cap the velocity + vel.Normalize(); + vel *= maxVelocity; + } + + GetPhysics()->SetLinearVelocity( vel ); + GetPhysics()->SetAxis( vel.ToMat3() ); + } + + //HUMANHEAD: aob + if (thinkFlags & TH_TICKER) { + Ticker(); + } + //HUMANHEAD + + Present(); +} + +//============================================================================= +// +// hhProjectileSoulCannon::Event_FindEnemy +// +// Finds a new enemy every second +//============================================================================= + +void hhProjectileSoulCannon::Event_FindEnemy( void ) { + int i; + idEntity *ent; + idActor *actor; + trace_t tr; + + PostEventSec( &EV_FindSoulEnemy, 1.0f ); + + for ( i = 0; i < gameLocal.num_entities; i++ ) { + ent = gameLocal.entities[ i ]; + + if ( !ent || !ent->IsType( idActor::Type ) ) { + continue; + } + + if ( ent == this || ent == this->GetOwner() ) { + continue; + } + + // Ignore dormant entities! + if( ent->IsHidden() || ent->fl.isDormant ) { // HUMANHEAD JRM - changed to fl.isDormant + continue; + } + + actor = static_cast( ent ); + if ( ( actor->health <= 0 ) ) { + continue; + } + + idVec3 center = actor->GetPhysics()->GetAbsBounds().GetCenter(); + + // Cannot see this enemy because he is too far away + if ( maxEnemyDist > 0.0f && ( center - GetOrigin() ).LengthSqr() > maxEnemyDist * maxEnemyDist ) + continue; + + // Quick trace to the center of the potential enemy + if ( !gameLocal.clip.TracePoint( tr, GetOrigin(), center, 1.0f, this ) ) { + thrustDir = center - GetOrigin(); + thrustDir.Normalize(); + break; + } + } +} + +/* +================ +hhProjectileSoulCannon::Save +================ +*/ +void hhProjectileSoulCannon::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( maxVelocity ); + savefile->WriteFloat( maxEnemyDist ); + savefile->WriteVec3( thrustDir ); +} + +/* +================ +hhProjectileSoulCannon::Restore +================ +*/ +void hhProjectileSoulCannon::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( maxVelocity ); + savefile->ReadFloat( maxEnemyDist ); + savefile->ReadVec3( thrustDir ); +} +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilesoulcannon.h b/src/Prey/prey_projectilesoulcannon.h new file mode 100644 index 0000000..f52e2be --- /dev/null +++ b/src/Prey/prey_projectilesoulcannon.h @@ -0,0 +1,30 @@ +#ifndef ID_DEMO_BUILD //HUMANHEAD jsh PCF 5/26/06: code removed for demo build +#ifndef __HH_PROJECTILE_SOUL_CANNON_H +#define __HH_PROJECTILE_SOUL_CANNON_H + +/*********************************************************************** + + hhProjectileSoulCannon + +***********************************************************************/ + +class hhProjectileSoulCannon : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileSoulCannon ); + + public: + void Spawn(); + void Think( void ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + float maxVelocity; + float maxEnemyDist; + idVec3 thrustDir; + + void hhProjectileSoulCannon::Event_FindEnemy( void ); +}; + +#endif +#endif //HUMANHEAD jsh PCF 5/26/06: code removed for demo build \ No newline at end of file diff --git a/src/Prey/prey_projectilespiritarrow.cpp b/src/Prey/prey_projectilespiritarrow.cpp new file mode 100644 index 0000000..7ad78b0 --- /dev/null +++ b/src/Prey/prey_projectilespiritarrow.cpp @@ -0,0 +1,105 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileSpiritArrow + +***********************************************************************/ +CLASS_DECLARATION( hhProjectile, hhProjectileSpiritArrow ) + EVENT( EV_AllowCollision_Spirit, hhProjectile::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Chaff, hhProjectile::Event_AllowCollision_PassThru ) + + EVENT( EV_AllowCollision_Flesh, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Metal, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_AltMetal, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Wood, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Stone, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Glass, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Pipe, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_Tile, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + EVENT( EV_AllowCollision_CardBoard, hhProjectileSpiritArrow::Event_AllowCollision_Collide ) + + EVENT( EV_Collision_Flesh, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_Metal, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_AltMetal, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_Wood, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_Stone, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_Glass, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_Wallwalk, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_Pipe, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_CardBoard, hhProjectileSpiritArrow::Event_Collision_Stick ) + EVENT( EV_Collision_Spirit, hhProjectileSpiritArrow::Event_Collision_Impact ) + EVENT( EV_Collision_Chaff, hhProjectileSpiritArrow::Event_Collision_Impact ) +END_CLASS + + +int hhProjectileSpiritArrow::DetermineClipmask() { + return MASK_SPIRITARROW; +} + +/* +================= +hhProjectileSpiritArrow::BindToCollisionObject +================= +*/ +void hhProjectileSpiritArrow::BindToCollisionObject( const trace_t* collision ) { + if( !collision || collision->fraction == 1.0f ) { + return; + } + + idEntity* pEntity = gameLocal.entities[collision->c.entityNum]; + HH_ASSERT( pEntity ); + + if( !pEntity->fl.applyDamageEffects ) { + PostEventMS( &EV_Fizzle, 0 ); + return; + } + + if ( pEntity->spawnArgs.GetBool( "no_arrow_stick" ) ) { + RemoveProjectile( 0 ); + return; + } + + jointHandle_t jointHandle = CLIPMODEL_ID_TO_JOINT_HANDLE( collision->c.id ); + idVec3 penetrationVector( collision->endAxis[0] * spawnArgs.GetFloat("penetrationDepth", "10") ); // Push the arrow into the hit object a bit + if ( jointHandle != INVALID_JOINT ) { + SetOrigin( collision->endpos + penetrationVector ); + BindToJoint( pEntity, jointHandle, true ); + } else { + SetOrigin( collision->endpos + penetrationVector ); + Bind( pEntity, true ); + } +} + +/* +================= +hhProjectileSpiritArrow::Event_Collision_Stick +================= +*/ +void hhProjectileSpiritArrow::Event_Collision_Stick( const trace_t* collision, const idVec3 &velocity ) { + + ProcessCollision( collision, velocity );//Assuming that EV_Fizzle is canceled in ProcessCollision + + if (gameLocal.isMultiplayer) { //rww - in mp we don't stick to players. + idEntity *pEntity = gameLocal.entities[collision->c.entityNum]; + if (pEntity && pEntity->IsType(hhPlayer::Type)) { + PostEventMS( &EV_Fizzle, 0 ); + + idThread::ReturnInt( 1 ); + return; + } + } + + PostEventMS( &EV_Fizzle, spawnArgs.GetInt("remove_time") ); + + BindToCollisionObject( collision ); + + fl.ignoreGravityZones = true; + SetGravity( idVec3(0.f, 0.f, 0.f) ); + spawnArgs.SetVector("gravity", idVec3(0.f, 0.f, 0.f) ); + + idThread::ReturnInt( 1 ); +} \ No newline at end of file diff --git a/src/Prey/prey_projectilespiritarrow.h b/src/Prey/prey_projectilespiritarrow.h new file mode 100644 index 0000000..5de2b73 --- /dev/null +++ b/src/Prey/prey_projectilespiritarrow.h @@ -0,0 +1,20 @@ +#ifndef __HH_PROJECTILE_SPIRITARROW_H +#define __HH_PROJECTILE_SPIRITARROW_H + +/*********************************************************************** + + hhProjectileSpiritArrow + +***********************************************************************/ +class hhProjectileSpiritArrow: public hhProjectile { + CLASS_PROTOTYPE( hhProjectileSpiritArrow ); + + protected: + virtual int DetermineClipmask(); + void BindToCollisionObject( const trace_t* collision ); + + protected: + void Event_Collision_Stick( const trace_t* collision, const idVec3 &velocity ); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectiletracking.cpp b/src/Prey/prey_projectiletracking.cpp new file mode 100644 index 0000000..cdbcaca --- /dev/null +++ b/src/Prey/prey_projectiletracking.cpp @@ -0,0 +1,330 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhProjectileTracking + +***********************************************************************/ +const idEventDef EV_Guide( "" ); +const idEventDef EV_Hover( "" ); +const idEventDef EV_StartTracking( "" ); +const idEventDef EV_StopTracking( "" ); + +CLASS_DECLARATION( hhProjectile, hhProjectileTracking ) + EVENT( EV_Guide, hhProjectileTracking::Event_TrackTarget ) + EVENT( EV_Hover, hhProjectileTracking::Event_Hover ) + EVENT( EV_StartTracking, hhProjectileTracking::Event_StartTracking ) + EVENT( EV_StopTracking, hhProjectileTracking::Event_StopTracking ) + + EVENT( EV_Collision_Flesh, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_AltMetal, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Wood, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Stone, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Glass, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Liquid, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_CardBoard, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Tile, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Forcefield, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Chaff, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Wallwalk, hhProjectileTracking::Event_Collision_Explode ) + EVENT( EV_Collision_Pipe, hhProjectileTracking::Event_Collision_Explode ) + + EVENT( EV_AllowCollision_Chaff, hhProjectileTracking::Event_AllowCollision_Collide ) +END_CLASS + + + +hhProjectileTracking::hhProjectileTracking() { + spinAngle = 0.0f; // Needs to be initialized before spawn since GetPhysicsToVisualTransform() is called before spawn +} + +/* +================ +hhProjectileTracking::Spawn +================ +*/ +void hhProjectileTracking::Spawn() { + angularVelocity.Zero(); + velocity.Zero(); + + cachedFovCos = idMath::Cos( DEG2RAD(spawnArgs.GetFloat("fov", "90")) ); + turnFactor = spawnArgs.GetFloat("turnfactor"); + updateRate = spawnArgs.GetInt("trackingUpdateRate"); + turnFactorAcc = spawnArgs.GetFloat( "turn_factor_accel", "1.1" ); + spinDelta = spawnArgs.GetFloat( "spin_delta" ); + + //rww - some things are hhProjectileTracking when they shouldn't be or don't have proper properties. if this is so, complain. + if (!updateRate) { + gameLocal.Warning("hhProjectileTracking with an updateRate of 0 (possible infinite event queue)."); + } +} + +/* +================ +hhProjectileTracking::IsEnemyValid +================ +*/ +bool hhProjectileTracking::EnemyIsValid( idEntity* ent ) const { + if ( ent && ent->IsType( idActor::Type ) ) { + idActor *entActor = static_cast(ent); + return ( entActor->GetHealth() > 0 && entActor->team!= 0 && !entActor->IsHidden() && gameLocal.InPlayerPVS(entActor) ); + } + return ( ent && ent->GetHealth() > 0 && !ent->IsHidden() && gameLocal.InPlayerPVS(ent) ); +} + +/* +================ +hhProjectileTracking::WhosClosest +================ +*/ +idEntity* hhProjectileTracking::WhosClosest( idEntity* possibleEnemy, idEntity* currentEnemy, float& currentEnemyDot, float& currentEnemyDist ) const { + if( !possibleEnemy ) { + return currentEnemy; + } + + idVec3 enemyDir = DetermineEnemyPosition( possibleEnemy ) - GetOrigin(); + float cachedDist = enemyDir.Normalize(); + float cachedDot = enemyDir * GetAxis()[0]; + + //AOB: Think about putting in tolerances for deciding which is better, + if( cachedDot > cachedFovCos && cachedDot > currentEnemyDot && cachedDist <= currentEnemyDist ) { + currentEnemyDot = cachedDot; + currentEnemyDist = cachedDist; + return possibleEnemy; + } + + return currentEnemy; +} + +/* +================ +hhProjectileTracking::DetermineEnemy +================ +*/ +idEntity* hhProjectileTracking::DetermineEnemy() { + idEntity* possibleEnemy = NULL; + float currentEnemyDot = 0.0f; + float currentEnemyDist = CM_MAX_TRACE_DIST; + idEntity* localEnemy = NULL; + + if ( spawnArgs.GetInt( "trackPlayersOnly", "0" ) ) { + for( int i = 0; i < gameLocal.numClients; i++ ) { + if ( gameLocal.entities[ i ] ) { + possibleEnemy = gameLocal.entities[i]; + if ( possibleEnemy && possibleEnemy->IsType( hhPlayer::Type ) ) { + hhPlayer *player = static_cast(possibleEnemy); + if ( player && player->IsSpiritOrDeathwalking() ) { + possibleEnemy = player->GetSpiritProxy(); + } + } + localEnemy = WhosClosest( possibleEnemy, localEnemy, currentEnemyDot, currentEnemyDist ); + } + } + + return localEnemy; + } + int num = hhMonsterAI::allSimpleMonsters.Num(); + for( int index = 0; index < num; ++index ) { + possibleEnemy = hhMonsterAI::allSimpleMonsters[ index ]; + if( EnemyIsValid(possibleEnemy) ) { + localEnemy = WhosClosest( possibleEnemy, localEnemy, currentEnemyDot, currentEnemyDist ); + } + } + + return localEnemy; +} + +/* +================ +hhProjectileTracking::Launch +================ +*/ +void hhProjectileTracking::Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire, const float launchPower, const float dmgPower ) { + hhProjectile::Launch( start, axis, pushVelocity, timeSinceFire, launchPower, dmgPower ); + + enemy = DetermineEnemy(); + + float randomStartSpread = spawnArgs.GetFloat( "randomStartSpread", "0" ); + if ( randomStartSpread > 0.0 ) { + velocity = spawnArgs.GetVector( "velocity" ).Length() * hhUtils::RandomSpreadDir( GetPhysics()->GetAxis(), 1.0 ); + } else { + velocity = spawnArgs.GetVector( "velocity" ); + } + angularVelocity = spawnArgs.GetAngles( "angular_velocity" ); + float trackDelay = spawnArgs.GetFloat( "trackDelay", "0" ); + if( trackDelay > 0.0 ) { + PostEventSec( &EV_Hover, spawnArgs.GetFloat( "trackStop", "0" ) ); + PostEventSec( &EV_StartTracking, trackDelay ); + } else if ( enemy.IsValid() ) { + PostEventMS( &EV_Guide, updateRate ); + } + float trackDuration = spawnArgs.GetFloat( "trackDuration", "0" ); + if ( trackDuration > 0.0 ) { + PostEventSec( &EV_StopTracking, trackDuration ); + } +} + +void hhProjectileTracking::Event_Hover() { + physicsObj.SetLinearVelocity( velocity * spawnArgs.GetFloat( "hoverScale", "0.15" ) ); +} + +void hhProjectileTracking::Event_StartTracking() { + float randomStartSpread = spawnArgs.GetFloat( "randomStartSpread", "0" ); + if ( randomStartSpread > 0.0 ) { + velocity = spawnArgs.GetVector( "velocity" ); + velocity.y = gameLocal.random.CRandomFloat() * randomStartSpread; + velocity.z = gameLocal.random.CRandomFloat() * randomStartSpread; + } + PostEventMS( &EV_Guide, updateRate ); +} + +void hhProjectileTracking::Event_StopTracking() { + CancelEvents(&EV_Guide); +} + +/* +================ +hhProjectileTracking::DetermineEnemyPosition +================ +*/ +idVec3 hhProjectileTracking::DetermineEnemyPosition( const idEntity* ent ) const { + if ( ent && ent->IsType( idActor::Type ) ) { + const idActor *entActor = static_cast(ent); + return entActor->GetEyePosition(); + } + + return ent->GetOrigin(); +} + +/* +================ +hhProjectileTracking::DetermineEnemyDir +================ +*/ +idVec3 hhProjectileTracking::DetermineEnemyDir( const idEntity* actor ) const { + idVec3 enemyPos = DetermineEnemyPosition( actor ); + idVec3 enemyDir = enemyPos - GetOrigin(); + enemyDir.Normalize(); + + return enemyDir; +} + +/* +================ +hhProjectileTracking::Explode +================ +*/ +void hhProjectileTracking::Explode( const trace_t *collision, const idVec3& velocity, int removeDelay ) { + hhProjectile::Explode( collision, velocity, removeDelay ); + CancelEvents(&EV_Guide); +} + +/* +================ +hhProjectileTracking::GetPhysicsToVisualTransform +================ +*/ +bool hhProjectileTracking::GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ) { + if ( spinDelta != 0.0f ) { + axis = idAngles(0,0,spinAngle).ToMat3(); + spinAngle += spinDelta; + if ( spinAngle > 360.0f ) { + spinAngle = 0.0f; + } + return true; + } + + return hhProjectile::GetPhysicsToVisualTransform( origin, axis ); +} + +/* +================ +hhProjectileTracking::Event_TrackTarget +================ +*/ +void hhProjectileTracking::Event_TrackTarget() { + if ( !spawnArgs.GetInt( "constantEnemy", "1" ) ) { + enemy = DetermineEnemy(); + } + + if( !enemy.IsValid() ) { + physicsObj.SetLinearVelocity( GetAxis()[ 0 ] * velocity[ 0 ] + GetAxis()[ 1 ] * velocity[ 1 ] + GetAxis()[ 2 ] * velocity[ 2 ] ); + physicsObj.SetAngularVelocity( angularVelocity.ToAngularVelocity() * GetAxis() ); + return; + } + + if( !enemy->GetHealth() ) { + //if enemy is dead, just explode + trace_t collision; + memset( &collision, 0, sizeof( collision ) ); + collision.endAxis = GetPhysics()->GetAxis(); + collision.endpos = GetPhysics()->GetOrigin(); + collision.c.point = GetPhysics()->GetOrigin(); + collision.c.normal.Set( 0, 0, 1 ); + Explode( &collision, idVec3(0,0,0), 3 ); + } + + if ( turnFactor < 1.0 && turnFactorAcc > 1.0 ) { + turnFactor *= turnFactorAcc; // Accelerate the turn as time goes on so we don't get stuck in any orbits + } + idVec3 enemyDir = DetermineEnemyDir( enemy.GetEntity() ); + idVec3 currentDir = GetAxis()[0]; + idVec3 newDir = currentDir*(1-turnFactor) + enemyDir*turnFactor; + newDir.Normalize(); + if ( driver.IsValid() ) { + driver->SetAxis(newDir.ToMat3()); + } else { + SetAxis(newDir.ToMat3()); + } + + physicsObj.SetLinearVelocity( GetAxis()[ 0 ] * velocity[ 0 ] + GetAxis()[ 1 ] * velocity[ 1 ] + GetAxis()[ 2 ] * velocity[ 2 ] ); + physicsObj.SetAngularVelocity( angularVelocity.ToAngularVelocity() * GetAxis() ); + + PostEventMS( &EV_Guide, updateRate ); +} + +/* +================ +hhProjectileTracking::Save +================ +*/ +void hhProjectileTracking::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( turnFactor ); + savefile->WriteInt( updateRate ); + + enemy.Save( savefile ); + + savefile->WriteAngles( angularVelocity ); + savefile->WriteVec3( velocity ); + savefile->WriteFloat( cachedFovCos ); + savefile->WriteFloat( spinAngle ); + savefile->WriteFloat( turnFactorAcc ); + savefile->WriteFloat( spinDelta ); +} + +/* +================ +hhProjectileTracking::Restore +================ +*/ +void hhProjectileTracking::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( turnFactor ); + savefile->ReadInt( updateRate ); + + enemy.Restore( savefile ); + + savefile->ReadAngles( angularVelocity ); + savefile->ReadVec3( velocity ); + savefile->ReadFloat( cachedFovCos ); + savefile->ReadFloat( spinAngle ); + savefile->ReadFloat( turnFactorAcc ); + savefile->ReadFloat( spinDelta ); +} + +void hhProjectileTracking::StartTracking() { + Event_StartTracking(); +} \ No newline at end of file diff --git a/src/Prey/prey_projectiletracking.h b/src/Prey/prey_projectiletracking.h new file mode 100644 index 0000000..79a6a4a --- /dev/null +++ b/src/Prey/prey_projectiletracking.h @@ -0,0 +1,48 @@ +#ifndef __HH_PROJECTILE_TRACKING_H +#define __HH_PROJECTILE_TRACKING_H + +/*********************************************************************** + + hhProjectileTracking + +***********************************************************************/ +class hhProjectileTracking: public hhProjectile { + CLASS_PROTOTYPE( hhProjectileTracking ); + + public: + hhProjectileTracking(); + void Spawn(); + virtual void Launch( const idVec3 &start, const idMat3 &axis, const idVec3 &pushVelocity, const float timeSinceFire = 0.0f, const float launchPower = 1.0f, const float dmgPower = 1.0f ); + virtual void Explode( const trace_t *collision, const idVec3& velocity, int removeDelay ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + void StartTracking(); + void SetEnemy( idEntity* newEnemy ) { enemy = newEnemy; } + protected: + virtual idEntity* DetermineEnemy(); + bool EnemyIsValid( idEntity* actor ) const; + idEntity* WhosClosest( idEntity* possibleEnemy, idEntity* currentEnemy, float& currentEnemyDot, float& currentEnemyDist ) const; + idVec3 DetermineEnemyPosition( const idEntity* enemy ) const; + idVec3 DetermineEnemyDir( const idEntity* enemy ) const; + bool GetPhysicsToVisualTransform( idVec3 &origin, idMat3 &axis ); + protected: + void Event_Hover(); + virtual void Event_TrackTarget(); + void Event_StartTracking(); + void Event_StopTracking(); + + protected: + float turnFactor; + int updateRate; + + idEntityPtr enemy; + idAngles angularVelocity; + idVec3 velocity; + float cachedFovCos; + float spinAngle; + float turnFactorAcc; + float spinDelta; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectiletrigger.cpp b/src/Prey/prey_projectiletrigger.cpp new file mode 100644 index 0000000..88ab547 --- /dev/null +++ b/src/Prey/prey_projectiletrigger.cpp @@ -0,0 +1,56 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhProjectile, hhProjectileTrigger ) + EVENT( EV_Collision_Flesh, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Metal, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_AltMetal, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Wood, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Stone, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Glass, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Liquid, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_CardBoard, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Tile, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Forcefield, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Chaff, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Pipe, hhProjectileTrigger::Event_Collision_Explode ) + EVENT( EV_Collision_Wallwalk, hhProjectileTrigger::Event_Collision_Explode ) + + EVENT( EV_AllowCollision_Chaff, hhProjectileTrigger::Event_AllowCollision_Collide ) +END_CLASS + +void hhProjectileTrigger::Event_Collision_Explode( const trace_t* collision, const idVec3& velocity ) { + ProcessCollision(collision, velocity); + if ( state == EXPLODED || state == FIZZLED || state == COLLIDED ) { + return; + } + if( !collision ) { + return; + } + int length = 0; + idEntity *trigger; + idDict Args; + + Args.Set( "def_damage", spawnArgs.GetString("trigger_damage") ); + Args.Set( "mins", spawnArgs.GetString("trigger_min") ); + Args.Set( "maxs", spawnArgs.GetString("trigger_max") ); + Args.Set( "snd_loop", spawnArgs.GetString("snd_loop") ); + Args.Set( "snd_explode", spawnArgs.GetString("snd_loop") ); + Args.SetVector( "origin", GetOrigin() ); + Args.SetMatrix( "rotation", GetAxis() ); + trigger = gameLocal.SpawnObject( spawnArgs.GetString("def_trigger"), &Args ); + if ( trigger ) { + trigger->PostEventSec( &EV_Remove, spawnArgs.GetFloat( "remove_time", "5" ) ); + trigger->PostEventSec( &EV_StopSound, spawnArgs.GetFloat( "remove_time", "5" ) - 1, SND_CHANNEL_ANY, 0 ); + trigger->StartSound( "snd_explode", SND_CHANNEL_VOICE, 0, true, &length ); + trigger->StartSound( "snd_loop", SND_CHANNEL_BODY, 0, true, &length ); + } + SpawnExplosionFx( collision ); + SpawnDebris( collision->c.normal, velocity.ToNormal() ); + state = EXPLODED; + RemoveProjectile( spawnArgs.GetFloat( "remove_time", "5" ) ); + ProcessCollision(collision, velocity); + idThread::ReturnInt( 1 ); +} diff --git a/src/Prey/prey_projectiletrigger.h b/src/Prey/prey_projectiletrigger.h new file mode 100644 index 0000000..90cfd48 --- /dev/null +++ b/src/Prey/prey_projectiletrigger.h @@ -0,0 +1,10 @@ +#ifndef __HH_PROJECTILE_TRIGGER_H +#define __HH_PROJECTILE_TRIGGER_H + +class hhProjectileTrigger: public hhProjectile { +public: + CLASS_PROTOTYPE( hhProjectileTrigger ) + void Event_Collision_Explode( const trace_t* collision, const idVec3& velocity ); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_projectilewrench.cpp b/src/Prey/prey_projectilewrench.cpp new file mode 100644 index 0000000..a2fbeec --- /dev/null +++ b/src/Prey/prey_projectilewrench.cpp @@ -0,0 +1,43 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhProjectile, hhProjectileWrench ) + EVENT( EV_Collision_Flesh, hhProjectileWrench::Event_Collision_Impact ) + EVENT( EV_Collision_Metal, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_AltMetal, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Wood, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Stone, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Glass, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Liquid, hhProjectileWrench::Event_Collision_DisturbLiquid ) + EVENT( EV_Collision_CardBoard, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Tile, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Forcefield, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Pipe, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Wallwalk, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + EVENT( EV_Collision_Chaff, hhProjectileWrench::Event_Collision_Impact_AlertAI ) + +END_CLASS + +void hhProjectileWrench::Event_Collision_Impact_AlertAI( const trace_t* collision, const idVec3& velocity ) { + gameLocal.AlertAI( owner.GetEntity() ); + + CancelEvents( &EV_Explode ); + RemoveProjectile( ProcessCollision(collision, velocity) ); + state = COLLIDED; + idThread::ReturnInt( 1 ); +} + +void hhProjectileWrench::DamageEntityHit( const trace_t* collision, const idVec3& velocity, idEntity* entHit ) { + if (!gameLocal.isMultiplayer) { + hhPlayer* pOwner = (owner->IsType(hhPlayer::Type)) ? static_cast(owner.GetEntity()) : NULL; + + if ( entHit && entHit->IsType( idActor::Type ) ) { + //pOwner->weapon->SetSkinByName( "skins/weapons/wrench_bloody" ); + pOwner->weapon->SetShaderParm( 7, -MS2SEC(gameLocal.time) ); + } + } + hhProjectile::DamageEntityHit( collision, velocity, entHit ); +} + diff --git a/src/Prey/prey_projectilewrench.h b/src/Prey/prey_projectilewrench.h new file mode 100644 index 0000000..128463d --- /dev/null +++ b/src/Prey/prey_projectilewrench.h @@ -0,0 +1,11 @@ +#ifndef __HH_PROJECTILE_WRENCH_H +#define __HH_PROJECTILE_WRENCH_H + +class hhProjectileWrench : public hhProjectile { + CLASS_PROTOTYPE( hhProjectileWrench ); + void Event_Collision_Impact_AlertAI( const trace_t* collision, const idVec3& velocity ); + protected: + void DamageEntityHit( const trace_t* collision, const idVec3& velocity, idEntity* entHit ); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_script_thread.cpp b/src/Prey/prey_script_thread.cpp new file mode 100644 index 0000000..114936f --- /dev/null +++ b/src/Prey/prey_script_thread.cpp @@ -0,0 +1,114 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idThread, hhThread ) +END_CLASS + +hhThread::hhThread() : idThread() {} +hhThread::hhThread( idEntity *self, const function_t *func ) : idThread( self, func ) {} +hhThread::hhThread( const function_t *func ) : idThread( func ) {} +hhThread::hhThread( idInterpreter *source, const function_t *func, int args ) : idThread( source, func, args ) {} + +/* +================ +hhThread::PushParm +================ +*/ +void hhThread::PushParm( int value ) { + interpreter.Push( value ); +} + +/* +================ +hhThread::PushString +================ +*/ +void hhThread::PushString( const char *text ) { + PushParm( (int)new idStr(text) ); +} + +/* +================ +hhThread::PushFloat +================ +*/ +void hhThread::PushFloat( float value ) { + PushParm( *(int*)&value ); +} + +/* +================ +hhThread::PushInt +================ +*/ +void hhThread::PushInt( int value ) { + PushParm( value ); +} + +/* +================ +hhThread::PushVector +================ +*/ +void hhThread::PushVector( const idVec3 &vec ) { + float val = 0.0f; + for( int ix = 0; ix < vec.GetDimension(); ++ix ) { + val = vec[ ix ]; + PushParm( *(int*)&val ); + } +} + +/* +================ +hhThread::PushEntity +================ +*/ +void hhThread::PushEntity( const idEntity *ent ) { + HH_ASSERT( ent ); + + PushParm( ent->entityNumber + 1 ); +} + +/* +================ +hhThread::ClearStack +================ +*/ +void hhThread::ClearStack() { + interpreter.Reset(); +} + +/* +================ +hhThread::ParseAndPushArgsOntoStack +================ +*/ +bool hhThread::ParseAndPushArgsOntoStack( const idCmdArgs &args, const function_t* function ) { + idList parmList; + + hhUtils::SplitString( args, parmList ); + + return ParseAndPushArgsOntoStack( parmList, function ); +} + +/* +================ +hhThread::ParseAndPushArgsOntoStack +================ +*/ +bool hhThread::ParseAndPushArgsOntoStack( const idList& args, const function_t* function ) { + int numParms = function->def->TypeDef()->NumParameters(); + idTypeDef* parmType = NULL; + const char* parm = NULL; + + for( int ix = 0; ix < numParms; ++ix ) { + parmType = function->def->TypeDef()->GetParmType( ix ); + parm = args[ ix ].c_str(); + + parmType->PushOntoStack( parm, this ); + } + + return true; +} \ No newline at end of file diff --git a/src/Prey/prey_script_thread.h b/src/Prey/prey_script_thread.h new file mode 100644 index 0000000..33c97dc --- /dev/null +++ b/src/Prey/prey_script_thread.h @@ -0,0 +1,27 @@ +#ifndef __HH_SCRIPT_THREAD_H__ +#define __HH_SCRIPT_THREAD_H__ + +class hhThread : public idThread { + CLASS_PROTOTYPE( hhThread ); + + public: + hhThread(); + hhThread( idEntity *self, const function_t *func ); + hhThread( const function_t *func ); + hhThread( idInterpreter *source, const function_t *func, int args ); + + void PushString( const char *text ); + void PushFloat( float value ); + void PushInt( int value ); + void PushVector( const idVec3 &vec ); + void PushEntity( const idEntity *ent ); + void ClearStack(); + + bool ParseAndPushArgsOntoStack( const idCmdArgs& args, const function_t* function ); + bool ParseAndPushArgsOntoStack( const idList& args, const function_t* function ); + + protected: + void PushParm( int value ); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_sound.cpp b/src/Prey/prey_sound.cpp new file mode 100644 index 0000000..30dc262 --- /dev/null +++ b/src/Prey/prey_sound.cpp @@ -0,0 +1,258 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_StartDelayedSoundShader( "", "sddd" ); +const idEventDef EV_ResetTargetHandles( "resetTargetHandles" ); +const idEventDef EV_SubtitleOff( "" ); + +CLASS_DECLARATION( idSound, hhSound ) + EVENT( EV_PostSpawn, hhSound::Event_SetTargetHandles ) + EVENT( EV_ResetTargetHandles, hhSound::Event_SetTargetHandles ) + EVENT( EV_SubtitleOff, hhSound::Event_SubtitleOff ) +END_CLASS + + +/* +================ +hhSound::hhSound +================ +*/ +hhSound::hhSound( void ) { + positionOffset.Zero(); +} + +/* +================ +hhSound::Spawn +================ +*/ +void hhSound::Spawn() { + PostEventMS( &EV_PostSpawn, 0 ); +} + +/* +================ +hhSound::StartDelayedSoundShader +================ +*/ +void hhSound::StartDelayedSoundShader( const idSoundShader *shader, const s_channelType channel, int soundShaderFlags, bool broadcast ) { + CancelEvents( &EV_StartDelayedSoundShader ); + PostEventSec( &EV_StartDelayedSoundShader, RandomRange(spawnArgs.GetFloat("s_minDelay"), spawnArgs.GetFloat("s_maxDelay")), (const char *)shader->GetName(), (int)channel, (int)soundShaderFlags, (int)broadcast ); +} + +/* +================ +hhSound::StopDelayedSound +================ +*/ +void hhSound::StopDelayedSound( const s_channelType channel, bool broadcast ) { + CancelEvents( &EV_StartDelayedSoundShader ); + idSound::StopSound( channel, broadcast ); +} + +/* +================ +hhSound::StartSoundShader +================ +*/ +bool hhSound::StartSoundShader( const idSoundShader *shader, const s_channelType channel, int soundShaderFlags, bool broadcast, int* length ) { + bool result = true; + float volume = 0.0f; + + positionOffset = DeterminePositionOffset(); + if( !spawnArgs.GetBool("s_useRandomDelay") ) { + StopDelayedSound( channel, broadcast ); + + result = idSound::StartSoundShader( shader, channel, soundShaderFlags, broadcast, length ); + + const char *subtitleText = spawnArgs.GetString( "subtitle", NULL ); + if ( result && subtitleText ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( length > 0 && player && player->hud ) { + player->hud->SetStateInt("subtitlefadetime", 0); + player->hud->SetStateInt("subtitlex", 0 ); + player->hud->SetStateInt("subtitley", 400 ); + player->hud->SetStateInt("subtitlecentered", true); + player->hud->SetStateString("subtitletext", common->GetLanguageDict()->GetString(subtitleText)); + player->hud->StateChanged(gameLocal.time); + player->hud->HandleNamedEvent("DisplaySubtitle"); + PostEventMS( &EV_SubtitleOff, *length ); + } + } + + if( DetermineVolume(volume) ) { + HH_SetSoundVolume( volume, channel ); + } + } else { + StartDelayedSoundShader( shader, channel, soundShaderFlags, broadcast ); + } + + //trigger targets when sound ends + if ( length && targets.Num() && spawnArgs.GetInt("trigger_targets","0") ) { + float delay = *length * 0.001 + spawnArgs.GetFloat("target_delay", "0"); + if ( delay < 0 ) { + delay = 0; + } + PostEventSec( &EV_ActivateTargets, delay , this ); + } + + return result; +} + +/* +================ +hhSound::StopSound +================ +*/ +void hhSound::StopSound( const s_channelType channel, bool broadcast ) { + StopDelayedSound( channel, broadcast ); +} + +/* +================ +hhSound::RandomRange +================ +*/ +float hhSound::RandomRange( const float min, const float max ) { + return hhMath::Lerp( min, max, gameLocal.random.RandomFloat() ); +} + +/* +=============== +hhSound::DetermineVolume +=============== +*/ +bool hhSound::DetermineVolume( float& volume ) { + if( !spawnArgs.GetBool("s_useRandomVolume") ) { + return false; + } + + volume = hhMath::dB2Scale( RandomRange(spawnArgs.GetInt("s_minVolume"), spawnArgs.GetInt("s_maxVolume")) ); + return true; +} + +/* +=============== +hhSound::DeterminePositionOffset +=============== +*/ +idVec3 hhSound::DeterminePositionOffset() { + if( !spawnArgs.GetBool("s_useRandomPosition") ) { + return vec3_origin; + } + + float radius = RandomRange( spawnArgs.GetFloat("s_minRadius"), spawnArgs.GetFloat("s_maxRadius") ); + return hhUtils::RandomVector() * radius; +} + +/* +================ +hhSound::GetCurrentAmplitude +================ +*/ +float hhSound::GetCurrentAmplitude(const s_channelType channel) { + if (refSound.referenceSound && refSound.referenceSound->CurrentlyPlaying()) { + return refSound.referenceSound->CurrentAmplitude(channel); + } + return 0.0f; +} + +/* +================ +hhSound::GetPhysicsToSoundTransform +================ +*/ +bool hhSound::GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis ) { + origin = positionOffset; + axis.Identity(); + return true; +} + +/* +================ +hhSound::Event_StartDelayedSoundShader +================ +*/ +void hhSound::Event_StartDelayedSoundShader( const char *shaderName, const s_channelType channel, int soundShaderFlags, bool broadcast ) { + int soundLength = 0; + float volume = 0.0f; + + const idSoundShader *shader = declManager->FindSound( shaderName ); + assert( shader ); + if( !shader ) { + return; + } + + if( !GetSoundEmitter() || !GetSoundEmitter()->CurrentlyPlaying() ) { + soundLength = idSound::StartSoundShader( shader, channel, soundShaderFlags, broadcast );//Not sure if we should broadcast + + positionOffset = DeterminePositionOffset(); + if( DetermineVolume(volume) ) { + HH_SetSoundVolume( volume, channel ); + } + } + + CancelEvents( &EV_StartDelayedSoundShader ); + PostEventSec( &EV_StartDelayedSoundShader, MS2SEC(soundLength) + RandomRange(spawnArgs.GetFloat("s_minDelay"), spawnArgs.GetFloat("s_maxDelay")), shaderName, (int)channel, (int)soundShaderFlags, (int)broadcast ); +} + +void hhSound::Event_SubtitleOff( void ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if ( player && player->hud ) { + player->hud->HandleNamedEvent("RemoveSubtitleInstant"); + } +} + +/* +================ +hhSound::Event_SetTargetHandles + +Copied from hhLight +================ +*/ +void hhSound::Event_SetTargetHandles( void ) { + int i; + idEntity *targetEnt = NULL; + + if ( !refSound.referenceSound ) { + refSound.referenceSound = gameSoundWorld->AllocSoundEmitter(); + } + + for( i = 0; i < targets.Num(); i++ ) { + targetEnt = targets[ i ].GetEntity(); + if ( targetEnt ) { + if( targetEnt->IsType(idLight::Type) ) { + static_cast(targetEnt)->SetLightParent( this ); + } + + targetEnt->FreeSoundEmitter( true ); + + // manually set the refSound to this light's refSound + targetEnt->GetRenderEntity()->referenceSound = refSound.referenceSound; + + // update the renderEntity to the renderer + targetEnt->UpdateVisuals(); + } + } +} + +/* +================ +hhSound::Save +================ +*/ +void hhSound::Save( idSaveGame *savefile ) const { + savefile->WriteVec3( positionOffset ); +} + +/* +================ +hhSound::Restore +================ +*/ +void hhSound::Restore( idRestoreGame *savefile ) { + savefile->ReadVec3( positionOffset ); +} + diff --git a/src/Prey/prey_sound.h b/src/Prey/prey_sound.h new file mode 100644 index 0000000..b262d60 --- /dev/null +++ b/src/Prey/prey_sound.h @@ -0,0 +1,38 @@ +#ifndef __HH_SOUND_H +#define __HH_SOUND_H + +extern const idEventDef EV_Sound; +extern const idEventDef EV_ResetTargetHandles; + +class hhSound : public idSound { + CLASS_PROTOTYPE( hhSound ); + + public: + hhSound(); + void Spawn(); + virtual bool StartSoundShader( const idSoundShader *shader, const s_channelType channel, int soundShaderFlags, bool broadcast, int* length ); + virtual void StopSound( const s_channelType channel, bool broadcast ); + void StartDelayedSoundShader( const idSoundShader *shader, const s_channelType channel, int soundShaderFlags, bool broadcast ); + void StopDelayedSound( const s_channelType channel, bool broadcast = false ); + float GetCurrentAmplitude(const s_channelType channel); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + virtual bool GetPhysicsToSoundTransform( idVec3 &origin, idMat3 &axis ); + + float RandomRange( const float min, const float max ); + + bool DetermineVolume( float& volume ); + idVec3 DeterminePositionOffset(); + + protected: + void Event_SetTargetHandles(); + void Event_StartDelayedSoundShader( const char *shader, const s_channelType channel, int soundShaderFlags = 0, bool broadcast = false ); + void Event_SubtitleOff( void ); + protected: + idVec3 positionOffset; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_soundleadincontroller.cpp b/src/Prey/prey_soundleadincontroller.cpp new file mode 100644 index 0000000..ff97b0c --- /dev/null +++ b/src/Prey/prey_soundleadincontroller.cpp @@ -0,0 +1,264 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( idClass, hhSoundLeadInController ) +END_CLASS + +/* +================ +hhSoundLeadInController::hhSoundLeadInController + +HUMANHEAD: aob +================ +*/ +hhSoundLeadInController::hhSoundLeadInController() { + leadInShader = NULL; + loopShader = NULL; + leadOutShader = NULL; + + startTime = 0; + endTime = 0; + + owner = NULL; + + //rww - networking-related variables + lastLeadChannel = SND_CHANNEL_ANY; + lastLoopChannel = SND_CHANNEL_ANY; + bPlaying = false; + iLoopOnlyOnLocal = -1; +} + +/* +================ +hhSoundLeadInController::SetOwner + +HUMANHEAD: aob +================ +*/ +void hhSoundLeadInController::SetOwner( idEntity* ent ) { + owner = ent; +} + +/* +================ +hhSoundLeadInController::WriteToSnapshot + +HUMANHEAD: rww +================ +*/ +void hhSoundLeadInController::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(lastLeadChannel, 8); + msg.WriteBits(lastLoopChannel, 8); + + msg.WriteBits(owner.GetSpawnId(), 32); + + msg.WriteBits(bPlaying, 1); +} + +/* +================ +hhSoundLeadInController::ReadFromSnapshot + +HUMANHEAD: rww +note that the methods used here are not failsafe given the range of functionality +within this class, and this logic may need to be adjusted on a per-case basis if +more instances are to be sync'd over the net using this method. +================ +*/ +void hhSoundLeadInController::ReadFromSnapshot( const idBitMsgDelta &msg ) { + lastLeadChannel = msg.ReadBits(8); + lastLoopChannel = msg.ReadBits(8); + + owner.SetSpawnId(msg.ReadBits(32)); + + bool nowPlaying = !!msg.ReadBits(1); + if (nowPlaying != bPlaying) { + if (nowPlaying) { + StartSound(lastLeadChannel, lastLoopChannel); + } + else { + StopSound(lastLeadChannel, lastLoopChannel); + } + } +} + +/* +================ +hhSoundLeadInController::SetLeadIn + +HUMANHEAD: aob +================ +*/ +void hhSoundLeadInController::SetLeadIn( const char* soundname ) { + leadInShader = declManager->FindSound( soundname, false ); +} + +/* +================ +hhSoundLeadInController::SetLoop + +HUMANHEAD: aob +================ +*/ +void hhSoundLeadInController::SetLoop( const char* soundname ) { + loopShader = declManager->FindSound( soundname, false ); +} + +/* +================ +hhSoundLeadInController::SetLeadOut + +HUMANHEAD: aob +================ +*/ +void hhSoundLeadInController::SetLeadOut( const char* soundname ) { + leadOutShader = declManager->FindSound( soundname, false ); +} + +/* +================ +hhSoundLeadInController::StartSound + +HUMANHEAD: aob +================ +*/ +int hhSoundLeadInController::StartSound( const s_channelType leadChannel, const s_channelType loopChannel, int soundShaderFlags, bool broadcast ) { + int length = 0; + + if( !owner.IsValid() ) { + return 0; + } + + if( loopShader ) { + owner->StopSound( loopChannel, broadcast ); + } + + if( leadOutShader ) { + owner->StopSound( leadChannel, broadcast ); + } + + if( leadInShader ) { + owner->StartSoundShader( leadInShader, leadChannel, 0, broadcast, &length ); + StartFade( leadInShader, leadChannel, endTime, startTime, leadInShader->GetVolume(), length ); + + startTime = gameLocal.GetTime(); + endTime = startTime + length; + } + + if (iLoopOnlyOnLocal == -1 || iLoopOnlyOnLocal == gameLocal.localClientNum) { //rww - for spirit music and whatever else needs it + owner->StartSoundShader( loopShader, loopChannel, 0, broadcast, NULL ); + StartFade( loopShader, loopChannel, startTime, endTime, loopShader->GetVolume(), length ); + } + + //rww - for networking + lastLeadChannel = leadChannel; + lastLoopChannel = loopChannel; + bPlaying = true; + + return length; +} + +/* +================ +hhSoundLeadInController::StopSound + +HUMANHEAD: aob +================ +*/ +void hhSoundLeadInController::StopSound( const s_channelType leadChannel, const s_channelType loopChannel, bool broadcast ) { + int length = 0; + + if( !owner.IsValid() ) { + return; + } + + if( leadInShader ) { + owner->StopSound( leadChannel, broadcast ); + } + + if( loopShader ) { + owner->StopSound( loopChannel, broadcast ); + } + + if( leadOutShader ) { + owner->StartSoundShader( leadOutShader, leadChannel, 0, broadcast, &length ); + StartFade( leadOutShader, leadChannel, startTime, endTime, hhMath::Scale2dB(0.0f), length ); + + startTime = gameLocal.GetTime(); + endTime = startTime + length; + } + + //rww - for networking + lastLeadChannel = leadChannel; + lastLoopChannel = loopChannel; + bPlaying = false; +} + +/* +================ +hhSoundLeadInController::CalculateScale + +HUMANHEAD: aob +================ +*/ +float hhSoundLeadInController::CalculateScale( const float value, const float min, const float max ) { + return hhUtils::CalculateScale( value, min, max ); +} + +/* +================ +hhSoundLeadInController::StartFade + +HUMANHEAD: aob +================ +*/ +void hhSoundLeadInController::StartFade( const idSoundShader* shader, const s_channelType channel, int start, int end, int finaldBVolume, int duration ) { +/* + float scale = CalculateScale( gameLocal.GetTime(), start, end ); + + scale *= hhMath::dB2Scale( shader->GetVolume() ); + owner->HH_SetSoundVolume( scale, channel ); + owner->FadeSoundShader( finaldBVolume, duration, channel ); +*/ +} + +/* +================ +hhSoundLeadInController::Save +================ +*/ +void hhSoundLeadInController::Save( idSaveGame *savefile ) const { + savefile->WriteSoundShader( leadInShader ); + savefile->WriteSoundShader( loopShader ); + savefile->WriteSoundShader( leadOutShader ); + savefile->WriteInt( startTime ); + savefile->WriteInt( endTime ); + + owner.Save( savefile ); +} + +/* +================ +hhSoundLeadInController::Restore +================ +*/ +void hhSoundLeadInController::Restore( idRestoreGame *savefile ) { + savefile->ReadSoundShader( leadInShader ); + savefile->ReadSoundShader( loopShader ); + savefile->ReadSoundShader( leadOutShader ); + savefile->ReadInt( startTime ); + savefile->ReadInt( endTime ); + + owner.Restore( savefile ); +} + +/* +================ +hhSoundLeadInController::SetLoopOnlyOnLocal +================ +*/ +void hhSoundLeadInController::SetLoopOnlyOnLocal(int loopOnlyOnLocal) { + iLoopOnlyOnLocal = loopOnlyOnLocal; +} diff --git a/src/Prey/prey_soundleadincontroller.h b/src/Prey/prey_soundleadincontroller.h new file mode 100644 index 0000000..91a4917 --- /dev/null +++ b/src/Prey/prey_soundleadincontroller.h @@ -0,0 +1,53 @@ +#ifndef __HH_SOUND_LEADIN_CONTROLLER_H +#define __HH_SOUND_LEADIN_CONTROLLER_H + +class hhSoundLeadInController : public idClass { + CLASS_PROTOTYPE( hhSoundLeadInController ); + + public: + hhSoundLeadInController(); + + void SetOwner( idEntity* ent ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + void SetLeadIn( const char* soundname ); + void SetLoop( const char* soundname ); + void SetLeadOut( const char* soundname ); + + int StartSound( const s_channelType leadChannel, const s_channelType loopChannel, int soundShaderFlags = 0, bool broadcast = false ); + void StopSound( const s_channelType leadChannel, const s_channelType loopChannel, bool broadcast = false ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + void SetLoopOnlyOnLocal(int loopOnlyOnLocal); + + protected: + float CalculateScale( const float value, const float min, const float max ); + void StartFade( const idSoundShader* shader, const s_channelType channel, int start, int end, int finaldBVolume, int duration ); + + protected: + void Event_StartSoundShaderEx( idSoundShader* shader, const s_channelType channel, int soundShaderFlags, bool broadcast ); + + protected: + const idSoundShader* leadInShader; + const idSoundShader* loopShader; + const idSoundShader* leadOutShader; + + int startTime; + int endTime; + + idEntityPtr owner; + + //rww - everything below here is for networking, no need to save/restore + s_channelType lastLeadChannel; + s_channelType lastLoopChannel; + public: + bool bPlaying; + int iLoopOnlyOnLocal; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_spiritbridge.cpp b/src/Prey/prey_spiritbridge.cpp new file mode 100644 index 0000000..4cd1710 --- /dev/null +++ b/src/Prey/prey_spiritbridge.cpp @@ -0,0 +1,70 @@ +//************************************************************************** +//** +//** PREY_SPIRITBRIDGE.CPP +//** +//** Game code for the spirit bridge +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS ------------------------------------------------------------------ + +// TYPES ------------------------------------------------------------------- + +// CLASS DECLARATIONS ------------------------------------------------------ + +CLASS_DECLARATION( idEntity, hhSpiritBridge ) + EVENT( EV_Activate, hhSpiritBridge::Event_Activate ) +END_CLASS + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// hhSpiritBridge::Spawn +// +//========================================================================== + +void hhSpiritBridge::Spawn(void) { + fl.takedamage = false; + + if ( !spawnArgs.GetInt( "start_off" ) ) { + GetPhysics()->SetContents( CONTENTS_SPIRITBRIDGE ); + } else { // Start the bridge off + GetPhysics()->SetContents( 0 ); + Hide(); + } + + SetShaderParm( SHADERPARM_MODE, gameLocal.random.CRandomFloat() ); +} + +//========================================================================== +// +// hhSpiritBridge::Event_Activate +// +//========================================================================== + +void hhSpiritBridge::Event_Activate( idEntity *activator ) { + if ( IsHidden() ) { + Show(); + GetPhysics()->SetContents( CONTENTS_SPIRITBRIDGE ); + } else { + Hide(); + GetPhysics()->SetContents( 0 ); + } +} diff --git a/src/Prey/prey_spiritbridge.h b/src/Prey/prey_spiritbridge.h new file mode 100644 index 0000000..f75ea9e --- /dev/null +++ b/src/Prey/prey_spiritbridge.h @@ -0,0 +1,17 @@ + +#ifndef __PREY_SPIRITBRIDGE_H__ +#define __PREY_SPIRITBRIDGE_H__ + +//class hhPlayer; + +class hhSpiritBridge : public idEntity { +public: + CLASS_PROTOTYPE( hhSpiritBridge ); + + void Spawn( void ); + +protected: + void Event_Activate( idEntity *activator ); +}; + +#endif /* __PREY_SPIRITBRIDGE_H__ */ diff --git a/src/Prey/prey_spiritproxy.cpp b/src/Prey/prey_spiritproxy.cpp new file mode 100644 index 0000000..4d95d37 --- /dev/null +++ b/src/Prey/prey_spiritproxy.cpp @@ -0,0 +1,1094 @@ +//************************************************************************** +//** +//** PREY_SPIRITPROXY.CPP +//** +//** Game code for the player proxy dropped when the player spirit walks +//** +//************************************************************************** + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define MIN_ACTIVATION_TIME SEC2MS( 1.0f ) + +const idEventDef EV_SpawnEffect( "", NULL ); +const idEventDef EV_OrientToGravity( "orientToGravity", "d" ); + +//========================================================================== +// hhSpiritProxy +//========================================================================== + +CLASS_DECLARATION( idActor, hhSpiritProxy ) + EVENT( EV_SpawnEffect, hhSpiritProxy::Event_SpawnEffect ) + EVENT( EV_OrientToGravity, hhSpiritProxy::Event_OrientToGravity ) + EVENT( EV_ResetGravity, hhSpiritProxy::Event_ResetGravity ) + EVENT( EV_ShouldRemainAlignedToAxial, hhSpiritProxy::Event_ShouldRemainAlignedToAxial ) + EVENT( EV_Broadcast_AssignFx, hhSpiritProxy::Event_AssignSpiritFx ) +END_CLASS + +hhSpiritProxy::hhSpiritProxy() { + fl.networkSync = true; + playerModelNum = 0; +} + +void hhSpiritProxy::Spawn(void) { + fl.takedamage = true; + spiritFx = NULL; + + clientAnimated = false; + netAnimType = 0; + + if (gameLocal.isMultiplayer) { + SetModel("model_multiplayer_tommy"); + SetSkinByName("skins/characters/tommy_mp_spirit"); + playerModelNum = 0; + } + + // Required so that models move in place. + GetAnimator()->RemoveOriginOffset( true ); + + if (gameLocal.isMultiplayer && !IsType(hhDeathProxy::Type)) { //rww - ambient sound for mp + StartSound( "snd_spiritSound", SND_CHANNEL_VOICE, 0, false, NULL ); + } +} + +//========================================================================== +// +// hhSpiritProxy::SetModel +// +//========================================================================== +void hhSpiritProxy::SetModel( const char *modelname ) { + spawnArgs.Set("playerModel", modelname); + idActor::SetModel(modelname); +} + +//========================================================================== +// +// hhSpiritProxy::UpdateModelForPlayer +// +//========================================================================== +void hhSpiritProxy::UpdateModelForPlayer(void) { + int modelNum = 0; + player->GetUserInfo()->GetInt("ui_modelNum", "0", modelNum); + + if (modelNum != playerModelNum) { //time to change models then. + idStr customModel; + if (!IsType(hhMPDeathProxy::Type)) { //don't do this for death prox, since it uses the spirit fadeaway skin + SetSkin(NULL); //destroy custom skin on the spirit proxy so that the appropriate player skin is used. + } + playerModelNum = modelNum; + if (player->spawnArgs.GetString(va("model_mp%i", modelNum), "", customModel)) { + SetModel(customModel.c_str()); + } + else { + SetModel("model_multiplayer_tommy"); + } + if (!IsType(hhMPDeathProxy::Type)) { + //new: check for a custom spirit proxy skin for the given model + if (!player->spawnArgs.GetString(va("skin_mpspirit%i", modelNum), "", customModel)) { + customModel = "skins/characters/tommy_mp_spirit"; + } + SetSkinByName(customModel); + + StartAnimation(); //restart animation on new model (unless you're a corpse) + } + } +} + +//========================================================================== +// +// hhSpiritProxy::Think +// +//========================================================================== +void hhSpiritProxy::Think() { + idVec3 oldOrigin = physicsObj.GetOrigin(); + idVec3 oldVelocity = physicsObj.GetLinearVelocity(); + + if (gameLocal.isMultiplayer) { + DrawPlayerIcons(); + + if (player.IsValid() && player.GetEntity() && player->IsType(hhPlayer::Type) && !IsType(hhMPDeathProxy::Type)) { + UpdateModelForPlayer(); + } + } + idActor::Think(); + + if (player.IsValid() && player->IsType(hhPlayer::Type) && (!IsType(hhMPDeathProxy::Type) || !IsType(hhMPDeathProxy::Type))) { + CrashLand(oldOrigin, oldVelocity); + } +} + +//========================================================================== +// +// hhSpiritProxy::CreateProxy +// +// Spawns a proxy object and then activates it +//========================================================================== + +hhSpiritProxy *hhSpiritProxy::CreateProxy( const char *name, hhPlayer *owner, const idVec3& origin, const idMat3& bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ) { + hhSpiritProxy *proxy; + + proxy = (hhSpiritProxy *)gameLocal.SpawnObject( name ); + if( !proxy ) { + gameLocal.Error("hhSpiritProxy::CreateProxy: Could not spawn the player proxy\n"); + } + + proxy->ActivateProxy( owner, origin, bboxAxis, newViewAxis, newViewAngles, newEyeAxis ); + + return proxy; +} + + +//========================================================================== +// +// hhSpiritProxy::Event_SpawnEffect +// +//========================================================================== + +void hhSpiritProxy::Event_SpawnEffect() { + hhFxInfo fxInfo; + idVec3 boneOffset; + idMat3 boneAxis; + + GetJointWorldTransform( spawnArgs.GetString( "bone_spiritFx" ), boneOffset, boneAxis ); + + fxInfo.RemoveWhenDone( false ); + fxInfo.SetNormal( boneAxis[1] ); + fxInfo.SetEntity( this ); + BroadcastFxInfo( spawnArgs.GetString( "fx_spirit" ), boneOffset, GetAxis(), &fxInfo, &EV_Broadcast_AssignFx ); +} + +//========================================================================== +// +// hhSpiritProxy::ActivateProxy +// +//========================================================================== + +void hhSpiritProxy::ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3 &bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ) { + assert( owner ); + + player = owner; + + viewAxis = newViewAxis; + viewAngles = newViewAngles; + eyeAxis = newEyeAxis; + + physicsObj.SetSelf( this ); + physicsObj.SetClipModel( new idClipModel(owner->GetPhysics()->GetClipModel()), 1.0f ); + physicsObj.SetOrigin( origin ); + physicsObj.SetAxis( bboxAxis ); + physicsObj.SetClipMask( CONTENTS_SOLID | CONTENTS_PLAYERCLIP | CONTENTS_FORCEFIELD ); // HUMANHEAD mdl: MASK_PLAYERSOLID - CONTENTS_BODY + physicsObj.SetContents( CONTENTS_CORPSE | CONTENTS_MONSTERCLIP | CONTENTS_RENDERMODEL ); + physicsObj.CheckWallWalk( true ); + if( player->IsCrouching() ) { + physicsObj.ForceCrouching(); + } + SetPhysics( &physicsObj ); + + if (owner->GetPhysics()->IsType(hhPhysics_Player::Type)) { //rww - don't stay half-oriented if player goes into spirit while gravity-flipping + hhPhysics_Player *plPhys = static_cast(owner->GetPhysics()); + OrientToGravity(plPhys->OrientToGravity()); + SetGravity(plPhys->GetGravity()); + } + + // Save the time when activated, to keep the player from being bounced back into the proxy too quickly + activationTime = gameLocal.time; + + StartAnimation(); + + UpdateVisuals(); + + // Set eye height + eyeOffset = GetPhysics()->GetBounds()[ 1 ].z - 6; + + // TODO: AIMSG_REMOVE: Tell the AI that spirit walk has started + + // Spawn spirit effect + PostEventMS( &EV_SpawnEffect, spawnArgs.GetInt( "spiritBlendTime", "400" ) ); + + if (!IsType(hhDeathProxy::Type) && !IsType(hhDeathWalkProxy::Type)) { + idVec3 vel = player->GetPhysics()->GetLinearVelocity(); + GetPhysics()->SetLinearVelocity( player->GetPhysics()->GetLinearVelocity() ); + GetPhysics()->SetAngularVelocity( player->GetPhysics()->GetAngularVelocity() ); + } + + // Make sure spiritproxy is affected by wallwalkmovers + BecomeActive( TH_PHYSICS ); +} + +//========================================================================== +// +// hhSpiritProxy::DeactivateProxy +// +//========================================================================== + +void hhSpiritProxy::DeactivateProxy(void) { + if( !player.IsValid() ) { + return; + } + + // TODO: AIMSG_REMOVED Tell the AI that spirit walk has ended + + // TODO: AIMSG_REMOVED Tell monsters they 'heard' a sound where the player spirit was removed from + // This makes it so monsters will 'investigate' where a player just disappeared from. Could be cool game dynamic? Maybe not needed? + + if (!IsType(hhDeathProxy::Type)) { //rww - for deathwalk, the player will manage values + RestorePlayerLocation( GetOrigin(), GetPhysics()->GetAxis(), viewAxis[0], viewAngles ); + idVec3 vel = GetPhysics()->GetLinearVelocity(); + player->GetPhysics()->SetLinearVelocity( GetPhysics()->GetLinearVelocity() ); + player->GetPhysics()->SetAngularVelocity( GetPhysics()->GetAngularVelocity() ); + } + + //HUMANHEAD rww - in multiplayer, telefrag other players who are in my body + if (gameLocal.isMultiplayer && !gameLocal.isClient) { + idBounds testBounds = player->GetPhysics()->GetAbsBounds(); + if (testBounds != bounds_zero) { + idEntity *touch[ MAX_GENTITIES ]; + + testBounds.ExpandSelf(-4.0f); //don't do anything if they are right on the edge, to avoid exploiting this by going up to someone and spiriting to kill them + int num = gameLocal.clip.EntitiesTouchingBounds(testBounds, player->GetPhysics()->GetClipMask(), touch, MAX_GENTITIES); + for (int i = 0; i < num; i++) { + if (touch[i] && touch[i]->IsType(hhPlayer::Type) && touch[i] != player.GetEntity() && touch[i]->fl.takedamage) { + touch[i]->Damage(player.GetEntity(), player.GetEntity(), vec3_origin, "damage_telefrag", 1.0f, INVALID_JOINT); + } + } + } + } + //HUMANHEAD END + + Hide(); // JRM: Hide this so it doesn't stick around while waiting to be removed + GetPhysics()->SetContents( 0 ); + PostEventMS( &EV_Remove, 1000 ); // Keep the proxy around for a few frames to deal with removal issues - JRM: made bigger + player = NULL; // Completely disconnect the proxy from the owner + + if ( spiritFx.IsValid() ) { // Remove spirit effect + spiritFx->Nozzle( false ); + SAFE_REMOVE( spiritFx ); + } + + fl.refreshReactions = FALSE; + CancelEvents( &EV_SpawnEffect ); +} + +//========================================================================== +// +// hhSpiritProxy::RestorePlayerLocation +// +//========================================================================== +void hhSpiritProxy::RestorePlayerLocation( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& angles ) { + if( IsCrouching() ) { + player->ForceCrouching(); + } + + player->TeleportNoKillBox( origin, bboxAxis, viewDir, (angles.ToMat3() * eyeAxis.Transpose()).ToAngles() ); +} + +//========================================================================== +// +// hhSpiritProxy::StartAnimation +// +// Starts the animation on the proxy +//========================================================================== +void hhSpiritProxy::StartAnimation() { + int spiritBlendTime = spawnArgs.GetInt( "spiritBlendTime", "400" ); + // Copy the current player animation to blend into the spirit anim + GetAnimator()->CopyAnimations( *( player->GetAnimator() ) ); + + // Play the initial animation + int anim; + + if (gameLocal.isClient) { //rww + switch (netAnimType) { + case 1: + anim = GetAnimator()->GetAnim("spiritleave"); + break; + case 2: + anim = GetAnimator()->GetAnim("crouch"); + break; + default: + assert(!"hhSpiritProxy has invalid netAnimType"); + anim = GetAnimator()->GetAnim("spiritleave"); + break; + } + } + else { + if ( IsCrouching() ) { + // Determine contents of the bounds if the player is crouching + idBounds bounds; + idTraceModel trm; + + bounds[0].Set( -pm_bboxwidth.GetFloat() * 0.5f, -pm_bboxwidth.GetFloat() * 0.5f, 0 ); + bounds[1].Set( pm_bboxwidth.GetFloat() * 0.5f, pm_bboxwidth.GetFloat() * 0.5f, pm_normalheight.GetFloat() ); + + trm.SetupBox( bounds ); + idClipModel *clipModel = new idClipModel(trm); + int contents = gameLocal.clip.Contents( player->GetOrigin(), clipModel, player->GetAxis(), CONTENTS_SOLID, player.GetEntity() ); + delete clipModel; + + if ( contents & CONTENTS_SOLID ) { // Standing upright collides with geometry, so play the crouch anim when spiritwalking + anim = GetAnimator()->GetAnim("crouch"); + netAnimType = 2; + } else { + anim = GetAnimator()->GetAnim("spiritleave"); + netAnimType = 1; + } + } else { + anim = GetAnimator()->GetAnim("spiritleave"); + netAnimType = 1; + } + } + GetAnimator()->ClearAllAnims( gameLocal.time, spiritBlendTime ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, spiritBlendTime ); +} + +//========================================================================== +// +// hhSpiritProxy::Damage +// +// When damaged, the proxy forces the player to automatically return +//========================================================================== + +void hhSpiritProxy::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + + if( !player.IsValid() || !player->IsSpiritOrDeathwalking() ) { + return; + } + if (gameLocal.isClient) { //rww - don't predict damage on the spirit proxy + return; + } + + if (!gameLocal.isMultiplayer) { //rww - not in mp + // Don't allow the player to be bounced back into the proxy immediately if damaged + if ( gameLocal.time - activationTime < MIN_ACTIVATION_TIME ) { + return; + } + } + + // Save the player, since it is NULLed on this proxy when removed from spiritwalk + hhPlayer *currentPlayer = player.GetEntity(); + + // Disable spiritwalk + // rww - let's defer this to the next frame so other projectiles and things that would impact this frame still hit + if (idEvent::NumQueuedEvents(currentPlayer, &EV_StopSpiritWalk) <= 0) { + currentPlayer->PostEventMS(&EV_StopSpiritWalk, 0); + } + //currentPlayer->StopSpiritWalk(); + + if ( attacker && attacker == player.GetEntity() ) { // If the attacker is the player itself, then just snap back from spirit mode, without doing any damage + return; + } + + // Apply the damage to the player itself + // rww - post this as an event too so we don't take the damage until after we're out of spirit form + float localDmgScale = damageScale; + if (gameLocal.isMultiplayer) { //rww - scale up the damage against bodies in mp, to increase punishment when your body is found + localDmgScale *= 2.0f; + } + currentPlayer->PostEventMS(&EV_DamagePlayer, 1, inflictor, attacker, dir, damageDefName, localDmgScale, location); + //currentPlayer->Damage( inflictor, attacker, dir, damageDefName, localDmgScale, location ); +} + +/* +=============== +hhSpiritProxy::OrientToGravity +=============== +*/ +void hhSpiritProxy::OrientToGravity( bool orient ) { + physicsObj.OrientToGravity( orient ); +} + +/* +=============== +hhSpiritProxy::Event_OrientToGravity +=============== +*/ +void hhSpiritProxy::Event_OrientToGravity( bool orient ) { + OrientToGravity( orient ); +} + +/* +=============== +hhSpiritProxy::Event_AssignSpiritFx +=============== +*/ +void hhSpiritProxy::Event_AssignSpiritFx( hhEntityFx* fx ) { + spiritFx = fx; +} + +/* +=============== +hhSpiritProxy::Event_ResetGravity + +HUMANHEAD: pdm: Posted when entity is leaving a gravity zone +=============== +*/ +void hhSpiritProxy::Event_ResetGravity() { + if( IsWallWalking() ) { + return; // Don't reset if wallwalking + } + + idActor::Event_ResetGravity(); + + OrientToGravity( true ); // let it reset orientation +} + +//========================================================================== +// +// hhSpiritProxy::ShouldRemainAlignedToAxial +// +//========================================================================== +void hhSpiritProxy::ShouldRemainAlignedToAxial( bool remainAligned ) {//HUMANHEAD + physicsObj.ShouldRemainAlignedToAxial( remainAligned ); +} + +//========================================================================== +// +// hhSpiritProxy::Event_ShouldRemainAlignedToAxial +// +//========================================================================== +void hhSpiritProxy::Event_ShouldRemainAlignedToAxial( bool remainAligned ) { + ShouldRemainAlignedToAxial( remainAligned ); +} + +/* +================ +hhSpiritProxy::UpdateModelTransform +mdl: Based on hhPlayer::UpdateModelTransform +================ +*/ +void hhSpiritProxy::UpdateModelTransform( void ) { + if( af.IsActive() ) { + return idActor::UpdateModelTransform(); + } + + idVec3 origin; + idMat3 axis; + + if( GetPhysicsToVisualTransform(origin, axis) ) { + GetRenderEntity()->axis = axis; + GetRenderEntity()->origin = GetPhysics()->GetOrigin() + origin * renderEntity.axis; + } else { + GetRenderEntity()->axis = GetAxis(); + GetRenderEntity()->origin = GetOrigin(); + } +} + +bool hhSpiritProxy::AllowCollision(const trace_t &collision) { + if (collision.fraction < 1.0f && collision.c.entityNum < MAX_CLIENTS && collision.c.entityNum >= 0 && gameLocal.entities[collision.c.entityNum]) { + if (player.GetEntity() == gameLocal.entities[collision.c.entityNum]) { + return false; //do not collide with the owner of this spirit proxy + } + } + return true; +} + +void hhSpiritProxy::Save( idSaveGame *savefile ) const { + savefile->WriteStaticObject( physicsObj ); + + player.Save( savefile ); + + savefile->WriteAngles( viewAngles ); + savefile->WriteMat3( eyeAxis ); + + spiritFx.Save( savefile ); + + savefile->WriteInt( cachedCurrentWeapon ); + savefile->WriteInt( activationTime ); +} + +void hhSpiritProxy::Restore( idRestoreGame *savefile ) { + savefile->ReadStaticObject( physicsObj ); + RestorePhysics( &physicsObj ); + + player.Restore( savefile ); + + savefile->ReadAngles( viewAngles ); + savefile->ReadMat3( eyeAxis ); + + spiritFx.Restore( savefile ); + + savefile->ReadInt( cachedCurrentWeapon ); + savefile->ReadInt( activationTime ); +} + +#define _CHEAP_PROX_SYNC + +void hhSpiritProxy::WriteToSnapshot( idBitMsgDelta &msg ) const { +#ifdef _CHEAP_PROX_SYNC + //we don't want to deal with physics stuff for this proxy on the client i suppose. + //write the origin/axis directly and don't worry about varying physics types. + const idVec3 &origin = renderEntity.origin; + msg.WriteFloat(origin.x); + msg.WriteFloat(origin.y); + msg.WriteFloat(origin.z); +#endif + + idQuat q = renderEntity.axis.ToQuat(); + msg.WriteFloat(q.w); + msg.WriteFloat(q.x); + msg.WriteFloat(q.y); + msg.WriteFloat(q.z); + + msg.WriteBits(fl.hidden, 1); + + msg.WriteBits(player.GetSpawnId(), 32); + + msg.WriteBits(netAnimType, 2); + +#ifndef _CHEAP_PROX_SYNC + msg.WriteBits(physicsObj.GetClipMask(), 32); + msg.WriteBits(physicsObj.GetContents(), 32); + physicsObj.WriteToSnapshot(msg, false); +#endif +} + +void hhSpiritProxy::ReadFromSnapshot( const idBitMsgDelta &msg ) { +#ifdef _CHEAP_PROX_SYNC + idVec3 origin; + origin.x = msg.ReadFloat(); + origin.y = msg.ReadFloat(); + origin.z = msg.ReadFloat(); +#endif + + idQuat q; + q.w = msg.ReadFloat(); + q.x = msg.ReadFloat(); + q.y = msg.ReadFloat(); + q.z = msg.ReadFloat(); + +#ifdef _CHEAP_PROX_SYNC + GetPhysics()->SetOrigin(origin); + + idMat3 axis = q.ToMat3(); + GetPhysics()->SetAxis(axis); + viewAxis = axis; +#else + viewAxis = q.ToMat3(); +#endif + + bool hidden = !!msg.ReadBits(1); + if (hidden != fl.hidden) { + if (hidden) { + Hide(); + } + else { + Show(); + } + } + + player.SetSpawnId(msg.ReadBits(32)); + + netAnimType = msg.ReadBits(2); + + //if we haven't started the animation on the client, then start it + if (!clientAnimated && netAnimType && player.IsValid() && player.GetEntity() && player->IsType(hhPlayer::Type) && player->GetAnimator()) { //verify the owner is still valid + StartAnimation(); + clientAnimated = true; + } + +#ifndef _CHEAP_PROX_SYNC + if (player.IsValid() && GetPhysics() != &physicsObj) { + physicsObj.SetSelf( this ); + physicsObj.SetClipModel( new idClipModel(player->GetPhysics()->GetClipModel()), 1.0f ); + physicsObj.CheckWallWalk( true ); + if( player->IsCrouching() ) { + physicsObj.ForceCrouching(); + } + SetPhysics( &physicsObj ); + } + physicsObj.SetClipMask(msg.ReadBits(32)); + physicsObj.SetContents(msg.ReadBits(32)); + physicsObj.ReadFromSnapshot(msg, false); +#endif +} + +void hhSpiritProxy::ClientPredictionThink( void ) { + Think(); +} + +//proxy needs to clear its icons as well when exiting the snapshot +void hhSpiritProxy::NetZombify(void) { + HidePlayerIcons(); + idActor::NetZombify(); +} + +// Derived from hhMonsterAI::CrashLand() and hhPlayer::CrashLand() -mdl +void hhSpiritProxy::CrashLand( const idVec3 &oldOrigin, const idVec3 &oldVelocity ) { + const trace_t& trace = physicsObj.GetGroundTrace(); + if ( af.IsActive() || (!physicsObj.HasGroundContacts() || trace.fraction == 1.0f) && !IsBound() ) { + return; + } + + //aob - only check when we land on the ground + //If we get here we can assume we currently have ground contacts + if( physicsObj.HadGroundContacts() ) { + return; + } + + // if the monster wasn't going down + if ( ( oldVelocity * -physicsObj.GetGravityNormal() ) >= 0.0f ) { + return; + } + + waterLevel_t waterLevel = physicsObj.GetWaterLevel(); + + // never take falling damage if completely underwater + if ( waterLevel == WATERLEVEL_HEAD ) { + return; + } + + // no falling damage if touching a nodamage surface + bool noDamage = false; + for ( int i = 0; i < physicsObj.GetNumContacts(); i++ ) { + const contactInfo_t &contact = physicsObj.GetContact( i ); + if ( contact.material->GetSurfaceFlags() & SURF_NODAMAGE ) { + noDamage = true; + break; + } + } + + idVec3 deltaVelocity = DetermineDeltaCollisionVelocity( oldVelocity, trace ); + float delta = (IsBound()) ? deltaVelocity.Length() : deltaVelocity * physicsObj.GetGravityNormal(); + + // reduce falling damage if there is standing water + if ( waterLevel == WATERLEVEL_WAIST ) { + delta *= 0.25f; + } + if ( waterLevel == WATERLEVEL_FEET ) { + delta *= 0.5f; + } + + if ( delta < player->crashlandSpeed_jump ) { + return; // Early out + } + + if( trace.fraction == 1.0f ) { + return; + } + + // Determine damage to what you're landing on + idVec3 fallDir = oldVelocity; + fallDir.Normalize(); + float damageScale = hhUtils::CalculateScale( delta, player->crashlandSpeed_soft, player->crashlandSpeed_fatal ); + idVec3 reverseContactNormal = -physicsObj.GetGroundContactNormal(); + idEntity *entity = gameLocal.GetTraceEntity( trace ); + if( entity && trace.c.entityNum != ENTITYNUM_WORLD ) { + entity->ApplyImpulse( this, 0, trace.c.point, (oldVelocity * reverseContactNormal) * reverseContactNormal );//Not sure if this impulse is large enough + + const char* entityDamageName = spawnArgs.GetString( "def_damageFellOnto" ); + if( *entityDamageName && damageScale > 0.0f) { + entity->Damage( this, this, fallDir, entityDamageName, damageScale, INVALID_JOINT ); + } + } + + // Calculate damage to self + const char* selfDamageName = NULL; + if ( delta < player->crashlandSpeed_soft ) { // Soft Fall + //AI_SOFTLANDING = true; + selfDamageName = player->spawnArgs.GetString( "def_damageSoftFall" ); + } + else if ( delta < player->crashlandSpeed_fatal ) { // Hard Fall + //AI_HARDLANDING = true; + selfDamageName = player->spawnArgs.GetString( "def_damageHardFall" ); + } + else { // Fatal Fall + //AI_HARDLANDING = true; + selfDamageName = player->spawnArgs.GetString( "def_damageFatalFall" ); + } + + if( *selfDamageName && damageScale > 0.0f && !noDamage ) { + pain_debounce_time = gameLocal.time + pain_delay + 1; // ignore pain since we'll play our landing anim + hhPlayer *tmp = player.GetEntity(); + player->StopSpiritWalk(true); // Invalidates player + HH_ASSERT(!player.IsValid()); // If it's still valid, we failed to stop spirit walking + tmp->Damage( NULL, NULL, fallDir, selfDamageName, damageScale, INVALID_JOINT ); + } +} + +//========================================================================== +// hhDeathWalkProxy +//========================================================================== + +CLASS_DECLARATION( hhSpiritProxy, hhDeathWalkProxy ) +END_CLASS + +hhDeathProxy::~hhDeathProxy(void) { + HH_ASSERT( gameLocal.isMultiplayer || !player.IsValid() || player->IsDeathWalking() ); +} + +/* +================ +hhDeathWalkProxy::Spawn +================ +*/ +void hhDeathWalkProxy::Spawn() { + initialPos = GetOrigin(); + spawnArgs.GetFloat("bodyMoveScale", "512", bodyMoveScale); + fl.takedamage = false; + timeSinceStage2Started = 0; +} + +void hhDeathWalkProxy::Save(idSaveGame *savefile) const { + savefile->WriteVec3(initialPos); + savefile->WriteFloat(bodyMoveScale); + savefile->WriteInt(timeSinceStage2Started); +} + +void hhDeathWalkProxy::Restore(idRestoreGame *savefile) { + savefile->ReadVec3(initialPos); + savefile->ReadFloat(bodyMoveScale); + savefile->ReadInt(timeSinceStage2Started); +} + +void hhDeathWalkProxy::Think() { + float lerpTime = 0.05f; + + if (!player.IsValid() || !player.GetEntity() || !player->IsType(hhPlayer::Type) || !player->IsDeathWalking()) { + //if player has become invalid or is no longer deathwalking, i wish to be removed. + PostEventMS(&EV_Remove, 0); + return; + } + + hhSpiritProxy::Think(); + + float spiritPercent = player->GetDeathWalkPower() / player->spawnArgs.GetFloat( "deathWalkPowerMax", "1000" ); + + idVec3 newPos = initialPos; + if (player->DeathWalkStage2()) { + timeSinceStage2Started += gameLocal.msec; + if (timeSinceStage2Started > player->spawnArgs.GetInt("deathwalkBodyDropDelayMS")) { + newPos.z = initialPos.z - player->spawnArgs.GetFloat("deathwalkOffsetBelowPortal"); + lerpTime = 0.02f; + } + } + else { + timeSinceStage2Started = 0; + newPos.z = initialPos.z + bodyMoveScale - spiritPercent*bodyMoveScale; + } + + //lerp into that position + idVec3 lerped; + lerped.SLerp(GetOrigin(), newPos, lerpTime ); + SetOrigin(lerped); +} + +void hhDeathWalkProxy::ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3 &bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ) { + hhSpiritProxy::ActivateProxy(owner, origin, bboxAxis, newViewAxis, newViewAngles, newEyeAxis); + + physicsObj.SetContents(0); //not solid + physicsObj.SetGravity(idVec3(0, 0, 0)); //no gravity + + SetInitialPos(origin); //this is our initial position to move vertically from +} + +void hhDeathWalkProxy::SetInitialPos(const idVec3 &pos) { + initialPos = pos; +} + +void hhDeathWalkProxy::StartAnimation() { + int spiritBlendTime = spawnArgs.GetInt( "spiritBlendTime", "400" ); + // Copy the current player animation to blend into the spirit anim + GetAnimator()->CopyAnimations( *( player->GetAnimator() ) ); + + // Play the initial animation + int anim = GetAnimator()->GetAnim("deathfloat"); + GetAnimator()->ClearAllAnims( gameLocal.time, spiritBlendTime ); + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, spiritBlendTime ); + netAnimType = 3; //rww +} + + + +//========================================================================== +// hhDeathProxy +//========================================================================== + +CLASS_DECLARATION( hhSpiritProxy, hhDeathProxy ) + EVENT( EV_SpawnEffect, hhDeathProxy::Event_SpawnEffect ) + EVENT( EV_Activate, hhDeathProxy::Event_Activate ) +END_CLASS + + +void hhDeathProxy::Spawn() { + lastPhysicalLocation.Zero(); + lastPhysicalAxis.Identity(); +} + +void hhDeathProxy::Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ) { + //idActor::Damage( inflictor, attacker, dir, damageDefName, damageScale, location ); +} + +void hhDeathProxy::Event_SpawnEffect() { +} + +void hhDeathProxy::ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3 &bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ) { + hhSpiritProxy::ActivateProxy( owner, origin, bboxAxis, newViewAxis, newViewAngles, newEyeAxis ); + + // Save "return to" location + lastPhysicalLocation = origin; + lastPhysicalAxis = bboxAxis; + + // Ragdoll the proxy + StartRagdoll(); +} + +void hhDeathProxy::StartAnimation() { + if (gameLocal.isMultiplayer) { //rww + UpdateModelForPlayer(); //update proxy model for whatever the player is using. + } + // Set the start of the death proxy to the current animation in the player + GetAnimator()->CopyAnimations( *( player->GetAnimator() ) ); + GetAnimator()->CopyPoses( *( player->GetAnimator() ) ); + netAnimType = 3; //rww +} + +// mdl: Handy for debugging the death proxy +void hhDeathProxy::Event_Activate() { + hhPlayer *player = reinterpret_cast (gameLocal.GetLocalPlayer()); + ActivateProxy(player, GetPhysics()->GetOrigin(), GetPhysics()->GetAxis(), viewAxis, viewAngles, player->GetEyeAxis()); +} + + + +//========================================================================== +// hhMPDeathProxy +//========================================================================== + +idCVar g_mpPlayerRagdollLife( "g_mpPlayerRagdollLife", "1500", CVAR_GAME | CVAR_INTEGER, "player ragdoll life" ); +idCVar g_mpSyncPlayerRagdoll( "g_mpSyncPlayerRagdoll", "0", CVAR_GAME | CVAR_BOOL, "whether to sync mp ragdolls" ); + +const idEventDef EV_CorpseRemove( "", NULL ); + +CLASS_DECLARATION( hhDeathProxy, hhMPDeathProxy ) + EVENT( EV_CorpseRemove, hhMPDeathProxy::Event_CorpseRemove ) +END_CLASS + + +/* +================ +hhMPDeathProxy::Spawn +================ +*/ +void hhMPDeathProxy::Spawn() { + hasInitial = false; + didFling = false; + + int delay = g_mpPlayerRagdollLife.GetInteger(); + fl.clientEvents = true; + PostEventMS(&EV_CorpseRemove, delay); + + SetShaderParm(SHADERPARM_TIME_OF_DEATH, MS2SEC(gameLocal.time+1000)); + SetSkinByName(spawnArgs.GetString("skin_death")); + //GetPhysics()->SetGravity(idVec3(0,0,512)); cheesy spirit floating upward effect (doesn't work inside grav zones) +} + +/* +================ +hhMPDeathProxy::ActivateProxy +================ +*/ +void hhMPDeathProxy::ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3 &bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ) { + hhDeathProxy::ActivateProxy(owner, origin, bboxAxis, newViewAxis, newViewAngles, newEyeAxis); + + hasInitial = true; + initialPos = origin; + initialRot = bboxAxis.ToCQuat(); + + GetPhysics()->SetOrigin(initialPos); + GetPhysics()->SetAxis(initialRot.ToMat3()); +} + +/* +================ +hhMPDeathProxy::Event_CorpseRemove +================ +*/ +void hhMPDeathProxy::Event_CorpseRemove(void) { + Hide(); + GetPhysics()->SetContents(0); + if (!gameLocal.isClient) { + PostEventMS(&EV_Remove, 1000); + } +} + +/* +================ +hhMPDeathProxy::ActivateProxy +================ +*/ +void hhMPDeathProxy::SetFling(const idVec3 &point, const idVec3 &force) { + //initialFlingPoint = point; + initialFlingForce = force; +} + +/* +================ +hhMPDeathProxy::WriteToSnapshot +================ +*/ +void hhMPDeathProxy::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(player.GetSpawnId(), 32); + + msg.WriteBits(IsActiveAF(), 1); + + bool physSync = g_mpSyncPlayerRagdoll.GetBool(); //the server can decide to do this. + msg.WriteBits(physSync, 1); + if (physSync) { + GetPhysics()->WriteToSnapshot(msg); + } + else { //if we are not properly sync'ing then use just an initial orientation + msg.WriteFloat(initialPos.x); + msg.WriteFloat(initialPos.y); + msg.WriteFloat(initialPos.z); + + msg.WriteFloat(initialRot.x); + msg.WriteFloat(initialRot.y); + msg.WriteFloat(initialRot.z); + + //FIXME get some of this from the server? may be necessary if a more complex method of determining the fling direction is devised. + /* + msg.WriteFloat(initialFlingPoint.x); + msg.WriteFloat(initialFlingPoint.y); + msg.WriteFloat(initialFlingPoint.z); + */ + + msg.WriteFloat(initialFlingForce.x); + msg.WriteFloat(initialFlingForce.y); + msg.WriteFloat(initialFlingForce.z); + } +} + +/* +================ +hhMPDeathProxy::ReadFromSnapshot +================ +*/ +void hhMPDeathProxy::ReadFromSnapshot( const idBitMsgDelta &msg ) { + player.SetSpawnId(msg.ReadBits(32)); + + bool isRagging = !!msg.ReadBits(1); + + bool physSync = !!msg.ReadBits(1); + + if (physSync) { + if (isRagging && !IsActiveAF()) { //then start ragging on the client + if (player.IsValid()) { //update model for player before initiating ragdoll + UpdateModelForPlayer(); + } + StartRagdoll(); + } + GetPhysics()->ReadFromSnapshot(msg); + } + else { //if we are not properly sync'ing then use just an initial orientation + initialPos.x = msg.ReadFloat(); + initialPos.y = msg.ReadFloat(); + initialPos.z = msg.ReadFloat(); + + initialRot.x = msg.ReadFloat(); + initialRot.y = msg.ReadFloat(); + initialRot.z = msg.ReadFloat(); + + initialFlingForce.x = msg.ReadFloat(); + initialFlingForce.y = msg.ReadFloat(); + initialFlingForce.z = msg.ReadFloat(); + + if (!hasInitial) { //if not set at all yet, put it here. + GetPhysics()->SetOrigin(initialPos); //set before we start to ragdoll, so that the af starts out at a valid orientation + GetPhysics()->SetAxis(initialRot.ToMat3()); + + hasInitial = true; + } + + if (isRagging && !IsActiveAF()) { //then start ragging on the client + if (player.IsValid()) { //update model for player before initiating ragdoll + UpdateModelForPlayer(); + } + StartRagdoll(); + + //set the initial orientation again after beginning ragdoll + GetPhysics()->SetOrigin(initialPos); + GetPhysics()->SetAxis(initialRot.ToMat3()); + } + } +} + +/* +================ +hhMPDeathProxy::ClientPredictionThink +================ +*/ +void hhMPDeathProxy::ClientPredictionThink( void ) { + if (!gameLocal.isNewFrame) { + return; + } + + Think(); + if (hasInitial && !didFling) { + GetPhysics()->AddForce(0, GetPhysics()->GetOrigin(0), initialFlingForce*256.0f*256.0f); + didFling = true; + } +} + + +//========================================================================== +// +// hhPossessedProxy +// +// A version of the spirit proxy spawned when the player is possessed +//========================================================================== + +CLASS_DECLARATION( hhSpiritProxy, hhPossessedProxy ) +END_CLASS + +void hhPossessedProxy::ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3 &bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ) { + hhSpiritProxy::ActivateProxy(owner, origin, bboxAxis, newViewAxis, newViewAngles, newEyeAxis); + + Hide(); + physicsObj.SetContents( 0 ); +} + +/* +=============== +hhSpiritProxy::DrawPlayerIcons +=============== +*/ +void hhSpiritProxy::DrawPlayerIcons( void ) { + if ( !NeedsIcon() ) { + //playerIcon.FreeIcon(); + playerTeamIcon.FreeIcon(); + return; + } + //player->UpdatePlayerIcons(); //update the owner's icon status + //playerIcon.Draw( this, INVALID_JOINT ); + playerTeamIcon.Draw( this, INVALID_JOINT ); +} + +/* +=============== +hhSpiritProxy::HidePlayerIcons +=============== +*/ +void hhSpiritProxy::HidePlayerIcons( void ) { + //playerIcon.FreeIcon(); + playerTeamIcon.FreeIcon(); +} + +/* +=============== +hhSpiritProxy::NeedsIcon +============== +*/ +bool hhSpiritProxy::NeedsIcon( void ) { + if (IsType(hhMPDeathProxy::Type)) { + return false; + } + if (!player.IsValid()) { + return false; + } + if (IsHidden()) { + return false; + } + return player->entityNumber != gameLocal.localClientNum && ( /*player->isLagged || player->isChatting ||*/ gameLocal.gameType == GAME_TDM ) && gameLocal.EntInClientSnapshot(entityNumber); +} diff --git a/src/Prey/prey_spiritproxy.h b/src/Prey/prey_spiritproxy.h new file mode 100644 index 0000000..40ebada --- /dev/null +++ b/src/Prey/prey_spiritproxy.h @@ -0,0 +1,200 @@ + +#ifndef __PREY_SPIRITPROXY_H__ +#define __PREY_SPIRITPROXY_H__ + +// nla - Forward declare +class hhPlayer; + +extern const idEventDef EV_OrientToGravity; + +// SPIRIT PROXY =============================================================== + +class hhSpiritProxy : public idActor { +public: + CLASS_PROTOTYPE( hhSpiritProxy ); + hhSpiritProxy( void ); + void Spawn( void ); + static hhSpiritProxy *CreateProxy( const char *name, hhPlayer *owner, const idVec3& origin, const idMat3& bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ); + + virtual void SetModel( const char *modelname ); //rww + + virtual void UpdateModelForPlayer(void); //rww + virtual void Think(); + virtual void ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3& bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ); + virtual void DeactivateProxy( void ); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual bool IsWallWalking() const; + virtual bool IsCrouching() const; + virtual bool ShouldRemainAlignedToAxial(); + virtual void RestorePlayerLocation( const idVec3& origin, const idMat3& bboxAxis, const idVec3& viewDir, const idAngles& angles ); + virtual void StartAnimation(); + hhPlayer* GetPlayer(void) const { return player.GetEntity(); } // JRM + void OrientToGravity( bool orient ); + virtual void ShouldRemainAlignedToAxial( bool remainAligned ); + virtual bool ShouldRemainAlignedToAxial() const { return physicsObj.ShouldRemainAlignedToAxial(); } + virtual void UpdateModelTransform( void ); // mdl + virtual bool AllowCollision(const trace_t &collision); //rww + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network code + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + virtual void NetZombify(void); + + //rww - for mp player icons transferring to the prox + void DrawPlayerIcons( void ); + void HidePlayerIcons( void ); + bool NeedsIcon( void ); + + ID_INLINE float GetActivationTime( void ) const { return activationTime; } + +public: + //Overridden methods + virtual void SetAxis( const idMat3& axis ) { idEntity::SetAxis( axis ); } + + void Event_SpawnEffect(); + void Event_OrientToGravity( bool orient ); + void Event_ResetGravity(); + void Event_ShouldRemainAlignedToAxial( bool remainAligned ); + void Event_AssignSpiritFx( hhEntityFx* fx ); + +protected: + void CrashLand(const idVec3 &oldOrigin, const idVec3 &oldVelocity); + + hhPhysics_Player physicsObj; + + idEntityPtr player; + + idAngles viewAngles; + idMat3 eyeAxis; + + idEntityPtr spiritFx; + + int cachedCurrentWeapon; + int activationTime; // Saved to keep the player from being bounced back into the body too quickly if damaged + + //rww - only matters for client keeping track of if animation has started. no need to save/restore. + bool clientAnimated; + //rww - only for telling clients which anim we are playing + int netAnimType; + + //rww - for keeping track of player model changes in mp + int playerModelNum; + + //rww - showing player status icons on the proxy in mp + //proxy could optionally support lag/chat/etc if we wanted to sync those states on the proxy itself + //idPlayerIcon playerIcon; + hhPlayerTeamIcon playerTeamIcon; +}; + +/* +===================== +hhSpiritProxy::IsWallWalking +===================== +*/ +ID_INLINE bool hhSpiritProxy::IsWallWalking() const { + return physicsObj.IsWallWalking(); +} + +/* +===================== +hhSpiritProxy::IsCrouching +===================== +*/ +ID_INLINE bool hhSpiritProxy::IsCrouching() const { + return physicsObj.IsCrouching(); +} + +/* +===================== +hhSpiritProxy::ShouldRemainAlignedToAxial +===================== +*/ +ID_INLINE bool hhSpiritProxy::ShouldRemainAlignedToAxial() { + return physicsObj.ShouldRemainAlignedToAxial(); +} + +// DEATH PROXY ================================================================ + +class hhDeathProxy : public hhSpiritProxy { +public: + CLASS_PROTOTYPE( hhDeathProxy ); + + ~hhDeathProxy(); + + void Spawn(); + virtual void Damage( idEntity *inflictor, idEntity *attacker, const idVec3 &dir, const char *damageDefName, const float damageScale, const int location ); + virtual void ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3 &bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ); + virtual void StartAnimation(); + +protected: + void Event_SpawnEffect(); + void Event_Activate(); + +protected: + idVec3 lastPhysicalLocation; // location to return to from deathwalk + idMat3 lastPhysicalAxis; // orientation to return to from deathwalk +}; + +// MP DEATHWALK PROXY ================================================================ +//rww - for corpses in multiplayer +class hhMPDeathProxy : public hhDeathProxy { +public: + CLASS_PROTOTYPE( hhMPDeathProxy ); + + void Spawn(); + virtual void ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3 &bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ); + + virtual void Event_CorpseRemove(void); + + void SetFling(const idVec3 &point, const idVec3 &force); + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + virtual void ClientPredictionThink( void ); + +private: + bool hasInitial; + bool didFling; + idVec3 initialPos; + idCQuat initialRot; + idVec3 initialFlingForce; +}; + +// DEATHWALK PROXY ================================================================ +//rww - this is the entity that sits in the middle of deathwalk and moves up/down. + +class hhDeathWalkProxy : public hhSpiritProxy { +public: + CLASS_PROTOTYPE( hhDeathWalkProxy ); + + void Spawn(); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + virtual void Think(); + virtual void ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3& bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ); + void SetInitialPos(const idVec3 &pos); + virtual void StartAnimation(); + +protected: + idVec3 initialPos; + float bodyMoveScale; + int timeSinceStage2Started; +}; + +// POSSESSED PROXY ================================================================ +// An invisible proxy that is spawned when the player is possessed + +class hhPossessedProxy : public hhSpiritProxy { +public: + CLASS_PROTOTYPE( hhPossessedProxy ); + + virtual void ActivateProxy( hhPlayer *owner, const idVec3& origin, const idMat3& bboxAxis, const idMat3& newViewAxis, const idAngles& newViewAngles, const idMat3& newEyeAxis ); + +protected: +}; + +#endif /* __PREY_SPIRITPROXY_H__ */ diff --git a/src/Prey/prey_spiritsecret.cpp b/src/Prey/prey_spiritsecret.cpp new file mode 100644 index 0000000..3f7a362 --- /dev/null +++ b/src/Prey/prey_spiritsecret.cpp @@ -0,0 +1,72 @@ +//************************************************************************** +//** +//** PREY_SPIRITSECRET.CPP +//** +//** Game code for the spirit secret entities +//** Spirit secret entities are entities that are only visible +//** and blockable in normal mode. +//************************************************************************** + +// HEADER FILES ------------------------------------------------------------ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +// MACROS ------------------------------------------------------------------ + +// TYPES ------------------------------------------------------------------- + +// CLASS DECLARATIONS ------------------------------------------------------ + +CLASS_DECLARATION( idEntity, hhSpiritSecret ) + EVENT( EV_Activate, hhSpiritSecret::Event_Activate ) +END_CLASS + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// hhSpiritSecret::Spawn +// +//========================================================================== + +void hhSpiritSecret::Spawn(void) { + fl.takedamage = false; + + if ( !spawnArgs.GetInt( "start_off" ) ) { + GetPhysics()->SetContents( CONTENTS_FORCEFIELD | CONTENTS_SHOOTABLE ); + } else { // Start the secret off + GetPhysics()->SetContents( 0 ); + Hide(); + } + + SetShaderParm( SHADERPARM_MODE, gameLocal.random.CRandomFloat() ); +} + +//========================================================================== +// +// hhSpiritSecret::Event_Activate +// +//========================================================================== + +void hhSpiritSecret::Event_Activate( idEntity *activator ) { + if ( IsHidden() ) { + Show(); + GetPhysics()->SetContents( CONTENTS_FORCEFIELD | CONTENTS_SHOOTABLE ); + } else { + Hide(); + GetPhysics()->SetContents( 0 ); + } +} diff --git a/src/Prey/prey_spiritsecret.h b/src/Prey/prey_spiritsecret.h new file mode 100644 index 0000000..52c47d1 --- /dev/null +++ b/src/Prey/prey_spiritsecret.h @@ -0,0 +1,15 @@ + +#ifndef __PREY_SPIRITSECRET_H__ +#define __PREY_SPIRITSECRET_H__ + +class hhSpiritSecret : public idEntity { +public: + CLASS_PROTOTYPE( hhSpiritSecret ); + + void Spawn( void ); + +protected: + void Event_Activate( idEntity *activator ); +}; + +#endif /* __PREY_SPIRITSECRET_H__ */ diff --git a/src/Prey/prey_vehiclefirecontroller.cpp b/src/Prey/prey_vehiclefirecontroller.cpp new file mode 100644 index 0000000..1b6f561 --- /dev/null +++ b/src/Prey/prey_vehiclefirecontroller.cpp @@ -0,0 +1,225 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhFireController, hhVehicleFireController ) +END_CLASS + +/* +================ +hhVehicleFireController::Init +================ +*/ +void hhVehicleFireController::Init( const idDict* viewDict, hhVehicle* self, idActor* owner ) { + hhFireController::Init( viewDict ); + + this->owner = owner; + this->self = self; + + recoil = dict->GetFloat( "recoil" ); + + if ( owner && owner->IsType( idAI::Type ) ) { + spread = dict->GetFloat( "monster_spread" ); + } + + SetBarrelOffsetList( "muzzleOffset", barrelOffsets ); +} + +/* +================ +hhVehicleFireController::UsesCrosshair +================ +*/ +bool hhVehicleFireController::UsesCrosshair() const { + return bCrosshair; +} + +/* +================ +hhVehicleFireController::Clear +================ +*/ +void hhVehicleFireController::Clear() { + hhFireController::Clear(); + + nextFireTime = gameLocal.GetTime(); + barrelOffsets.Clear(); + + owner = NULL; + self = NULL; +} + +/* +================ +hhVehicleFireController::Save +================ +*/ +void hhVehicleFireController::Save( idSaveGame *savefile ) const { + self.Save( savefile ); + owner.Save( savefile ); + savefile->WriteFloat( recoil ); + + int num = barrelOffsets.Num(); + savefile->WriteInt( num ); + for( int i = 0; i < num; i++ ) { + savefile->WriteVec3( barrelOffsets[i] ); + } + + savefile->WriteInt( nextFireTime ); +} + +/* +================ +hhVehicleFireController::Restore +================ +*/ +void hhVehicleFireController::Restore( idRestoreGame *savefile ) { + self.Restore( savefile ); + owner.Restore( savefile ); + savefile->ReadFloat( recoil ); + + int num; + savefile->ReadInt( num ); + idVec3 tmp; + for( int i = 0; i < num; i++ ) { + savefile->ReadVec3( tmp ); + barrelOffsets.Append( tmp ); + } + + savefile->ReadInt( nextFireTime ); +} + +/* +================ +hhVehicleFireController::WeaponFeedback +================ +*/ +void hhVehicleFireController::WeaponFeedback() { + if( self.IsValid() && self->GetPhysics() ) { + hhPhysics_Vehicle* selfPhysics = static_cast( self->GetPhysics() ); + self->ApplyImpulse( gameLocal.world, 0, self->GetOrigin() + selfPhysics->GetCenterOfMass(), -self->GetAxis()[0] * GetRecoil() * selfPhysics->GetMass() ); + } +} + +/* +================ +hhVehicleFireController::SetBarrelOffsetList +================ +*/ +void hhVehicleFireController::SetBarrelOffsetList( const char* keyPrefix, hhCycleList& offsetList ) { + const idKeyValue* kv = dict->MatchPrefix( keyPrefix ); + while( kv ) { + offsetList.Append( dict->GetVector(kv->GetKey().c_str()) ); + kv = dict->MatchPrefix( keyPrefix, kv ); + } +} + +/* +================= +hhVehicleFireController::DetermineAimAxis +================= +*/ +idMat3 hhVehicleFireController::DetermineAimAxis( const idVec3& muzzlePos, const idMat3& weaponAxis ) { + idAngles aimAngles; + idVec3 aimPos; + + aimPos = idVec3( 0.0f, 0.0f, owner->EyeHeight() ) + dict->GetVector( "offset_gunTarget" ); + aimPos *= self->GetFireAxis(); + aimPos += self->GetFireOrigin(); + + aimAngles = (aimPos - muzzlePos).ToAngles(); + aimAngles[2] = weaponAxis.ToAngles()[2]; + return aimAngles.ToMat3(); +} + +/* +================ +hhVehicleFireController::LaunchProjectiles +================ +*/ +bool hhVehicleFireController::LaunchProjectiles( const idVec3& pushVelocity ) { + if( nextFireTime > gameLocal.GetTime() ) { + return false; + } + + if( !hhFireController::LaunchProjectiles(pushVelocity) ) { + return false; + } + + gameLocal.AlertAI( owner.GetEntity() ); + nextFireTime = gameLocal.GetTime() + SEC2MS( GetFireDelay() ); + return true; +} + +/* +================ +hhFireController::AmmoAvailable +================ +*/ +int hhVehicleFireController::AmmoAvailable() const { + if ( self.IsValid() ) { + return self->HasPower( AmmoRequired() ); + } else { + return 0; + } +} + +/* +================ +hhFireController::UseAmmo +================ +*/ +void hhVehicleFireController::UseAmmo() { + if( self.IsValid() ) { + self->ConsumePower( AmmoRequired() ); + } +} + +/* +================ +hhVehicleFireController::GetCollisionBBox +================ +*/ +const idBounds& hhVehicleFireController::GetCollisionBBox() { + return self->GetPhysics()->GetAbsBounds(); +} + +/* +================ +hhVehicleFireController::CalculateMuzzlePosition +================ +*/ +void hhVehicleFireController::CalculateMuzzlePosition( idVec3& origin, idMat3& axis ) { + axis = self->GetFireAxis(); + origin = barrelOffsets.Next() * axis + self->GetFireOrigin(); +} + +/* +================ +hhVehicleFireController::GetProjectileOwner +================ +*/ +idEntity *hhVehicleFireController::GetProjectileOwner() const { + return self.GetEntity(); +} + +/* +================ +hhVehicleFireController::GetSelf +================ +*/ +hhRenderEntity *hhVehicleFireController::GetSelf() { + return self.GetEntity(); +} + +/* +================ +hhVehicleFireController::GetSelfConst +================ +*/ +const hhRenderEntity *hhVehicleFireController::GetSelfConst() const { + return self.GetEntity(); +} + diff --git a/src/Prey/prey_vehiclefirecontroller.h b/src/Prey/prey_vehiclefirecontroller.h new file mode 100644 index 0000000..f0e8b76 --- /dev/null +++ b/src/Prey/prey_vehiclefirecontroller.h @@ -0,0 +1,47 @@ +#ifndef __HH_VEHICLE_FIRE_CONTROLLER_H +#define __HH_VEHICLE_FIRE_CONTROLLER_H + +#include "gamesys/Class.h" + +class hhVehicleFireController : public hhFireController { + CLASS_PROTOTYPE(hhVehicleFireController); + +public: + virtual void Clear(); + virtual void Init( const idDict* viewDict, hhVehicle* self, idActor* owner ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool LaunchProjectiles( const idVec3& pushVelocity ); + virtual void WeaponFeedback(); + virtual idMat3 DetermineAimAxis( const idVec3& muzzlePos, const idMat3& weaponAxis ); + + virtual int AmmoAvailable() const; + virtual void UseAmmo(); + + virtual float GetRecoil() const { return recoil; } + virtual bool UsesCrosshair() const; + + //rww - made public + hhCycleList barrelOffsets; + +protected: + virtual const idBounds& GetCollisionBBox(); + virtual idEntity* GetProjectileOwner() const; + void SetBarrelOffsetList( const char* keyPrefix, hhCycleList& offsetList ); + void SaveBarrelOffsetList( const hhCycleList& offsetList, idSaveGame *savefile ) const; + void RestoreBarrelOffsetList( hhCycleList& offsetList, idRestoreGame *savefile ); + + virtual void CalculateMuzzlePosition( idVec3& origin, idMat3& axis ); + virtual hhRenderEntity *GetSelf(); + virtual const hhRenderEntity *GetSelfConst() const; + +protected: + idEntityPtr self; + idEntityPtr owner; + + float recoil; + int nextFireTime; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weapon.cpp b/src/Prey/prey_weapon.cpp new file mode 100644 index 0000000..fee20a2 --- /dev/null +++ b/src/Prey/prey_weapon.cpp @@ -0,0 +1,4 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" diff --git a/src/Prey/prey_weapon.h b/src/Prey/prey_weapon.h new file mode 100644 index 0000000..b8b460b --- /dev/null +++ b/src/Prey/prey_weapon.h @@ -0,0 +1,5 @@ +#ifndef __PREY_WEAPON_H__ +#define __PREY_WEAPON_H__ + + +#endif /* !__PREY_WEAPON_H__ */ diff --git a/src/Prey/prey_weaponautocannon.cpp b/src/Prey/prey_weaponautocannon.cpp new file mode 100644 index 0000000..2755f86 --- /dev/null +++ b/src/Prey/prey_weaponautocannon.cpp @@ -0,0 +1,363 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhWeaponAutoCannon + +***********************************************************************/ +const idEventDef EV_Weapon_AdjustHeat( "adjustHeat", "f" ); +const idEventDef EV_Weapon_GetHeatLevel( "getHeatLevel", "", 'f' ); + +const idEventDef EV_Weapon_SpawnRearGasFX( "spawnRearGasFX" ); +const idEventDef EV_SpawnSparkLocal( "", "s" ); + +const idEventDef EV_Broadcast_AssignLeftRearFx( "", "e" ); +const idEventDef EV_Broadcast_AssignRightRearFx( "", "e" ); + +const idEventDef EV_Weapon_OverHeatNetEvent( "overHeatNetEvent" ); //rww + +CLASS_DECLARATION( hhWeapon, hhWeaponAutoCannon ) + EVENT( EV_Weapon_AdjustHeat, hhWeaponAutoCannon::Event_AdjustHeat ) + EVENT( EV_Weapon_GetHeatLevel, hhWeaponAutoCannon::Event_GetHeatLevel ) + EVENT( EV_Weapon_SpawnRearGasFX, hhWeaponAutoCannon::Event_SpawnRearGasFX ) + EVENT( EV_SpawnSparkLocal, hhWeaponAutoCannon::Event_SpawnSparkLocal ) + EVENT( EV_Broadcast_AssignLeftRearFx, hhWeaponAutoCannon::Event_AssignLeftRearFx ) + EVENT( EV_Broadcast_AssignRightRearFx, hhWeaponAutoCannon::Event_AssignRightRearFx ) + EVENT( EV_Weapon_OverHeatNetEvent, hhWeaponAutoCannon::Event_OverHeatNetEvent ) +END_CLASS + +/* +================ +hhWeaponAutoCannon::Spawn +================ +*/ +void hhWeaponAutoCannon::Spawn() { + BecomeActive( TH_TICKER ); + + beamSystem.Clear(); + rearGasFxL.Clear(); + rearGasFxR.Clear(); +} + +/* +================ +hhWeaponAutoCannon::~hhWeaponAutoCannon +================ +*/ +hhWeaponAutoCannon::~hhWeaponAutoCannon() { + SAFE_REMOVE( beamSystem ); + SAFE_REMOVE( rearGasFxL ); + SAFE_REMOVE( rearGasFxR ); +} + +/* +================ +hhWeaponAutoCannon::ParseDef +================ +*/ +void hhWeaponAutoCannon::ParseDef( const char *objectname ) { + hhWeapon::ParseDef( objectname ); + + SetHeatLevel( 0.0f ); + + InitBoneInfo(); + + if (owner.IsValid() && owner.GetEntity() && owner.GetEntity() == gameLocal.GetLocalPlayer()) { //rww - let's make the beam locally, since it is a client entity. no need for the server to do anything. + ProcessEvent( &EV_SpawnSparkLocal, dict->GetString("beam_spark") ); + } + //BroadcastBeam( dict->GetString("beam_spark"), EV_SpawnSparkLocal ); +} + +/* +================ +hhWeaponAutoCannon::UpdateGUI +================ +*/ +void hhWeaponAutoCannon::UpdateGUI() { + if ( GetRenderEntity()->gui[ 0 ] && state != idStr(WP_HOLSTERED) ) { + GetRenderEntity()->gui[ 0 ]->SetStateFloat( "temperature", GetHeatLevel() ); + } +} + +/* +================ +hhWeaponAutoCannon::Ticker +================ +*/ +void hhWeaponAutoCannon::Ticker() { + idVec3 boneOriginL, boneOriginR; + idMat3 boneAxisL, boneAxisR; + + if( beamSystem.IsValid() ) { + GetJointWorldTransform( sparkBoneL, boneOriginL, boneAxisL ); + GetJointWorldTransform( sparkBoneR, boneOriginR, boneAxisR ); + + if( (boneOriginL - boneOriginR).Length() > sparkGapSize ) { + if( !beamSystem->IsHidden() ) { + beamSystem->Hide(); + SetShaderParm( SHADERPARM_MODE, 0.0f ); + } + } else if ( owner->CanShowWeaponViewmodel() ) { + if( beamSystem->IsHidden() ) { + beamSystem->Show(); + SetShaderParm( SHADERPARM_MODE, 1.0f ); + } + } + + beamSystem->SetOrigin( boneOriginL ); + beamSystem->SetTargetLocation( boneOriginR ); + } +} + +/* +================ +hhWeaponAutoCannon::InitBoneInfo +================ +*/ +void hhWeaponAutoCannon::InitBoneInfo() { + assert( dict ); + + GetJointHandle( dict->GetString("joint_sparkL"), sparkBoneL ); + GetJointHandle( dict->GetString("joint_sparkR"), sparkBoneR ); +} + +/* +================ +hhWeaponAutoCannon::SetHeatLevel +================ +*/ +void hhWeaponAutoCannon::SetHeatLevel( const float heatLevel ) { + this->heatLevel = heatLevel; + + SetShaderParm( SHADERPARM_MISC, heatLevel ); +} + +/* +================ +hhWeaponAutoCannon::AdjustHeat +================ +*/ +void hhWeaponAutoCannon::AdjustHeat( const float amount ) { + SetHeatLevel( hhMath::ClampFloat(0.0f, 1.0f, GetHeatLevel() + amount) ); + +//#if _DEBUG +// gameLocal.Printf("HeatLevel: %.2f\n", GetHeatLevel()); +//#endif +} + +/* +================ +hhWeaponAutoCannon::PresentWeapon +================ +*/ +void hhWeaponAutoCannon::PresentWeapon( bool showViewModel ) { + if( IsHidden() || !owner->CanShowWeaponViewmodel() || pm_thirdPerson.GetBool() ) { + if( beamSystem.IsValid() ) { + beamSystem->Activate( false ); + } + } else { + if( beamSystem.IsValid() ) { + beamSystem->Activate( true ); + } + } + + hhWeapon::PresentWeapon( showViewModel ); +} + +void hhWeaponAutoCannon::Show() { + if ( beamSystem.IsValid() ) + beamSystem->Show(); + hhWeapon::Show(); +} + +void hhWeaponAutoCannon::Hide() { + if ( beamSystem.IsValid() ) + beamSystem->Hide(); + hhWeapon::Hide(); +} + +void hhWeaponAutoCannon::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhWeapon::WriteToSnapshot( msg ); + + msg.WriteFloat( GetHeatLevel() ); +} + +void hhWeaponAutoCannon::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhWeapon::ReadFromSnapshot( msg ); + + SetHeatLevel( msg.ReadFloat() ); +} + +/* +================ +hhWeaponAutoCannon::Event_SpawnSparkLocal +================ +*/ +void hhWeaponAutoCannon::Event_SpawnSparkLocal( const char* defName ) { + assert(dict); //rww - this could happen if the server sent a beam event before we initialized the weap on the client + sparkGapSize = dict->GetFloat( "sparkGapSize" ); + + SAFE_REMOVE( beamSystem ); + beamSystem = hhBeamSystem::SpawnBeam( GetOrigin(), defName, mat3_identity, true ); + if( !beamSystem.IsValid() ) { + return; + } + + //rww - this particular beam not a network entity + beamSystem->fl.networkSync = false; + beamSystem->fl.clientEvents = true; + + beamSystem->fl.neverDormant = true; + beamSystem->GetRenderEntity()->weaponDepthHack = true; + beamSystem->GetRenderEntity()->allowSurfaceInViewID = owner->entityNumber + 1; +} + +/* +================ +hhWeaponAutoCannon::Event_SpawnRearGasFX +================ +*/ +void hhWeaponAutoCannon::Event_SpawnRearGasFX() { + if( !owner->CanShowWeaponViewmodel() || pm_thirdPerson.GetBool() ) + return; + + hhFxInfo fxInfo; + + float fxHeatThreshold = hhMath::ClampFloat( 0.0f, 1.0f, dict->GetFloat("heatThreshold") ); + if( fxHeatThreshold >= GetHeatLevel() ) { + return; + } + + //Checking if fx's are done yet. Only want to get in when fx's are done + if( !rearGasFxL.IsValid() && !rearGasFxR.IsValid() ) { + fxInfo.UseWeaponDepthHack( true ); + fxInfo.RemoveWhenDone( true ); + + BroadcastFxInfoAlongBone( dict->RandomPrefix("fx_rearGas", gameLocal.random), dict->GetString("joint_rearGasFxL"), &fxInfo, &EV_Broadcast_AssignLeftRearFx, false ); + BroadcastFxInfoAlongBone( dict->RandomPrefix("fx_rearGas", gameLocal.random), dict->GetString("joint_rearGasFxR"), &fxInfo, &EV_Broadcast_AssignRightRearFx, false ); + + if( GetHeatLevel() >= 1.0f ) + StartSound( "snd_overheat", SND_CHANNEL_BODY, 0, false, NULL ); + else + StartSound( "snd_steam_vent", SND_CHANNEL_BODY, 0, false, NULL ); + } +} + +/* +================ +hhWeaponAutoCannon::Event_AssignLeftRearFx +================ +*/ +void hhWeaponAutoCannon::Event_AssignLeftRearFx( hhEntityFx* fx ) { + rearGasFxL = fx; +} + +/* +================ +hhWeaponAutoCannon::Event_AssignRightRearFx +================ +*/ +void hhWeaponAutoCannon::Event_AssignRightRearFx( hhEntityFx* fx ) { + rearGasFxR = fx; +} + +/* +================ +hhWeaponAutoCannon::Event_AdjustHeat +================ +*/ +void hhWeaponAutoCannon::Event_AdjustHeat( const float fAmount ) { + if (gameLocal.isClient) { //rww - don't adjust on client + return; + } + AdjustHeat( fAmount ); +} + +/* +================ +hhWeaponAutoCannon::Event_GetHeatLevel +================ +*/ +void hhWeaponAutoCannon::Event_GetHeatLevel() { + idThread::ReturnFloat( GetHeatLevel() ); +} + +/* +================ +hhWeaponAutoCannon::Event_OverHeatNetEvent +================ +*/ +void hhWeaponAutoCannon::Event_OverHeatNetEvent() { //rww + if (gameLocal.isClient) { + return; + } + + idBitMsg msg; + byte msgBuf[MAX_EVENT_PARAM_SIZE]; + + msg.Init( msgBuf, sizeof( msgBuf ) ); + ServerSendEvent( EVENT_OVERHEAT, &msg, false, -1 ); +} + +/* +================ +hhWeaponAutoCannon::ClientReceiveEvent +================ +*/ +bool hhWeaponAutoCannon::ClientReceiveEvent( int event, int time, const idBitMsg &msg ) { //rww + switch (event) { + case EVENT_OVERHEAT: { + SetState("OverHeated", 10); + return true; + } + default: { + return hhWeapon::ClientReceiveEvent( event, time, msg ); + } + } +} + + +/* +================ +hhWeaponAutoCannon::Save +================ +*/ +void hhWeaponAutoCannon::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( heatLevel ); + savefile->WriteFloat( sparkGapSize ); + + beamSystem.Save( savefile ); + + savefile->WriteInt( sparkBoneL.view ); + savefile->WriteInt( sparkBoneL.world ); + + savefile->WriteInt( sparkBoneR.view ); + savefile->WriteInt( sparkBoneR.world ); + + rearGasFxL.Save( savefile ); + rearGasFxR.Save( savefile ); +} + +/* +================ +hhWeaponAutoCannon::Restore +================ +*/ +void hhWeaponAutoCannon::Restore( idRestoreGame *savefile ) { + savefile->ReadFloat( heatLevel ); + savefile->ReadFloat( sparkGapSize ); + + beamSystem.Restore( savefile ); + + savefile->ReadInt( reinterpret_cast ( sparkBoneL.view ) ); + savefile->ReadInt( reinterpret_cast ( sparkBoneL.world ) ); + + savefile->ReadInt( reinterpret_cast ( sparkBoneR.view ) ); + savefile->ReadInt( reinterpret_cast ( sparkBoneR.world ) ); + + rearGasFxL.Restore( savefile ); + rearGasFxR.Restore( savefile ); +} diff --git a/src/Prey/prey_weaponautocannon.h b/src/Prey/prey_weaponautocannon.h new file mode 100644 index 0000000..d385acd --- /dev/null +++ b/src/Prey/prey_weaponautocannon.h @@ -0,0 +1,67 @@ +#ifndef __HH_WEAPON_AUTOCANNON_H +#define __HH_WEAPON_AUTOCANNON_H + +/*********************************************************************** + + hhWeaponAutoCannon + +***********************************************************************/ +class hhWeaponAutoCannon : public hhWeapon { + CLASS_PROTOTYPE( hhWeaponAutoCannon ); + + public: + enum { + EVENT_OVERHEAT = hhWeapon::EVENT_MAXEVENTS, + EVENT_MAXEVENTS + }; + + void Spawn(); + virtual ~hhWeaponAutoCannon(); + + virtual void ParseDef( const char* objectname ); + virtual void UpdateGUI(); + + virtual void Show(); + virtual void Hide(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + protected: + void Ticker(); + void AdjustHeat( const float amount ); + + void InitBoneInfo(); + void SpawnSpark(); + + void SetHeatLevel( const float heatLevel ); + float GetHeatLevel() const { return heatLevel; } + + virtual void PresentWeapon( bool showViewModel ); + + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + protected: + void Event_SpawnRearGasFX(); + void Event_AssignLeftRearFx( hhEntityFx* fx ); + void Event_AssignRightRearFx( hhEntityFx* fx ); + void Event_AdjustHeat( const float amount ); + void Event_GetHeatLevel(); + void Event_SpawnSparkLocal( const char* defName ); + void Event_OverHeatNetEvent(); //rww + + virtual bool ClientReceiveEvent( int event, int time, const idBitMsg &msg ); //rww + + protected: + float heatLevel; + + float sparkGapSize; + idEntityPtr beamSystem; + weaponJointHandle_t sparkBoneL; + weaponJointHandle_t sparkBoneR; + + idEntityPtr rearGasFxL; + idEntityPtr rearGasFxR; +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weaponcrawlergrenade.cpp b/src/Prey/prey_weaponcrawlergrenade.cpp new file mode 100644 index 0000000..e85ed95 --- /dev/null +++ b/src/Prey/prey_weaponcrawlergrenade.cpp @@ -0,0 +1,51 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhWeaponCrawlerGrenade + +***********************************************************************/ +const idEventDef EV_SpawnBloodSpray( "spawnBloodSpray" ); + +CLASS_DECLARATION( hhWeapon, hhWeaponCrawlerGrenade ) + EVENT( EV_SpawnBloodSpray, hhWeaponCrawlerGrenade::Event_SpawnBloodSpray ) +END_CLASS + +/* +================= +hhWeaponCrawlerGrenade::WriteToSnapshot +================= +*/ +void hhWeaponCrawlerGrenade::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhWeapon::WriteToSnapshot(msg); + msg.WriteBits(WEAPON_ALTMODE, 1); +} + +/* +================= +hhWeaponCrawlerGrenade::ReadFromSnapshot +================= +*/ +void hhWeaponCrawlerGrenade::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhWeapon::ReadFromSnapshot(msg); + WEAPON_ALTMODE = !!msg.ReadBits(1); +} + +/* +================= +hhWeaponCrawlerGrenade::Event_SpawnBloodSpray +================= +*/ +void hhWeaponCrawlerGrenade::Event_SpawnBloodSpray() { + hhFxInfo fxInfo; + fxInfo.UseWeaponDepthHack( true ); + if (WEAPON_ALTMODE) { + BroadcastFxInfoAlongBonePrefix( dict, "fx_blood", "joint_AltBloodFx", &fxInfo ); + } + else { + BroadcastFxInfoAlongBonePrefix( dict, "fx_blood", "joint_bloodFx", &fxInfo ); + } +} \ No newline at end of file diff --git a/src/Prey/prey_weaponcrawlergrenade.h b/src/Prey/prey_weaponcrawlergrenade.h new file mode 100644 index 0000000..025e50a --- /dev/null +++ b/src/Prey/prey_weaponcrawlergrenade.h @@ -0,0 +1,21 @@ +#ifndef __HH_WEAPON_CRAWLERGRENADE_H +#define __HH_WEAPON_CRAWLERGRENADE_H + +/*********************************************************************** + + hhWeaponCrawlerGrenade + +***********************************************************************/ +class hhWeaponCrawlerGrenade : public hhWeapon { + CLASS_PROTOTYPE( hhWeaponCrawlerGrenade ); + + public: + //rww - network friendliness + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + protected: + void Event_SpawnBloodSpray(); +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weaponfirecontroller.cpp b/src/Prey/prey_weaponfirecontroller.cpp new file mode 100644 index 0000000..17bdb22 --- /dev/null +++ b/src/Prey/prey_weaponfirecontroller.cpp @@ -0,0 +1,574 @@ + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +CLASS_DECLARATION( hhFireController, hhWeaponFireController ) +END_CLASS + +/* +================ +hhWeaponFireController::Clear +================ +*/ +void hhWeaponFireController::Clear() { + hhFireController::Clear(); + + self = NULL; + owner = NULL; + + brassDef = NULL; + brassDelay = -1; + + ammoType = 0; + clipSize = 0; // 0 means no reload + ammoClip = 0; + lowAmmo = 0; + + // weapon kick + muzzle_kick_time = 0; + muzzle_kick_maxtime = 0; + muzzle_kick_angles.Zero(); + muzzle_kick_offset.Zero(); + + aimDist.Zero(); + + // joints from models + barrelJoints.Clear(); + ejectJoints.Clear(); +} + +/* +================ +hhWeaponFireController::Init +================ +*/ +void hhWeaponFireController::Init( const idDict* viewDict, hhWeapon* self, hhPlayer* owner ) { + const char *brassDefName = NULL; + + hhFireController::Init( viewDict ); + + this->self = self; + this->owner = owner; + + muzzleFlash.lightId = LIGHTID_VIEW_MUZZLE_FLASH + owner->entityNumber; + //muzzleFlash.allowLightInViewID = owner->entityNumber+1; + + if( !dict ) { + return; + } + + muzzle_kick_time = SEC2MS( dict->GetFloat( "muzzle_kick_time" ) ); + muzzle_kick_maxtime = SEC2MS( dict->GetFloat( "muzzle_kick_maxtime" ) ); + muzzle_kick_angles = dict->GetAngles( "muzzle_kick_angles" ); + muzzle_kick_offset = dict->GetVector( "muzzle_kick_offset" ); + + // find some joints in the model for locating effects + SetWeaponJointHandleList( "joint_barrel", barrelJoints ); + SetWeaponJointHandleList( "joint_eject", ejectJoints ); + + // get the brass def + SetBrassDict( dict->GetString("def_ejectBrass") ); + + ammoType = GetAmmoType( dict->GetString( "ammoType" ) ); + clipSize = dict->GetInt( "clipSize" ); + lowAmmo = dict->GetInt( "lowAmmo" ); + + if( ( ammoType < 0 ) || ( ammoType >= AMMO_NUMTYPES ) ) { + gameLocal.Warning( "Unknown ammotype in object '%s'", dict->GetString("classname") ); + } + + //HUMANHEAD bjk: set in hhWeapon::GetWeaponDef + ammoClip = 0; + /*if ( clipSize ) { + ammoClip = owner->HasAmmo( ammoType, ammoRequired ); + if ( ( ammoClip < 0 ) || ( ammoClip > clipSize ) ) { + // first time using this weapon so have it fully loaded to start + ammoClip = clipSize; + } + }*/ + + aimDist = dict->GetVec2( "aimDist", "20 50" ); + scriptFunction = dict->GetString( "script_function", "Fire" ); +} + +/* +================ +hhWeaponFireController::SetWeaponJointHandleList +================ +*/ +void hhWeaponFireController::SetWeaponJointHandleList( const char* keyPrefix, hhCycleList& jointList ) { + weaponJointHandle_t handle; + const idKeyValue* kv = dict->MatchPrefix( keyPrefix ); + while( kv ) { + self->GetJointHandle( kv->GetValue().c_str(), handle ); + jointList.Append( handle ); + kv = dict->MatchPrefix( keyPrefix, kv ); + } +} + +/* +================ +hhWeaponFireController::SaveWeaponJointHandleList +================ +*/ +void hhWeaponFireController::SaveWeaponJointHandleList( const hhCycleList& jointList, idSaveGame *savefile ) const { + const weaponJointHandle_t* handle = NULL; + + int num = jointList.Num(); + savefile->WriteInt( num ); + for( int ix = 0; ix < num; ++ix ) { + handle = &jointList[ix]; + if( handle ) { + savefile->WriteInt( handle->view ); + savefile->WriteInt( handle->world ); + } + } +} + +/* +================ +hhWeaponFireController::RestoreWeaponJointHandleList +================ +*/ +void hhWeaponFireController::RestoreWeaponJointHandleList( hhCycleList& jointList, idRestoreGame *savefile ) { + //weaponJointHandle_t* handle = NULL; + + jointList.Clear(); + + int num; + savefile->ReadInt( num ); + weaponJointHandle_t handle; + for( int ix = 0; ix < num; ++ix ) { + savefile->ReadInt( (int&)handle.view ); + savefile->ReadInt( (int&)handle.world ); + jointList.Append( handle ); + } +} + +/* +================ +hhWeaponFireController::SetBrassDict +================ +*/ +void hhWeaponFireController::SetBrassDict( const char* name ) { + brassDefName = name; + if ( name[0] ) { + brassDef = gameLocal.FindEntityDefDict( name, false ); + if ( !brassDef ) { + gameLocal.Warning( "Unknown brass '%s'", name ); + } + } else { + brassDef = NULL; + } +} + +/* +================ +hhWeaponFireController::LaunchProjectiles +================ +*/ +bool hhWeaponFireController::LaunchProjectiles( const idVec3& pushVelocity ) { + if( !hhFireController::LaunchProjectiles(pushVelocity) ) { + return false; + } + + if( !gameLocal.isClient ) { + owner->AddProjectilesFired( numProjectiles ); + } + + //HUMANHEAD bjk PATCH 7-27-06 + if( gameLocal.isMultiplayer ) { + owner->inventory.lastShot[owner->GetWeaponNum( self->spawnArgs.GetString( "classname" ) )] = gameLocal.time + 1000; + } + + return true; +} + +/* +================= +hhWeaponFireController::DetermineAimAxis +================= +*/ +idMat3 hhWeaponFireController::DetermineAimAxis( const idVec3& muzzlePos, const idMat3& weaponAxis ) { + float traceDist = 1024.0f; // was CM_MAX_TRACE_DIST + float aimTraceDist; + trace_t aimTrace = self->GetEyeTraceInfo(); + + //HUMANHEAD bjk + if( aimTrace.fraction < 1.0f ) { + idVec3 eyePos = owner->GetEyePosition(); + + // Perform eye trace + gameLocal.clip.TracePoint( aimTrace, eyePos, eyePos + weaponAxis[0] * traceDist * 4.0f, + MASK_SHOT_RENDERMODEL | CONTENTS_GAME_PORTAL, owner.GetEntity() ); + + if ( aimTrace.fraction < 1.0f ) { // CJR: Check for portals + idEntity *ent = gameLocal.entities[ aimTrace.c.entityNum ]; // cannot use GetTraceEntity() as the portal could be bound to something + if ( ent && ent->IsType( hhPortal::Type ) ) { + aimTrace.endpos = eyePos + weaponAxis[0] * traceDist; + aimTrace.fraction = 1.0f; + } + } + + aimTraceDist = aimTrace.fraction * traceDist * 4.0f; + } else { + aimTraceDist = aimTrace.fraction * traceDist; + } //HUMANHEAD END + + idVec3 aimVector( aimTrace.endpos - muzzlePos ); + idVec3 aimDir = aimVector; + + aimDir.Normalize(); + aimDir.Lerp( weaponAxis[0], aimDir, hhUtils::CalculateScale(aimTraceDist, aimDist[0], aimDist[1]) ); + + idAngles aimAngles( aimDir.ToAngles() ); + + aimAngles.roll = weaponAxis.ToAngles().roll; + return aimAngles.ToMat3(); +} + +/* +================ +hhWeaponFireController::WeaponFeedback +================ +*/ +void hhWeaponFireController::WeaponFeedback() { + if( owner.IsValid() ) { + owner->WeaponFireFeedback( dict ); + } +} + +/* +================ +hhWeaponFireController::EjectBrass +================ +*/ +void hhWeaponFireController::EjectBrass() { + idMat3 axis; + idVec3 origin; + idEntity* ent = NULL; + idDebris* brass = NULL; + + if( !g_showBrass.GetBool() || !brassDef ) { + return; + } + + if (gameLocal.isClient && !gameLocal.isNewFrame) { //rww + return; + } + + if( TransformBrass(ejectJoints.Next(), origin, axis) ) { + gameLocal.SpawnEntityDef( *brassDef, &ent, true, gameLocal.isClient ); //rww - localized debris + HH_ASSERT( ent && ent->IsType(idDebris::Type) ); + + brass = static_cast( ent ); + brass->Create( owner.GetEntity(), origin, axis ); + brass->Launch(); + brass->fl.networkSync = false; //rww + brass->fl.clientEvents = true; //rww + //HACK: this should be in debris object. Just not sure if I should re-write it or not. + brass->SetShaderParm( SHADERPARM_TIME_OF_DEATH, MS2SEC(gameLocal.GetTime()) ); + } +} + +/* +================= +hhWeaponFireController::TransformBrass +================= +*/ +bool hhWeaponFireController::TransformBrass( const weaponJointHandle_t& handle, idVec3 &origin, idMat3 &axis ) { + if( !self->GetJointWorldTransform(handle, origin, axis) ) { + return false; + } + + axis = self->GetAxis(); + + return true; +} + +/* +================ +hhWeaponFireController::CalculateMuzzleRise +================ +*/ +void hhWeaponFireController::CalculateMuzzleRise( idVec3& origin, idMat3& axis ) { + int time; + float amount; + idAngles ang; + idVec3 offset; + + time = self->GetKickEndTime() - gameLocal.GetTime(); + if ( time <= 0 ) { + return; + } + + if ( muzzle_kick_maxtime <= 0 ) { + return; + } + + if ( time > muzzle_kick_maxtime ) { + time = muzzle_kick_maxtime; + } + + amount = ( float )time / ( float )muzzle_kick_maxtime; + ang = muzzle_kick_angles * amount; + offset = muzzle_kick_offset * amount; + + origin = origin - axis * offset; + axis = ang.ToMat3() * axis; +} + +/* +================ +hhWeaponFireController::UpdateMuzzleKick +================ +*/ +void hhWeaponFireController::UpdateMuzzleKick() { + // add some to the kick time, incrementally moving repeat firing weapons back + if ( self->GetKickEndTime() < gameLocal.GetTime() ) { + self->SetKickEndTime( gameLocal.GetTime() ); + } + self->SetKickEndTime( self->GetKickEndTime() + muzzle_kick_time ); + if ( self->GetKickEndTime() > gameLocal.GetTime() + muzzle_kick_maxtime ) { + self->SetKickEndTime( gameLocal.GetTime() + muzzle_kick_maxtime ); + } +} + +/* +================ +hhWeaponFireController::CalculateMuzzlePosition +================ +*/ +void hhWeaponFireController::CalculateMuzzlePosition( idVec3& origin, idMat3& axis ) { + if( !barrelJoints.Num() || !self->GetJointWorldTransform(barrelJoints.Next(), origin, axis) ) { + origin = GetMuzzlePosition(); + axis = self->GetAxis(); + } +} + +/* +================ +hhWeaponFireController::UseAmmo +================ +*/ +void hhWeaponFireController::UseAmmo() { + if( owner.IsValid() ) { + owner->UseAmmo( GetAmmoType(), AmmoRequired() ); + if ( ClipSize() && AmmoRequired() ) { + ammoClip--; + } + } +} + +/* +================ +hhWeaponFireController::AddToClip +================ +*/ +void hhWeaponFireController::AddToClip( int amount ) { + ammoClip += amount; + if ( ammoClip > clipSize ) { + ammoClip = clipSize; + } + + if ( ammoClip > AmmoAvailable() ) { + ammoClip = AmmoAvailable(); + } +} + +/* +================ +hhWeaponFireController::GetAmmoType +================ +*/ +ammo_t hhWeaponFireController::GetAmmoType( const char *ammoname ) { + int num; + const idDict *ammoDict; + + assert( ammoname ); + + ammoDict = gameLocal.FindEntityDefDict( "ammo_types", false ); + if ( !ammoDict ) { + gameLocal.Error( "Could not find entity definition for 'ammo_types'\n" ); + } + + if ( !ammoname[ 0 ] ) { + ammoname = "ammo_none"; + } + + if ( !ammoDict->GetInt( ammoname, "-1", num ) ) { + gameLocal.Error( "Unknown ammo type '%s'", ammoname ); + } + + if ( ( num < 0 ) || ( num >= AMMO_NUMTYPES ) ) { + gameLocal.Error( "Ammo type '%s' value out of range. Maximum ammo types is %d.\n", ammoname, AMMO_NUMTYPES ); + } + + return ( ammo_t )num; +} + +/* +================ +hhWeaponFireController::Save +================ +*/ +void hhWeaponFireController::Save( idSaveGame *savefile ) const { + self.Save( savefile ); + owner.Save( savefile ); + + savefile->WriteString( brassDefName ); + savefile->WriteInt( brassDelay ); + savefile->WriteString( scriptFunction ); + + savefile->WriteInt( ammoType ); + savefile->WriteInt( clipSize ); + savefile->WriteInt( ammoClip ); + savefile->WriteInt( lowAmmo ); + + savefile->WriteInt( muzzle_kick_time ); + savefile->WriteInt( muzzle_kick_maxtime ); + savefile->WriteAngles( muzzle_kick_angles ); + savefile->WriteVec3( muzzle_kick_offset ); + + savefile->WriteVec2( aimDist ); + + SaveWeaponJointHandleList( barrelJoints, savefile ); + SaveWeaponJointHandleList( ejectJoints, savefile ); + +} + +/* +================ +hhWeaponFireController::Restore +================ +*/ +void hhWeaponFireController::Restore( idRestoreGame *savefile ) { + self.Restore( savefile ); + owner.Restore( savefile ); + savefile->ReadString( brassDefName ); + savefile->ReadInt( brassDelay ); + savefile->ReadString( scriptFunction ); + + savefile->ReadInt( reinterpret_cast ( ammoType ) ); + savefile->ReadInt( clipSize ); + savefile->ReadInt( ammoClip ); + savefile->ReadInt( lowAmmo ); + + savefile->ReadInt( muzzle_kick_time ); + savefile->ReadInt( muzzle_kick_maxtime ); + savefile->ReadAngles( muzzle_kick_angles ); + savefile->ReadVec3( muzzle_kick_offset ); + + savefile->ReadVec2( aimDist ); + + RestoreWeaponJointHandleList( barrelJoints, savefile ); + RestoreWeaponJointHandleList( ejectJoints, savefile ); + + SetBrassDict( brassDefName ); +} + +/* +================ +hhWeaponFireController::AmmoAvailable +================ +*/ +int hhWeaponFireController::AmmoAvailable() const { + if ( owner.IsValid() ) { + return owner->HasAmmo( GetAmmoType(), AmmoRequired() ); + } else { + return 0; + } +} + +/* +================ +hhWeaponFireController::GetProjectileOwner +================ +*/ +idEntity *hhWeaponFireController::GetProjectileOwner() const { + return owner.GetEntity(); +} + +/* +================ +hhWeaponFireController::GetCollisionBBox +================ +*/ +const idBounds& hhWeaponFireController::GetCollisionBBox() { + return owner->GetPhysics()->GetAbsBounds(); +} + +/* +================ +hhWeaponFireController::GetSelf +================ +*/ +hhRenderEntity *hhWeaponFireController::GetSelf() { + return self.GetEntity(); +} + +/* +================ +hhWeaponFireController::GetSelfConst +================ +*/ +const hhRenderEntity *hhWeaponFireController::GetSelfConst() const { + return self.GetEntity(); +} + +/* +================ +hhWeaponFireController::UsesCrosshair +================ +*/ +bool hhWeaponFireController::UsesCrosshair() const { + return (self.IsValid() && bCrosshair && self->ShowCrosshair()); +} + +//rww - net friendliness +void hhWeaponFireController::WriteToSnapshot( idBitMsgDelta &msg ) const +{ + msg.WriteBits(self.GetSpawnId(), 32); + msg.WriteBits(owner.GetSpawnId(), 32); + + msg.WriteBits(ammoClip, self->GetClipBits()); + + //RWWTODO: Added lowAmmo int in case it needs to be sent --paul +} + +void hhWeaponFireController::ReadFromSnapshot( const idBitMsgDelta &msg ) +{ + self.SetSpawnId(msg.ReadBits(32)); + owner.SetSpawnId(msg.ReadBits(32)); + + ammoClip = msg.ReadBits(self->GetClipBits()); +} + +/* +================ +hhWeaponFireController::UpdateWeaponJoints +================ +*/ +void hhWeaponFireController::UpdateWeaponJoints(void) { //rww + barrelJoints.Clear(); + ejectJoints.Clear(); + + SetWeaponJointHandleList( "joint_barrel", barrelJoints ); + SetWeaponJointHandleList( "joint_eject", ejectJoints ); +} + +/* +================ +hhWeaponFireController::CheckThirdPersonMuzzle +================ +*/ +bool hhWeaponFireController::CheckThirdPersonMuzzle(idVec3 &origin, idMat3 &axis) { //rww + if (barrelJoints.Num() <= 0) { + return false; + } + + int i = barrelJoints.GetCurrentIndex(); + return self->GetJointWorldTransform(barrelJoints[i], origin, axis, true); +} diff --git a/src/Prey/prey_weaponfirecontroller.h b/src/Prey/prey_weaponfirecontroller.h new file mode 100644 index 0000000..1fbc8ce --- /dev/null +++ b/src/Prey/prey_weaponfirecontroller.h @@ -0,0 +1,138 @@ +#ifndef __HH_WEAPON_FIRE_CONTROLLER_H +#define __HH_WEAPON_FIRE_CONTROLLER_H + +struct weaponJointHandle_t { + jointHandle_t view; + jointHandle_t world; + + weaponJointHandle_t() { Clear(); } + void Clear() { view = INVALID_JOINT; world = INVALID_JOINT; } +}; + +/*********************************************************************** + + hhWeaponFireController + + Base class that manages the firing projectiles. +***********************************************************************/ +class hhWeaponFireController : public hhFireController { + CLASS_PROTOTYPE(hhWeaponFireController) + +public: + virtual void Clear(); + virtual void Init( const idDict* viewDict, hhWeapon* self, hhPlayer* owner ); + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + virtual bool LaunchProjectiles( const idVec3& pushVelocity ); + virtual idMat3 DetermineAimAxis( const idVec3& muzzlePos, const idMat3& weaponAxis ); + virtual void WeaponFeedback(); + + void CalculateMuzzleRise( idVec3& origin, idMat3& axis ); + void UpdateMuzzleKick(); + + virtual void EjectBrass(); + virtual bool TransformBrass( const weaponJointHandle_t& handle, idVec3& origin, idMat3& axis ); + idStr GetScriptFunction() { return scriptFunction; } + const char * GetString( const char *key ) { return dict->GetString( key ); } + + ID_INLINE virtual bool HasAmmo() const; + virtual void UseAmmo(); + virtual int AmmoAvailable() const; + void AddToClip( int amount ); + static ammo_t GetAmmoType( const char *ammoname ); + ID_INLINE ammo_t GetAmmoType() const; + ID_INLINE int AmmoInClip() const; + ID_INLINE int ClipSize() const; + virtual bool UsesCrosshair() const; + ID_INLINE int LowAmmo() const; + + //rww - net friendliness + void WriteToSnapshot( idBitMsgDelta &msg ) const; + void ReadFromSnapshot( const idBitMsgDelta &msg ); + + void UpdateWeaponJoints(void); //rww + + virtual bool CheckThirdPersonMuzzle(idVec3 &origin, idMat3 &axis); //rww +protected: + void SetWeaponJointHandleList( const char* keyPrefix, hhCycleList& jointList ); + void SaveWeaponJointHandleList( const hhCycleList& jointList, idSaveGame *savefile ) const; + void RestoreWeaponJointHandleList( hhCycleList& jointList, idRestoreGame *savefile ); + + void SetBrassDict( const char* name ); + + virtual void CalculateMuzzlePosition( idVec3& origin, idMat3& axis ); + virtual idEntity* GetProjectileOwner() const; + virtual const idBounds& GetCollisionBBox(); + + virtual hhRenderEntity *GetSelf(); + virtual const hhRenderEntity *GetSelfConst() const; + +protected: + idEntityPtr self; + idEntityPtr owner; + + idStr scriptFunction; + idStr brassDefName; // HUMANHEAD mdl: Added to save/load brassDef dict + const idDict * brassDef; + int brassDelay; + + ammo_t ammoType; + int clipSize; // 0 means no reload + int ammoClip; + int lowAmmo; + + // weapon kick + int muzzle_kick_time; + int muzzle_kick_maxtime; + idAngles muzzle_kick_angles; + idVec3 muzzle_kick_offset; + + idVec2 aimDist; + + // joints from models + hhCycleList barrelJoints; + hhCycleList ejectJoints; +}; + +/* +================ +hhWeaponFireController::HasAmmo +================ +*/ +ID_INLINE bool hhWeaponFireController::HasAmmo() const { + return hhFireController::HasAmmo() && ( (ClipSize() == 0) || (AmmoInClip() > 0) ); +} + +/* +================ +hhWeaponFireController::GetAmmoType +================ +*/ +ammo_t hhWeaponFireController::GetAmmoType() const { + return ammoType; +} + +/* +================ +hhWeaponFireController::AmmoInClip +================ +*/ +int hhWeaponFireController::AmmoInClip() const { + return ammoClip; +} + +/* +================ +hhWeaponFireController::ClipSize +================ +*/ +int hhWeaponFireController::ClipSize() const { + return clipSize; +} + +int hhWeaponFireController::LowAmmo() const { + return lowAmmo; +} + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weaponhider.cpp b/src/Prey/prey_weaponhider.cpp new file mode 100644 index 0000000..a210879 --- /dev/null +++ b/src/Prey/prey_weaponhider.cpp @@ -0,0 +1,95 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +#define FIRE_PREDICTION_TOLERANCE 0.16f //rww - time lag to allow prediction to handle fire and reload times + +CLASS_DECLARATION( hhWeaponFireController, hhHiderWeaponAltFireController ) +END_CLASS + +//============================================================================= +// +// hhHiderWeaponAltFireController::GetProjectileDict +// +//============================================================================= + +const idDict* hhHiderWeaponAltFireController::GetProjectileDict() const +{ + switch( self->AmmoInClip() ) { + case 1: return gameLocal.FindEntityDefDict( dict->GetString("def_projectile1"), false ); + case 2: return gameLocal.FindEntityDefDict( dict->GetString("def_projectile2"), false ); + case 3: return gameLocal.FindEntityDefDict( dict->GetString("def_projectile3"), false ); + case 4: return gameLocal.FindEntityDefDict( dict->GetString("def_projectile4"), false ); + default: return gameLocal.FindEntityDefDict( dict->GetString("def_projectile"), false ); + } +} + +CLASS_DECLARATION( hhWeapon, hhWeaponHider ) +END_CLASS + +hhWeaponHider::hhWeaponHider() { + nextPredictionAttack = 0.0f; + lastPredictionAttack = 0.0f; + nextPredictionTimeSkip = 0; +} + +void hhWeaponHider::ClientPredictionThink( void ) { //rww + hhWeapon::ClientPredictionThink(); + if (!gameLocal.isNewFrame) { + return; + } + if (owner.IsValid()) { + if (fabsf(WEAPON_NEXTATTACK-nextPredictionAttack) > FIRE_PREDICTION_TOLERANCE) { //allow some margin of error for prediction + if (owner->entityNumber == gameLocal.localClientNum) { + if (nextPredictionTimeSkip < gameLocal.time) { + if (nextPredictionTimeSkip) { + WEAPON_NEXTATTACK = nextPredictionAttack; + nextPredictionTimeSkip = 0; + } + else { + nextPredictionTimeSkip = gameLocal.time + 300; + } + } + } + else { + if (nextPredictionTimeSkip < gameLocal.time) { //for non-local clients, sync up if we go out of range on every new prediction frame + WEAPON_NEXTATTACK = nextPredictionAttack; + } + else { + owner->forcePredictionButtons |= BUTTON_ATTACK; + } + } + } + else if (owner->entityNumber == gameLocal.localClientNum) { + nextPredictionTimeSkip = 0; + } + } +} + +void hhWeaponHider::WriteToSnapshot( idBitMsgDelta &msg ) const { //rww + hhWeapon::WriteToSnapshot(msg); + + msg.WriteFloat(WEAPON_NEXTATTACK); +} +void hhWeaponHider::ReadFromSnapshot( const idBitMsgDelta &msg ) { //rww + hhWeapon::ReadFromSnapshot(msg); + + nextPredictionAttack = msg.ReadFloat(); + + if (owner.IsValid()) { + if (owner->entityNumber != gameLocal.localClientNum) { + if (fabsf(WEAPON_NEXTATTACK-nextPredictionAttack) > FIRE_PREDICTION_TOLERANCE) { //ensure this client "fires" his next prediction frame + owner->forcePredictionButtons |= BUTTON_ATTACK; + if (lastPredictionAttack != nextPredictionAttack) { + nextPredictionTimeSkip = gameLocal.time + 300; //give a few snapshots to straighten out for the local client + } + } + else { + owner->forcePredictionButtons &= ~BUTTON_ATTACK; + } + } + } + + lastPredictionAttack = nextPredictionAttack; +} diff --git a/src/Prey/prey_weaponhider.h b/src/Prey/prey_weaponhider.h new file mode 100644 index 0000000..1c8fc8a --- /dev/null +++ b/src/Prey/prey_weaponhider.h @@ -0,0 +1,42 @@ +#ifndef __HH_WEAPON_HIDER_H +#define __HH_WEAPON_HIDER_H + + +//============================================================================= +// +// hhHiderWeaponAltFireController +// +//============================================================================= +class hhHiderWeaponAltFireController : public hhWeaponFireController { + CLASS_PROTOTYPE(hhHiderWeaponAltFireController); +public: + ID_INLINE int AmmoRequired() const; + virtual const idDict* GetProjectileDict() const; +}; + +ID_INLINE int hhHiderWeaponAltFireController::AmmoRequired() const { + return self->AmmoInClip(); +} + + +class hhWeaponHider : public hhWeapon { + CLASS_PROTOTYPE( hhWeaponHider ); + + public: + hhWeaponHider(); + float nextPredictionAttack; //rww - does not need to be saved/restored, used only in client code. + float lastPredictionAttack; //rww - does not need to be saved/restored, used only in client code. + int nextPredictionTimeSkip; //rww - does not need to be saved/restored, used only in client code. + + protected: + ID_INLINE virtual hhWeaponFireController* CreateAltFireController(); + virtual void ClientPredictionThink( void ); //rww + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; //rww + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); //rww +}; + +ID_INLINE hhWeaponFireController* hhWeaponHider::CreateAltFireController() { + return new hhHiderWeaponAltFireController; +} + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weaponrifle.cpp b/src/Prey/prey_weaponrifle.cpp new file mode 100644 index 0000000..72e8026 --- /dev/null +++ b/src/Prey/prey_weaponrifle.cpp @@ -0,0 +1,466 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhWeaponZoomable + +***********************************************************************/ +const idEventDef EV_Weapon_ZoomIn( "zoomIn" ); +const idEventDef EV_Weapon_ZoomOut( "zoomOut" ); + +CLASS_DECLARATION( hhWeapon, hhWeaponZoomable ) + EVENT( EV_Weapon_ZoomIn, hhWeaponZoomable::Event_ZoomIn ) + EVENT( EV_Weapon_ZoomOut, hhWeaponZoomable::Event_ZoomOut ) +END_CLASS + +/* +================ +hhWeaponZoomable::ZoomIn +================ +*/ +void hhWeaponZoomable::ZoomIn() { + if( owner.IsValid() && dict ) { + owner->GetZoomFov().Init( gameLocal.GetTime(), SEC2MS(dict->GetFloat("zoomDuration")), owner->CalcFov(true), GetZoomFov() ); + } + + clientZoomTime = gameLocal.time + CLIENT_ZOOM_FUDGE; //rww + + bZoomed = true; +} + +/* +================ +hhWeaponZoomable::ZoomOut +================ +*/ +void hhWeaponZoomable::ZoomOut() { + if( owner.IsValid() && dict ) { + owner->GetZoomFov().Init( gameLocal.GetTime(), SEC2MS(dict->GetFloat("zoomDuration")), owner->CalcFov(true), g_fov.GetInteger() ); + } + + clientZoomTime = gameLocal.time + CLIENT_ZOOM_FUDGE; //rww + + bZoomed = false; +} + +/* +================ +hhWeaponZoomable::ZoomIn +================ +*/ +void hhWeaponZoomable::ZoomInStep() { + int maxFov = spawnArgs.GetInt( "zoomFovMax" ); + if ( zoomFov < maxFov ) { + zoomFov += spawnArgs.GetInt( "zoomFovStep" ); + StartSound( "snd_scope_click", SND_CHANNEL_ANY, 0, false, NULL ); + if ( zoomFov > maxFov ) { + zoomFov = maxFov; + } + ZoomIn(); + } + bZoomed = true; + owner->inventory.zoomFov = zoomFov; +} + +/* +================ +hhWeaponZoomable::ZoomOut +================ +*/ +void hhWeaponZoomable::ZoomOutStep() { + int minFov = spawnArgs.GetInt( "zoomFovMin" ); + if ( zoomFov > minFov ) { + zoomFov -= spawnArgs.GetInt( "zoomFovStep" ); + StartSound( "snd_scope_click", SND_CHANNEL_ANY, 0, false, NULL ); + if ( zoomFov < minFov ) { + zoomFov = minFov; + } + ZoomIn(); + } + bZoomed = true; + owner->inventory.zoomFov = zoomFov; +} + +/* +================ +hhWeaponZoomable::Event_ZoomIn +================ +*/ +void hhWeaponZoomable::Event_ZoomIn() { + ZoomIn(); +} + +/* +================ +hhWeaponZoomable::Event_ZoomOut +================ +*/ +void hhWeaponZoomable::Event_ZoomOut() { + ZoomOut(); +} + +/* +================ +hhWeaponZoomable::WriteToSnapshot +================ +*/ +void hhWeaponZoomable::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhWeapon::WriteToSnapshot(msg); + + msg.WriteBits(renderEntity.suppressSurfaceInViewID, GENTITYNUM_BITS); //if suppressSurfaceInViewID were to equal the last ent+1 this would be bad + //but, since it should never be world/none, this should be fine. + msg.WriteBits(bZoomed, 1); + msg.WriteBits(WEAPON_ALTMODE, 1); + + msg.WriteBits(IsHidden(), 1); +} + +/* +================ +hhWeaponZoomable::ReadFromSnapshot +================ +*/ +void hhWeaponZoomable::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhWeapon::ReadFromSnapshot(msg); + + int suppressSurfInViewID = msg.ReadBits(GENTITYNUM_BITS); + bool zoomed = !!msg.ReadBits(1); + bool weaponAltMode = !!msg.ReadBits(1); + bool hidden = !!msg.ReadBits(1); + if (clientZoomTime < gameLocal.time) { + renderEntity.suppressSurfaceInViewID = suppressSurfInViewID; + bZoomed = zoomed; + WEAPON_ALTMODE = weaponAltMode; + if (hidden != IsHidden()) { + if (hidden) { + Hide(); + } + else { + Show(); + } + } + } +} + +/* +================ +hhWeaponZoomable::Save +================ +*/ +void hhWeaponZoomable::Save( idSaveGame *savefile ) const { + savefile->WriteBool( bZoomed ); +} + +/* +================ +hhWeaponZoomable::Restore +================ +*/ +void hhWeaponZoomable::Restore( idRestoreGame *savefile ) { + savefile->ReadBool( bZoomed ); +} + +/*********************************************************************** + + hhWeaponRifle + +***********************************************************************/ +//This probably should be on entity but since no body else needs it I'll leave it here +const idEventDef EV_SuppressSurfaceInViewID( "", "d" ); + +CLASS_DECLARATION( hhWeaponZoomable, hhWeaponRifle ) + EVENT( EV_SuppressSurfaceInViewID, hhWeaponRifle::Event_SuppressSurfaceInViewID ) +END_CLASS + +/* +================ +hhWeaponRifle::Spawn +================ +*/ +void hhWeaponRifle::Spawn(void) { + zoomOverlayGui = NULL; + clientZoomTime = 0; //rww - no need to save/restore this + fl.clientEvents = true; //rww - for "prediction" +} + +/* +================ +hhWeaponRifle::~hhWeaponRifle +================ +*/ +hhWeaponRifle::~hhWeaponRifle(void) { + // Ensure that the scope view is disabled anytime the rifle goes away + ZoomOut(); + + zoomOverlayGui = NULL; + + SAFE_REMOVE( laserSight ); +} + +/* +================ +hhWeaponRifle::ParseDef +================ +*/ +void hhWeaponRifle::ParseDef( const char* objectname ) { + hhWeaponZoomable::ParseDef( objectname ); + + SAFE_REMOVE(laserSight); + + //rww - client-server-localized. + laserSight = static_cast( gameLocal.SpawnClientObject(dict->GetString("beam_laserSight"), NULL) ); + if ( laserSight.IsValid() ) { // cjr - Don't allow the laser show up in the player's zoomed view + assert(owner.IsValid()); + laserSight->GetRenderEntity()->suppressSurfaceInViewID = owner->entityNumber + 1; + laserSight->fl.neverDormant = true; + laserSight->snapshotOwner = this; //rww + } + + DeactivateLaserSight(); + + if( owner->inventory.zoomFov ) + zoomFov = owner->inventory.zoomFov; + WEAPON_ALTMODE = false; +} + +/* +================ +hhWeaponRifle::Ticker +================ +*/ +void hhWeaponRifle::Ticker() { + trace_t trace; + idVec3 start; + idMat3 axis; + + if( laserSight.IsValid() && laserSight->IsActive() ) { + GetJointWorldTransform( dict->GetString("joint_laserSight"), start, axis ); + gameLocal.clip.TracePoint( trace, start, start + GetAxis()[0] * CM_MAX_TRACE_DIST, MASK_VISIBILITY, owner.GetEntity() ); + laserSight->SetOrigin( start ); + laserSight->SetTargetLocation( trace.endpos ); + } + + hhWeaponZoomable::Ticker(); +} + +/* +================ +hhWeaponRifle::ZoomIn +================ +*/ +void hhWeaponRifle::ZoomIn() { + CancelEvents( &EV_SuppressSurfaceInViewID ); + + bool alreadyZoomed = bZoomed; + + hhWeaponZoomable::ZoomIn(); + + SetViewAnglesSensitivity( owner->GetZoomFov().GetEndValue() ); + + ProcessEvent( &EV_SuppressSurfaceInViewID, owner->entityNumber + 1 ); + RestoreGUI( "gui_zoomOverlay", &zoomOverlayGui ); + + if (zoomOverlayGui && !alreadyZoomed) { + zoomOverlayGui->HandleNamedEvent("zoomIn"); + + // Display Tips + if (!gameLocal.isMultiplayer && g_tips.GetBool()) { + gameLocal.SetTip(zoomOverlayGui, "_impulse15", "#str_41156", NULL, NULL, "tip1"); + gameLocal.SetTip(zoomOverlayGui, "_impulse14", "#str_41157", NULL, NULL, "tip2"); + zoomOverlayGui->HandleNamedEvent( "tipWindowUp" ); + } + } + + ActivateLaserSight(); + + if (owner.IsValid() && owner.GetEntity()) { + if (owner->entityNumber == gameLocal.localClientNum) { + renderSystem->SetScopeView( true ); // CJR: Enable special render scope view + } + owner->bScopeView = true; + } +} + +/* +================ +hhWeaponRifle::ZoomOut +================ +*/ +void hhWeaponRifle::ZoomOut() { + CancelEvents( &EV_SuppressSurfaceInViewID ); + + bool wasZoomed = bZoomed; + + hhWeaponZoomable::ZoomOut(); + + if (owner.IsValid() && owner.GetEntity()) { //rww - this seems to be possible when a player disconnects while zoomed i think + SetViewAnglesSensitivity( owner->GetZoomFov().GetEndValue() ); + } + + // Remove tips + if (zoomOverlayGui) { + zoomOverlayGui->HandleNamedEvent( "tipWindowDown" ); + } + + if ( dict ) { + PostEventMS( &EV_SuppressSurfaceInViewID, SEC2MS(dict->GetFloat("zoomDuration")) * 0.5f, 0 ); + } + zoomOverlayGui = NULL; + + if( wasZoomed ) { + SetShaderParm(7, -MS2SEC(gameLocal.time)); + } + + DeactivateLaserSight(); + + if (owner.IsValid() && owner.GetEntity()) { + if (owner->entityNumber == gameLocal.localClientNum) { + renderSystem->SetScopeView( false ); // CJR: Disable special render scope view + } + owner->bScopeView = false; + } +} + +/* +================ +hhWeaponRifle::ActivateLaserSight +================ +*/ +void hhWeaponRifle::ActivateLaserSight() { + if( laserSight.IsValid() ) { + laserSight->Activate( true ); + BecomeActive( TH_TICKER ); + } +} + +/* +================ +hhWeaponRifle::DeactivateLaserSight +================ +*/ +void hhWeaponRifle::DeactivateLaserSight() { + if( laserSight.IsValid() ) { + laserSight->Activate( false ); + BecomeInactive( TH_TICKER ); + } +} + +/* +================ +hhWeaponRifle::UpdateGUI +================ +*/ +void hhWeaponRifle::UpdateGUI() { + float parmValue = 0.0f; + idAngles angles; + + if( zoomOverlayGui ) {//This is for the sniper + float minFov = spawnArgs.GetFloat( "zoomFovMin" ); + float maxFov = spawnArgs.GetFloat( "zoomFovMax" ); + parmValue = idMath::ClampFloat(0.3f, 1.0f, 1.0f - 0.7f*(zoomFov - minFov)/(maxFov - minFov)); + zoomOverlayGui->SetStateFloat( "fov", parmValue ); + + angles = GetAxis().ToAngles(); + parmValue = angles.Normalize360().yaw; + zoomOverlayGui->SetStateFloat( "yaw", parmValue ); + zoomOverlayGui->StateChanged( gameLocal.time ); + zoomOverlayGui->Redraw( gameLocal.GetTime() ); + } +} + +/* +================ +hhWeaponRifle::Event_SuppressSurfaceInViewID +================ +*/ +void hhWeaponRifle::Event_SuppressSurfaceInViewID( const int id ) { + renderEntity.suppressSurfaceInViewID = id; +} + +/* +================ +hhWeaponRifle::Save +================ +*/ +void hhWeaponRifle::Save( idSaveGame *savefile ) const { + laserSight.Save( savefile ); + savefile->WriteUserInterface( zoomOverlayGui, false ); +} + +/* +================ +hhWeaponRifle::Restore +================ +*/ +void hhWeaponRifle::Restore( idRestoreGame *savefile ) { + laserSight.Restore( savefile ); + savefile->ReadUserInterface( zoomOverlayGui ); +} + +/* +================ +hhWeaponRifle::WriteToSnapshot +================ +*/ +void hhWeaponRifle::WriteToSnapshot( idBitMsgDelta &msg ) const { + hhWeaponZoomable::WriteToSnapshot(msg); + + bool laserSightActive = (laserSight.IsValid() && IsActive(TH_TICKER)); + msg.WriteBits(laserSightActive, 1); + + if (zoomOverlayGui) { + msg.WriteBits(1, 1); + } + else { + msg.WriteBits(0, 1); + } +} + +/* +================ +hhWeaponRifle::ReadFromSnapshot +================ +*/ +void hhWeaponRifle::ReadFromSnapshot( const idBitMsgDelta &msg ) { + hhWeaponZoomable::ReadFromSnapshot(msg); + + bool laserSightActive = !!msg.ReadBits(1); + if (laserSight.IsValid() && IsActive(TH_TICKER) != laserSightActive) { + if (laserSightActive) { + ActivateLaserSight(); + } + else { + DeactivateLaserSight(); + } + } + + bool guiActive = !!msg.ReadBits(1); + if (clientZoomTime < gameLocal.time) { + if ((!!zoomOverlayGui) != guiActive) { + if (guiActive) { + if (!zoomOverlayGui) { + RestoreGUI( "gui_zoomOverlay", &zoomOverlayGui ); + if (zoomOverlayGui) { + zoomOverlayGui->HandleNamedEvent("zoomIn"); + } + } + } + else { + if (zoomOverlayGui) { + zoomOverlayGui = NULL; + } + } + } + } +} + +CLASS_DECLARATION( hhWeaponFireController, hhSniperRifleFireController ) +END_CLASS + +void hhSniperRifleFireController::CalculateMuzzlePosition( idVec3& origin, idMat3& axis ) { + origin = owner->GetEyePosition(); + axis = self->GetAxis(); +} \ No newline at end of file diff --git a/src/Prey/prey_weaponrifle.h b/src/Prey/prey_weaponrifle.h new file mode 100644 index 0000000..ddb8bba --- /dev/null +++ b/src/Prey/prey_weaponrifle.h @@ -0,0 +1,91 @@ +#ifndef __HH_WEAPON_RIFLE_H +#define __HH_WEAPON_RIFLE_H + +#define CLIENT_ZOOM_FUDGE 100 + +/*********************************************************************** + + hhWeaponZoomable + +***********************************************************************/ +class hhWeaponZoomable : public hhWeapon { + CLASS_PROTOTYPE( hhWeaponZoomable ); + + public: + hhWeaponZoomable() : hhWeapon(), bZoomed( false ) { } + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network friendliness + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + ID_INLINE bool IsZoomed() const { return bZoomed; } + virtual void ZoomInStep(); + virtual void ZoomOutStep(); + + void Event_ZoomIn(); + void Event_ZoomOut(); + protected: + virtual void ZoomIn(); + virtual void ZoomOut(); + protected: + bool bZoomed; + + public: + int clientZoomTime; //rww - a bit hacky, to prevent jittering in prediction +}; + +/*********************************************************************** + + hhWeaponRifle + +***********************************************************************/ +class hhWeaponRifle : public hhWeaponZoomable { + CLASS_PROTOTYPE( hhWeaponRifle ); + + public: + void Spawn(); + virtual ~hhWeaponRifle(); + + virtual void UpdateGUI(); + + virtual void Ticker(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network friendliness + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual void ZoomIn(); + virtual void ZoomOut(); + + protected: + virtual void ParseDef( const char* objectname ); + + virtual void ActivateLaserSight(); + virtual void DeactivateLaserSight(); + + protected: + ID_INLINE virtual hhWeaponFireController* CreateAltFireController(); + void Event_SuppressSurfaceInViewID( const int id ); + + protected: + idUserInterface* zoomOverlayGui; + idEntityPtr laserSight; +}; + +class hhSniperRifleFireController : public hhWeaponFireController { + CLASS_PROTOTYPE(hhSniperRifleFireController); +public: + void CalculateMuzzlePosition( idVec3& origin, idMat3& axis ); +}; + +ID_INLINE hhWeaponFireController* hhWeaponRifle::CreateAltFireController() { + return new hhSniperRifleFireController; +} + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weaponrocketlauncher.cpp b/src/Prey/prey_weaponrocketlauncher.cpp new file mode 100644 index 0000000..5eb2e0f --- /dev/null +++ b/src/Prey/prey_weaponrocketlauncher.cpp @@ -0,0 +1,29 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhWeaponRocketLauncherFireController + +***********************************************************************/ +CLASS_DECLARATION( hhWeaponFireController, hhRocketLauncherFireController ) +END_CLASS + +/*********************************************************************** + + hhWeaponRocketLauncherAltFireController + +***********************************************************************/ +CLASS_DECLARATION( hhRocketLauncherFireController, hhRocketLauncherAltFireController ) +END_CLASS + +/*********************************************************************** + + hhWeaponRocketLauncher + +***********************************************************************/ +CLASS_DECLARATION( hhWeapon, hhWeaponRocketLauncher ) +END_CLASS + diff --git a/src/Prey/prey_weaponrocketlauncher.h b/src/Prey/prey_weaponrocketlauncher.h new file mode 100644 index 0000000..f4b2cb7 --- /dev/null +++ b/src/Prey/prey_weaponrocketlauncher.h @@ -0,0 +1,97 @@ +#ifndef __HH_WEAPON_ROCKETLAUNCHER_H +#define __HH_WEAPON_ROCKETLAUNCHER_H + +/*********************************************************************** + + hhRocketLauncherFireController + +***********************************************************************/ +class hhRocketLauncherFireController : public hhWeaponFireController { + CLASS_PROTOTYPE(hhRocketLauncherFireController) + + public: + ID_INLINE virtual void EjectBrass(); + + protected: + ID_INLINE virtual bool TransformBrass( const weaponJointHandle_t& handle, idVec3 &origin, idMat3 &axis ); +}; + +/* +================ +hhRocketLauncherFireController::EjectBrass +================ +*/ +ID_INLINE void hhRocketLauncherFireController::EjectBrass() { + for( int ix = ejectJoints.Num() - 1; ix >= 0; --ix ) { + hhWeaponFireController::EjectBrass(); + } +} + +/* +================ +hhRocketLauncherFireController::TransformBrass +================ +*/ +ID_INLINE bool hhRocketLauncherFireController::TransformBrass( const weaponJointHandle_t& handle, idVec3 &origin, idMat3 &axis ) { + return self->GetJointWorldTransform( handle, origin, axis ); +} + +/*********************************************************************** + + hhRocketLauncherAltFireController + +***********************************************************************/ +class hhRocketLauncherAltFireController : public hhRocketLauncherFireController { + CLASS_PROTOTYPE(hhRocketLauncherAltFireController) + + protected: + ID_INLINE virtual idMat3 DetermineProjectileAxis( const idMat3& muzzleAxis ); +}; + +/* +================ +hhRocketLauncherAltFireController::DetermineProjectileAxis +================ +*/ +ID_INLINE idMat3 hhRocketLauncherAltFireController::DetermineProjectileAxis( const idMat3& muzzleAxis ) { + float ang = spread * hhMath::Sqrt( gameLocal.random.RandomFloat() ); + float yaw = DEG2RAD(yawSpread); + float spin = hhMath::TWO_PI * gameLocal.random.RandomFloat(); + idVec3 dir = muzzleAxis[ 0 ] + muzzleAxis[ 2 ] * ( hhMath::Sin(ang) * hhMath::Sin(spin) ) - muzzleAxis[ 1 ] * ( hhMath::Sin(ang+yaw) * hhMath::Cos(spin) ); + dir.Normalize(); + + return dir.ToMat3(); +} + +/*********************************************************************** + + hhWeaponRocketLauncher + +***********************************************************************/ +class hhWeaponRocketLauncher : public hhWeapon { + CLASS_PROTOTYPE( hhWeaponRocketLauncher ); + + protected: + ID_INLINE virtual hhWeaponFireController* CreateFireController(); + ID_INLINE virtual hhWeaponFireController* CreateAltFireController(); +}; + +/* +================ +hhWeaponRocketLauncher::CreateFireController +================ +*/ +ID_INLINE hhWeaponFireController* hhWeaponRocketLauncher::CreateFireController() { + return new hhRocketLauncherFireController; +} + +/* +================ +hhWeaponRocketLauncher::CreateAltFireController +================ +*/ +ID_INLINE hhWeaponFireController* hhWeaponRocketLauncher::CreateAltFireController() { + return new hhRocketLauncherAltFireController; +} + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weaponsoulstripper.cpp b/src/Prey/prey_weaponsoulstripper.cpp new file mode 100644 index 0000000..a54c5d0 --- /dev/null +++ b/src/Prey/prey_weaponsoulstripper.cpp @@ -0,0 +1,1427 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +const idEventDef EV_KillBeam( "killBeam", NULL, NULL ); + +CLASS_DECLARATION( hhWeaponFireController, hhSoulStripperAltFireController ) +END_CLASS + +/*********************************************************************** + + hhSoulStripperAltFireController::LaunchProjectiles + +***********************************************************************/ +void hhSoulStripperAltFireController::LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ) { + // Spawn minion + idDict args; + + idVec3 minionOffset = dict->GetVector( "minionOffset" ); + args.SetVector( "origin", launchOrigin + aimAxis * minionOffset ); + args.SetMatrix( "rotation", aimAxis ); + const char *minionDef = dict->GetString( "def_minion" ); + gameLocal.SpawnObject( minionDef, &args ); + + // Spawn blast effect + hhFxInfo fxInfo; + fxInfo.SetNormal( aimAxis[0] ); + fxInfo.RemoveWhenDone( true ); + self->BroadcastFxInfo( dict->GetString( "fx_blastflash" ), launchOrigin, aimAxis, &fxInfo ); +} + +CLASS_DECLARATION( hhWeaponFireController, hhBeamBasedFireController ) +END_CLASS + +/*********************************************************************** + + hhBeamBasedFireController::LaunchProjectiles + +***********************************************************************/ +void hhBeamBasedFireController::LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ) { + idVec3 start, end; + trace_t results; + + float traceLength; + + idVec3 boneOrigin; + idMat3 boneAxis; + + if ( projTime < gameLocal.time ) { + hhFireController::LaunchProjectiles(launchOrigin, aimAxis, pushVelocity, projOwner); + projTime = gameLocal.time + dict->GetInt("projRate", "100"); + } + + start = launchOrigin; + + traceLength = dict->GetFloat("traceLength","100"); + + end = start + traceLength * aimAxis[0]; + + // Check to see if the beam should attempt to hit something + if (owner.IsValid()) { + //rww - can hit owners due to different angles in mp (although not the local client since the trace is done on the client every frame) + gameLocal.clip.TracePoint( results, start, end, MASK_SHOT_RENDERMODEL | CONTENTS_GAME_PORTAL, owner.GetEntity() ); + } + else { + gameLocal.clip.TracePoint( results, start, end, MASK_SHOT_RENDERMODEL | CONTENTS_GAME_PORTAL, NULL ); + } + + if ( !shotbeam.IsValid() ) { + shotbeam = hhBeamSystem::SpawnBeam( launchOrigin, dict->GetString( "beam" ), aimAxis, true ); + if( !shotbeam.IsValid() ) + return; + + if (self->GetOwner()) { + shotbeam->snapshotOwner = self->GetOwner(); //rww - this beam is client-local, but will remove itself when the gun is not actively in the snapshot (meaning, it's not actively updated) + } + } + + shotbeam->Activate( true ); + + hhFxInfo fxInfo; + fxInfo.SetNormal( results.c.normal ); + fxInfo.RemoveWhenDone( true ); + + if( !impactFx.IsValid() ) { + const char* str=dict->GetString("fx_impact"); + if ( str && str[0] ) + impactFx=self.GetEntity()->SpawnFxLocal( str, results.endpos, results.c.normal.ToMat3(), &fxInfo, true ); + } + + if( impactFx.IsValid() ) { + if (self.IsValid() && self->GetOwner()) { + impactFx->snapshotOwner = self->GetOwner(); //rww + } + static_cast( impactFx.GetEntity() )->SetFxInfo(fxInfo); + impactFx->Nozzle(true); + impactFx->SetOrigin(results.endpos-aimAxis[0]*20); + } + + idVec3 beamOrigin; + idMat3 beamAxis; + CalculateMuzzlePosition( beamOrigin, beamAxis ); // Must use this instead of launchOrigin, since launch origin is forced into the player's bbox + + if (gameLocal.isMultiplayer) { //rww - the visual origin of the beam needs to be from the player's world weapon joint to avoid the optic blast + idVec3 tpOrigin; + idMat3 tpAxis; + if (CheckThirdPersonMuzzle(tpOrigin, tpAxis)) { + beamOrigin = tpOrigin; + //just keep the existing axis + } + } + shotbeam->SetOrigin( beamOrigin ); + shotbeam->SetAxis( aimAxis ); + shotbeam->SetTargetLocation( results.endpos-aimAxis[0]*40 ); + float length = (results.endpos - start).Length(); + shotbeam->SetBeamOffsetScale( length/150.0f ); + + GetSelf()->CancelEvents( &EV_KillBeam ); + GetSelf()->PostEventSec( &EV_KillBeam, dict->GetFloat( "beamTime", "0.1" ) ); +} + +void hhBeamBasedFireController::KillBeam() +{ + if( impactFx.IsValid() ) { + impactFx->Nozzle(false); + if (gameLocal.isClient) { + impactFx->snapshotOwner = NULL; //rww - prevent it from being unhidden between now and the removal frame on the client + } + } + + if( shotbeam.IsValid() ) + shotbeam->Activate( false ); +} + +void hhBeamBasedFireController::Think() +{ + if( shotbeam.IsValid() ) { + idVec3 beamOrigin; + idMat3 beamAxis; + CalculateMuzzlePosition( beamOrigin, beamAxis ); // Must use this instead of launchOrigin, since launch origin is forced into the player's bbox + idVec3 adjustedOrigin = AssureInsideCollisionBBox( beamOrigin, GetSelf()->GetAxis(), GetCollisionBBox(), projectileMaxHalfDimension ); + idMat3 aimAxis = DetermineAimAxis( adjustedOrigin, GetSelf()->GetAxis() ); + + if (gameLocal.isMultiplayer) { //rww - the visual origin of the beam needs to be from the player's world weapon joint to avoid the optic blast + idVec3 tpOrigin; + idMat3 tpAxis; + if (CheckThirdPersonMuzzle(tpOrigin, tpAxis)) { + beamOrigin = tpOrigin; + //just keep the existing axis + } + } + shotbeam->SetOrigin( beamOrigin ); + shotbeam->SetAxis( aimAxis ); + } +} + +/*********************************************************************** + + hhBeamBasedFireController::Init + +***********************************************************************/ +void hhBeamBasedFireController::Init( const idDict* viewDict, hhWeapon* self, hhPlayer* owner ) +{ + hhWeaponFireController::Init( viewDict, self, owner ); + + shotbeam = NULL; + projTime = 0; +} + +/*********************************************************************** + + hhBeamBasedFireController::~hhBeamBasedFireController + +***********************************************************************/ +hhBeamBasedFireController::~hhBeamBasedFireController() +{ + KillBeam(); + if( impactFx.IsValid() ) + SAFE_REMOVE(impactFx); + if( shotbeam.IsValid() ) + SAFE_REMOVE(shotbeam); +} + +void hhBeamBasedFireController::Save( idSaveGame *savefile ) const { + shotbeam.Save( savefile ); + savefile->WriteInt( projTime ); + impactFx.Save( savefile ); +} + +void hhBeamBasedFireController::Restore( idRestoreGame *savefile ) { + shotbeam.Restore( savefile ); + savefile->ReadInt( projTime ); + impactFx.Restore( savefile ); +} + +CLASS_DECLARATION( hhBeamBasedFireController, hhSunbeamFireController ) +END_CLASS + +void hhSunbeamFireController::LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ) { + hhBeamBasedFireController::LaunchProjectiles(launchOrigin, aimAxis, pushVelocity, projOwner); + //HUMANHEAD PCF rww 05/18/06 - since values are assuming 60hz, make them relative + float backpush = dict->GetFloat("backpush","0.0")*(60.0f/(float)USERCMD_HZ); + float slowwalk = dict->GetFloat("slowwalk","0.0")*(60.0f/(float)USERCMD_HZ); + int knockback = dict->GetInt("knockback","0")*(60/USERCMD_HZ); + //HUMANHEAD END + + hhPhysics_Player* pp = static_cast(owner->GetPlayerPhysics()); + idVec3 v = pp->GetLinearVelocity()+owner->GetAxis()[0]*backpush; + float f = pp->GetLinearVelocity()*owner->GetAxis()[0]; + v += owner->GetAxis()[0] * ( (f>0) ? slowwalk*f : 0 ); + pp->SetLinearVelocity(v); + pp->SetKnockBack(knockback); +} + +void hhSunbeamFireController::Think() +{ + idVec3 start, end; + + if( shotbeam.IsValid() ) { + start = shotbeam->GetOrigin(); + end = shotbeam->GetTargetLocation(); + float length = (end - start).Length(); + + idVec3 beamOrigin; + idMat3 beamAxis; + CalculateMuzzlePosition( beamOrigin, beamAxis ); // Must use this instead of launchOrigin, since launch origin is forced into the player's bbox + idVec3 adjustedOrigin = AssureInsideCollisionBBox( beamOrigin, GetSelf()->GetAxis(), GetCollisionBBox(), projectileMaxHalfDimension ); + idMat3 aimAxis = DetermineAimAxis( adjustedOrigin, GetSelf()->GetAxis() ); + end = beamOrigin + length * aimAxis[0]; + + if (gameLocal.isMultiplayer) { //rww - the visual origin of the beam needs to be from the player's world weapon joint to avoid the optic blast + idVec3 tpOrigin; + idMat3 tpAxis; + if (CheckThirdPersonMuzzle(tpOrigin, tpAxis)) { + beamOrigin = tpOrigin; + //just keep the existing axis + } + } + shotbeam->SetOrigin( beamOrigin ); + shotbeam->SetAxis( aimAxis ); + shotbeam->SetTargetLocation( end ); + + if( impactFx.IsValid() ) + impactFx->SetOrigin( end ); + } +} + +CLASS_DECLARATION( hhWeaponFireController, hhPlasmaFireController ) +END_CLASS + +//============================================================================= +// +// hhPlasmaFireController::LaunchProjectiles +// +//============================================================================= +bool hhPlasmaFireController::LaunchProjectiles( const idVec3& pushVelocity ) { + char* projStr = "projectile_plasma"; + + fireDelay = dict->GetFloat( "fireRate" ); + spread = DEG2RAD( dict->GetFloat("spread") ); + yawSpread = dict->GetFloat("yawSpread"); + + usercmd_t cmd; + idAngles temp; + owner->GetPilotInput(cmd,temp); + + idVec3 dir(cmd.forwardmove, -cmd.rightmove, cmd.upmove); + + if( dir.x > 0 ) { + projStr = "projectile_plasmaf"; + fireDelay = 0.1f; + } + + if( dir.x < 0 ) { + spread = DEG2RAD(10); + fireDelay = 0.08f; + projStr = "projectile_plasmab"; + } + + if( dir.y != 0 ) { + spread *= 0.4f; + yawSpread +=8; + } + + if( dir.z < 0 ) { //crouch + spread = DEG2RAD(0); + fireDelay = 1.0f; + projStr = "projectile_plasmac"; + } + + if( !owner->AI_ONGROUND ) { //jump + fireDelay = 0.06f; + projStr = "projectile_plasmaj"; + } + + projectile = gameLocal.FindEntityDefDict( projStr, false ); + + return hhWeaponFireController::LaunchProjectiles( pushVelocity ); +} + +/*********************************************************************** + + hhWeaponSoulStripper + +***********************************************************************/ + +//const idEventDef EV_DamageBeamTarget( "", NULL ); +const idEventDef EV_GetAnimPostFix( "getAnimPostfix", "", 's' ); +const idEventDef EV_Leech( "leech", NULL, 'd' ); +const idEventDef EV_EndLeech( "endLeech", NULL, 'f' ); +const idEventDef EV_LightFadeIn( "", "f" ); +const idEventDef EV_LightFadeOut( "", "f" ); +const idEventDef EV_LightFade( "lightFade", "fff" ); +const idEventDef EV_GetFireFunction( "getFireFunction", NULL, 's' ); + +CLASS_DECLARATION( hhWeapon, hhWeaponSoulStripper ) + EVENT( EV_PostSpawn, hhWeaponSoulStripper::Event_PostSpawn ) + EVENT( EV_GetAnimPostFix, hhWeaponSoulStripper::Event_GetAnimPostFix ) + EVENT( EV_Leech, hhWeaponSoulStripper::Event_Leech ) + EVENT( EV_EndLeech, hhWeaponSoulStripper::Event_EndLeech ) + EVENT( AI_PlayAnim, hhWeaponSoulStripper::Event_PlayAnim ) + EVENT( AI_PlayCycle, hhWeaponSoulStripper::Event_PlayCycle ) + EVENT( EV_LightFadeIn, hhWeaponSoulStripper::Event_LightFadeIn ) + EVENT( EV_LightFadeOut, hhWeaponSoulStripper::Event_LightFadeOut ) + EVENT( EV_LightFade, hhWeaponSoulStripper::Event_LightFade ) + EVENT( EV_GetFireFunction, hhWeaponSoulStripper::Event_GetFireFunction ) + EVENT( EV_KillBeam, hhWeaponSoulStripper::Event_KillBeam ) +END_CLASS + +//============================================================================= +// +// hhWeaponSoulStripper::hhWeaponSoulStripper +// +//============================================================================= + +hhWeaponSoulStripper::hhWeaponSoulStripper() { + // Initialize SFX on the weapon + beam = NULL; // Beam is properly spawned in PostSpawn + beamCanA1 = NULL; // Beam is properly spawned in PostSpawn + beamCanB1 = NULL; // Beam is properly spawned in PostSpawn + beamCanC1 = NULL; // Beam is properly spawned in PostSpawn + beamCanA2 = NULL; // Beam is properly spawned in PostSpawn + beamCanB2 = NULL; // Beam is properly spawned in PostSpawn + beamCanC2 = NULL; // Beam is properly spawned in PostSpawn + beamCanA3 = NULL; // Beam is properly spawned in PostSpawn + beamCanB3 = NULL; // Beam is properly spawned in PostSpawn + beamCanC3 = NULL; // Beam is properly spawned in PostSpawn + cansValid = false; + + fxCanA = NULL; + fxCanB = NULL; + fxCanC = NULL; + + targetTime = 0; + + fcDeclNum = 0; + + netInitialized = false; + //HUMANHEAD PCF mdl 05/04/06 - Initialize beamLightHandle here + beamLightHandle = -1; +} + +//============================================================================= +// +// hhWeaponSoulStripper::Spawn +// +//============================================================================= + +void hhWeaponSoulStripper::Spawn() { + BecomeActive( TH_TICKER ); + fl.clientEvents = true; //rww +} + +//============================================================================= +// +// hhWeaponSoulStripper::ParseDef +// +//============================================================================= + +void hhWeaponSoulStripper::ParseDef( const char *objectname ) { + hhWeapon::ParseDef( objectname ); + + maxBeamLength = dict->GetFloat( "maxBeamLength", "1024" ); + + // Initialize weapon logic + targetNode = NULL; + beamLength = 0.0f; + + if (gameLocal.isMultiplayer) + { //rww - then do postspawn now. + Event_PostSpawn(); + } + else + { // Spawn the beams and SFX + PostEventMS( &EV_PostSpawn, 0 ); + } +} + +//============================================================================= +// +// hhWeaponSoulStripper::Event_PostSpawn +// +//============================================================================= + +void hhWeaponSoulStripper::Event_PostSpawn() { //rww - note that this function can get called more than once in a leechgun's life time in mp on the client. + idVec3 boneOrigin; + idMat3 boneAxis; + + GetJointWorldTransform( dict->GetString("attach_beam"), boneOrigin, boneAxis); + + if (!beam.IsValid()) { + beam = hhBeamSystem::SpawnBeam( boneOrigin, dict->GetString( "beam_stripper" ), boneAxis, true ); + if ( beam.IsValid() ) { + beam->Activate( false ); + } + } + DestroyCans(); + SpawnCans(); + + // Spawn beam light + memset( &beamLight, 0, sizeof( beamLight ) ); + beamLight.lightId = LIGHTID_VIEW_MUZZLE_FLASH + owner->entityNumber; + + beamLight.origin = GetOrigin(); + beamLight.axis = GetAxis(); + beamLight.pointLight = true; + beamLight.shader = declManager->FindMaterial( dict->GetString( "mtr_light" ) ); + beamLight.shaderParms[ SHADERPARM_RED ] = 1.0f; + beamLight.shaderParms[ SHADERPARM_GREEN ] = 1.0f; + beamLight.shaderParms[ SHADERPARM_BLUE ] = 1.0f; + beamLight.shaderParms[ SHADERPARM_TIMESCALE ] = 1.0f; + + beamLight.lightRadius[0] = 115; + beamLight.lightRadius[1] = 115; + beamLight.lightRadius[2] = 1; + + //HUMANHEAD PCF mdl 05/04/06 - Moved this to the constructor + //beamLightHandle = -1; + + const idDict *energyDef; + const char* str = owner->inventory.energyType; + if ( !str || !str[0] ) + return; + + energyDef = gameLocal.FindEntityDefDict( str ); + if ( !energyDef ) + return; + + if (gameLocal.isClient) { //rww - do the minimal fireController update for the client here. + const idDeclEntityDef *fcDecl = gameLocal.FindEntityDef( energyDef->GetString("def_fireInfo"), false ); + if (fcDecl) { + const char *fcSpawnClass = fcDecl->dict.GetString("spawnclass", NULL); + if (!fcSpawnClass || !fcSpawnClass[0]) { + fcSpawnClass = "hhWeaponFireController"; //default to regular fire controller + } + idTypeInfo *cls = idClass::GetClass(fcSpawnClass); + if (cls) { + ClientUpdateFC(cls->typeNum, fcDecl->Index()); + } + } + } + else { + GiveEnergy( energyDef->GetString("def_fireInfo"), false ); + } +} + +void hhWeaponSoulStripper::CheckCans(void) { + bool wantCans = true; + if (owner.IsValid() && owner->entityNumber != gameLocal.localClientNum) { //rww - we don't need cannister fx unless we are the client that owns this weapon. (or spectating him) + if ( gameLocal.localClientNum >= 0 && gameLocal.entities[ gameLocal.localClientNum ] && gameLocal.entities[ gameLocal.localClientNum ]->IsType( idPlayer::Type ) ) { + idPlayer *p = static_cast< idPlayer * >( gameLocal.entities[ gameLocal.localClientNum ] ); + if ( !p->spectating || p->spectator != owner->entityNumber ) { + wantCans = false; + } + } else { + wantCans = false; + } + } + + if (wantCans != cansValid) { + DestroyCans(); + if (wantCans) { + SpawnCans(); + } + } +} + +void hhWeaponSoulStripper::SpawnCans() { + if (owner.IsValid() && owner->entityNumber != gameLocal.localClientNum) { //rww - we don't need cannister fx unless we are the client that owns this weapon. (or spectating him) + if ( gameLocal.localClientNum >= 0 && gameLocal.entities[ gameLocal.localClientNum ] && gameLocal.entities[ gameLocal.localClientNum ]->IsType( idPlayer::Type ) ) { + idPlayer *p = static_cast< idPlayer * >( gameLocal.entities[ gameLocal.localClientNum ] ); + if ( !p->spectating || p->spectator != owner->entityNumber ) { + return; + } + } else { + return; + } + } + + // Spawn individual canister beams (top to center) + beamCanA1 = SpawnCanisterBeam( "attach_topA", "attach_soulA", beam_canTop ); + beamCanB1 = SpawnCanisterBeam( "attach_topB", "attach_soulB", beam_canTop ); + beamCanC1 = SpawnCanisterBeam( "attach_topC", "attach_soulC", beam_canTop ); + + // Spawn individual canister beams (bottom to center) + beamCanA2 = SpawnCanisterBeam( "attach_bottomA", "attach_soulA", beam_canBot ); + beamCanB2 = SpawnCanisterBeam( "attach_bottomB", "attach_soulB", beam_canBot ); + beamCanC2 = SpawnCanisterBeam( "attach_bottomC", "attach_soulC", beam_canBot ); + + // Spawn individual canister beams (faint bottom to top glow) + beamCanA3 = SpawnCanisterBeam( "attach_bottomA", "attach_topA", beam_canGlow ); + beamCanB3 = SpawnCanisterBeam( "attach_bottomB", "attach_topB", beam_canGlow ); + beamCanC3 = SpawnCanisterBeam( "attach_bottomC", "attach_topC", beam_canGlow ); + + fxCanA = SpawnCanisterFx( "attach_bottomA", fx_can ); + fxCanB = SpawnCanisterFx( "attach_bottomB", fx_can ); + fxCanC = SpawnCanisterFx( "attach_bottomC", fx_can ); + + cansValid = true; +} + +void hhWeaponSoulStripper::DestroyCans() { + SAFE_REMOVE( beamCanA1 ); + SAFE_REMOVE( beamCanB1 ); + SAFE_REMOVE( beamCanC1 ); + + SAFE_REMOVE( beamCanA2 ); + SAFE_REMOVE( beamCanB2 ); + SAFE_REMOVE( beamCanC2 ); + + SAFE_REMOVE( beamCanA3 ); + SAFE_REMOVE( beamCanB3 ); + SAFE_REMOVE( beamCanC3 ); + + SAFE_REMOVE( fxCanA ); + SAFE_REMOVE( fxCanB ); + SAFE_REMOVE( fxCanC ); + + cansValid = false; +} + +//============================================================================= +// +// hhWeaponSoulStripper::SpawnCanisterBeam +// +//============================================================================= + +hhBeamSystem *hhWeaponSoulStripper::SpawnCanisterBeam( const char *bottom, const char *top, const idStr &beamName ) { + hhBeamSystem *system = NULL; + idVec3 boneOrigin; + idMat3 boneAxis; + + if( beamName.IsEmpty() ) + return NULL; + + GetJointWorldTransform( dict->GetString(bottom), boneOrigin, boneAxis); + + system = hhBeamSystem::SpawnBeam( boneOrigin, beamName, boneAxis, true ); + if ( !system ) { + return NULL; + } + + if (owner.IsValid()) { + system->snapshotOwner = owner.GetEntity(); //rww + } + + GetJointWorldTransform( dict->GetString(top), boneOrigin, boneAxis); + + system->SetTargetLocation( boneOrigin ); + system->GetRenderEntity()->weaponDepthHack = true; + system->GetRenderEntity()->allowSurfaceInViewID = owner->entityNumber+1; + system->fl.neverDormant = true; + + return system; +} + +//============================================================================= +// +// hhWeaponSoulStripper::SpawnCanisterSprite +// +//============================================================================= + +idEntity *hhWeaponSoulStripper::SpawnCanisterSprite( const char *attach, const char *spriteName ) { + idEntity *ent; + const char *jointName; + + assert(gameLocal.isMultiplayer); //rww - i don't see this being used in mp, but if it is, it needs to be reworked. + + jointName = dict->GetString( attach ); + ent = gameLocal.SpawnObject( dict->GetString( spriteName ) ); + if ( ent ) { + ent->MoveToJoint( this, jointName ); + ent->BindToJoint( this, jointName, false ); + ent->GetRenderEntity()->weaponDepthHack = true; + } + + return ent; +} + +hhEntityFx* hhWeaponSoulStripper::SpawnCanisterFx( const char *attach, const idStr &name ) { + const char *jointName; + idVec3 boneOrigin; + idMat3 boneAxis; + hhEntityFx* fx; + + if( name.IsEmpty() ) + return NULL; + + jointName = dict->GetString( attach ); + + GetJointWorldTransform( dict->GetString(attach), boneOrigin, boneAxis); + + hhFxInfo fxInfo; + fxInfo.SetEntity( this ); + fxInfo.SetBindBone( jointName ); + fx = SpawnFxLocal( name, boneOrigin, boneAxis, &fxInfo, true ); + fx->GetRenderEntity()->weaponDepthHack = true; + fx->GetRenderEntity()->allowSurfaceInViewID = owner->entityNumber+1; + //don't attach canister fx to snapshotOwner, since they are local and temporary + /* + if (owner.IsValid()) { + fx->snapshotOwner = owner.GetEntity(); //rww + } + */ + + return fx; +} + +//============================================================================= +// +// hhWeaponSoulStripper::CreateAltFireController +// +//============================================================================= +hhWeaponFireController* hhWeaponSoulStripper::CreateAltFireController() { + return hhWeapon::CreateAltFireController(); + //return new hhSoulStripperAltFireController; +} + +//============================================================================= +// +// hhWeaponSoulStripper::~hhWeaponSoulStripper +// +//============================================================================= + +hhWeaponSoulStripper::~hhWeaponSoulStripper() { + SAFE_REMOVE( beam ); + + DestroyCans(); + + // Remove the beam light + if ( beamLightHandle != -1 ) { + gameRenderWorld->FreeLightDef( beamLightHandle ); + beamLightHandle = -1; + } + + StopSound( SND_CHANNEL_WEAPON, false ); //rww - don't broadcast this. you're removing the entity on the client too, so not only would it get + //called there too, but once you receive the event the entity will be gone and it will be tossed. +} + +//============================================================================= +// +// hhWeaponSoulStripper::UpdateGUI +// +//============================================================================= +void hhWeaponSoulStripper::UpdateGUI() { + if ( renderEntity.gui[0] ) { + renderEntity.gui[0]->SetStateInt( "souls", (GetAnimPostfix()>0) ? GetAnimPostfix()-'A'+1 : 0 ); + float p = 3.0f * owner->inventory.AmmoPercentage(owner.GetEntity(), GetAmmoType()); + renderEntity.gui[0]->SetStateFloat( "soulpercentageA", idMath::ClampFloat(0.0f, 1.0f, p - 0.0f) ); + renderEntity.gui[0]->SetStateFloat( "soulpercentageB", idMath::ClampFloat(0.0f, 1.0f, p - 1.0f) ); + renderEntity.gui[0]->SetStateFloat( "soulpercentageC", idMath::ClampFloat(0.0f, 1.0f, p - 2.0f) ); + renderEntity.gui[0]->SetStateBool( "leeching", (targetNode) ? true : false); + + const char* str; + if( targetNode ) { + const idDict *energyDef; + str = targetNode->spawnArgs.GetString( "def_energy" ); + if ( str && str[0] ) { + energyDef = gameLocal.FindEntityDefDict( str ); + if ( energyDef ) { + renderEntity.gui[0]->SetStateFloat( "meter", (gameLocal.time - targetTime)/energyDef->GetFloat( "leech_time", "500" ) ); + renderEntity.gui[0]->SetStateInt( "leechtype", energyDef->GetInt("beamType")+1 ); + } + } + } + + if( fireController ) { + str = fireController->GetString("intgui"); + if( str && str[0] ) + renderEntity.gui[0]->SetStateInt( "type", str[0]-'1'+1 ); + } + else { + renderEntity.gui[0]->SetStateInt( "type", 0 ); + } + } +} + +void hhWeaponSoulStripper::Event_Leech() { + idVec3 start, end; + trace_t results; + int captured = 0; + + if ( !beam.IsValid() ) { + idThread::ReturnInt( 0 ); + return; + } + + start = GetOrigin(); + + // Currently latched onto a target, check to see if the target is within the valid cone and there is still a LOS to the target + if ( targetNode ) { + end = beam->GetTargetLocation(); + + // Verify that the target is within a cone in front of the weapon + idVec3 vecToTarget = end - start; + vecToTarget.Normalize(); + float dpX = GetAxis()[0] * vecToTarget; + float dpY = GetAxis()[1] * vecToTarget; + + // Target isn't in the cone, so reset the trace + if ( idMath::Fabs( dpY ) >= spawnArgs.GetFloat( "beamTargetAngle", "0.4" ) || !targetNode->CanLeech() ) { + //ReleaseEntity(); + targetNode->LeechTrigger(GetOwner(),"leech_end"); + targetNode = NULL; + end = start + maxBeamLength * GetAxis()[0]; + } + } + else + end = start + maxBeamLength * GetAxis()[0]; + + // Check to see if the beam should attempt to hit something + if (owner.IsValid()) { + //rww - can hit owners due to different angles in mp (although not the local client since the trace is done on the client every frame) + gameLocal.clip.TracePoint( results, start, end, MASK_SHOT_RENDERMODEL | CONTENTS_GAME_PORTAL, owner.GetEntity() ); + } + else { + gameLocal.clip.TracePoint( results, start, end, MASK_SHOT_RENDERMODEL | CONTENTS_GAME_PORTAL, this ); + } + + beamLength = (results.endpos - start).Length(); + + // Hit something + if ( results.fraction < 1.0f ) { + captured = CaptureEnergy( results ); + + // Update the main beam + UpdateBeam( start, true ); // struck an entity + } + + //if( owner.IsValid() ) { + // owner->WeaponFireFeedback( dict ); + //} + + // Return true if the beam has struck an entity + idThread::ReturnFloat( (float)captured ); +} + +//============================================================================= +// +// hhWeaponSoulStripper::Event_RecoilBeam +// +//============================================================================= + +void hhWeaponSoulStripper::Event_EndLeech() { + idVec3 start; + idMat3 axis; + + if ( !beam.IsValid() ) { + return; + } + + if ( targetNode ) + targetNode->LeechTrigger(GetOwner(),"leech_end"); + targetNode = NULL; + + // Update the main beam + UpdateBeam( start, false ); + + if( owner.IsValid() ) { + owner->WeaponFireFeedback( dict ); + } + + idThread::ReturnFloat( 0 ); +} + +//============================================================================= +// +// hhWeaponSoulStripper::Ticker +// +// Controls the length of the beam and firing projectiles +//============================================================================= + +void hhWeaponSoulStripper::Ticker() { + // Update canister beams + UpdateCanisterBeam( beamCanA1.GetEntity(), "attach_topA", "attach_soulA" ); + UpdateCanisterBeam( beamCanB1.GetEntity(), "attach_topB", "attach_soulB" ); + UpdateCanisterBeam( beamCanC1.GetEntity(), "attach_topC", "attach_soulC" ); + UpdateCanisterBeam( beamCanA2.GetEntity(), "attach_bottomA", "attach_soulA" ); + UpdateCanisterBeam( beamCanB2.GetEntity(), "attach_bottomB", "attach_soulB" ); + UpdateCanisterBeam( beamCanC2.GetEntity(), "attach_bottomC", "attach_soulC" ); + UpdateCanisterBeam( beamCanA3.GetEntity(), "attach_bottomA", "attach_topA" ); + UpdateCanisterBeam( beamCanB3.GetEntity(), "attach_bottomB", "attach_topB" ); + UpdateCanisterBeam( beamCanC3.GetEntity(), "attach_bottomC", "attach_topC" ); + + hhBeamBasedFireController *beamController; + hhSunbeamFireController *sun; + if ( fireController && fireController->IsType(hhBeamBasedFireController::Type) ) { + if ( fireController->IsType(hhSunbeamFireController::Type) ) { + sun = static_cast(fireController); + sun->Think(); + } + else { + beamController = static_cast(fireController); + beamController->Think(); + } + } + + // Update our current animation if cans changed + if ( GetAnimPostfix() != lastCanState ) { + if ( GetAnimator()->IsAnimating( gameLocal.time ) && lastAnim.Length() != 0 ) { + idStr animname = lastAnim; + animname.Append( GetAnimPostfix() ); + int anim = GetAnimator()->GetAnim( animname ); + if( anim ) + GetAnimator()->CycleAnim( ANIMCHANNEL_ALL, anim, gameLocal.time, FRAME2MS(4) ); + } + + lastCanState = GetAnimPostfix(); + } +} + +//============================================================================= +// +// hhWeaponSoulStripper::UpdateBeam +// +// Updates the beam system +// - beam length +// - scale issues (don't let the beam wiggle as wildly when it is pulled close) +// - beam light +//============================================================================= + +void hhWeaponSoulStripper::UpdateBeam( idVec3 start, bool struckEntity ) { + float scale; + idVec3 boneOrigin; + idMat3 boneAxis; + + if ( !beam.IsValid() ) { + return; + } + + if ( targetNode ) { + beam->Activate( true ); + + GetJointWorldTransform( dict->GetString("attach_beam"), boneOrigin, boneAxis); + + beam->SetOrigin( boneOrigin ); + + beam->SetArcVector( GetAxis()[0] ); + scale = beamLength / 150.0f; + if( scale > 1 ) { + scale = 1; + } + + if ( targetNode->entityNumber < ENTITYNUM_MAX_NORMAL ) { + beam->SetTargetLocation( targetNode->leechPoint ); + } + + int beamType=0; + const idDict *energyDef; + const char* str = targetNode->spawnArgs.GetString( "def_energy" ); + if ( str && str[0] ) { + energyDef = gameLocal.FindEntityDefDict( str ); + if ( energyDef ) + beamType=energyDef->GetInt("beamType"); + } + + beam->SetShaderParm( SHADERPARM_MODE, beamType ); + beamLight.shaderParms[ SHADERPARM_MODE ] = beamType; + + beam->SetBeamOffsetScale( scale ); + + // Update the beam light + beamLight.axis[0] = GetAxis()[2]; + beamLight.axis[1] = GetAxis()[1]; + beamLight.axis[2] = GetAxis()[0]; // Re-align the light to point along the beam + beamLight.origin = (GetOrigin() + GetAxis()[0] * beamLength * 0.25f); // Set beam origin to the midpoint of the weapon origin and the target + beamLight.lightRadius[2] = 128.0f + beamLength * 0.5f; // Scale the light size based upon the beam (larger than half, so the beam extends beyond the extents slightly) + + if ( beamLightHandle != -1 ) { + gameRenderWorld->UpdateLightDef( beamLightHandle, &beamLight ); + } else { + beamLightHandle = gameRenderWorld->AddLightDef( &beamLight ); + } + } else { + beam->Activate( false ); + + // Hide the beam light + if ( beamLightHandle != -1 ) { + gameRenderWorld->FreeLightDef( beamLightHandle ); + beamLightHandle = -1; + } + } +} + +//============================================================================= +// +// hhWeaponSoulStripper::UpdateCanisterBeam +// +//============================================================================= + +void hhWeaponSoulStripper::UpdateCanisterBeam( hhBeamSystem *system, const char *top, const char *bottom ) { + idVec3 boneOrigin; + idMat3 boneAxis; + + if ( !system ) { + return; + } + + GetJointWorldTransform( dict->GetString( top ), boneOrigin, boneAxis); + system->SetTargetLocation( boneOrigin ); + GetJointWorldTransform( dict->GetString( bottom ), boneOrigin, boneAxis); + system->SetOrigin( boneOrigin ); +} + +//============================================================================= +// +// hhWeaponSoulStripper::CaptureEnergy +// +//============================================================================= + +int hhWeaponSoulStripper::CaptureEnergy( trace_t &results ) { + idEntity *hitEntity; + hhEnergyNode* hitNode; + + hitEntity = gameLocal.GetTraceEntity(results); + + if ( hitEntity && hitEntity->IsType( hhEnergyNode::Type ) ) { + hitNode = static_cast(hitEntity); + if ( !hitNode->CanLeech() ) + return 0; + } + else { + if ( targetNode ) + targetNode->LeechTrigger(GetOwner(),"leech_end"); + targetNode = NULL; + return 0; + } + + if ( targetNode != hitNode ) { + if ( targetNode ) + targetNode->LeechTrigger(GetOwner(),"leech_end"); + targetTime = gameLocal.time; + targetNode = hitNode; + targetNode->LeechTrigger(GetOwner(),"leech_start"); + } + + const idDict *energyDef; + const char* str = targetNode->spawnArgs.GetString( "def_energy" ); + if ( !str || !str[0] ) + return 0; // blank node + + energyDef = gameLocal.FindEntityDefDict( str ); + if ( !energyDef ) + return 0; + + if ( gameLocal.time - targetTime < energyDef->GetInt( "leech_time", "500" ) ) + return 1; //not enough time + + targetNode->LeechTrigger(GetOwner(),"leech_success"); + targetNode->Finish(); + targetTime = gameLocal.time; + + //create new firecontroller for new energy type + if( !GiveEnergy( energyDef->GetString("def_fireInfo"), true ) ) + return 0; + + StartSound( "snd_intake", SND_CHANNEL_ANY ); + SpawnCanisterFx( "attach_sidevent", spawnArgs.GetString("fx_sidevent") ); + SpawnCanisterFx( "attach_steam", spawnArgs.GetString("fx_steam") ); + + owner->inventory.energyType = str; + + return 2; +} + +bool hhWeaponSoulStripper::GiveEnergy( const char *energyType, bool fill ) { + if ( !energyType || !energyType[0] ) + return false; + + //create new firecontroller for new energy type + if (gameLocal.isClient) //rww - handle this properly through snapshots + return true; + + SAFE_DELETE_PTR(fireController); + const idDeclEntityDef *fcDecl = gameLocal.FindEntityDef( energyType, false ); + const idDict* infoDict = fcDecl ? &fcDecl->dict : NULL; + fcDeclNum = 0; + if( infoDict ) { + const char *spawn; + idTypeInfo *cls; + + if( fill ) { + int num = owner->GetWeaponNum("weaponobj_soulstripper"); + assert(num); + owner->weaponInfo[ num ].ammoMax = infoDict->GetInt("ammoAmount"); + owner->spawnArgs.SetInt( "max_ammo_energy", infoDict->GetInt("ammoAmount") ); + owner->inventory.ammo[owner->inventory.AmmoIndexForAmmoClass("ammo_energy")]=0; + owner->Give( "ammo_energy", infoDict->GetString("ammoAmount") ); + } + beam_canTop = infoDict->GetString( "beam_canTop" ); + beam_canBot = infoDict->GetString( "beam_canBot" ); + beam_canGlow = infoDict->GetString( "beam_canGlow" ); + fx_can = infoDict->GetString( "fx_can" ); + + DestroyCans(); + SpawnCans(); + + infoDict->GetString( "spawnclass", NULL, &spawn ); + if ( !spawn || !spawn[0] ) { //rww - changed to be in line with client behavior + spawn = "hhWeaponFireController"; //default to regular fire controller + } + cls = idClass::GetClass( spawn ); + if ( !cls || !cls->IsType( hhWeaponFireController::Type ) ) { + common->Warning( "Could not spawn. Class '%s' not found.", spawn ); + return false; + } + fireController = static_cast( cls->CreateInstance() ); + if ( !fireController ) { + common->Warning( "Could not spawn '%s'. Instance could not be created.", spawn ); + return false; + } + fcDeclNum = fcDecl->Index(); + /* + else { + fireController = CreateFireController(); + } + */ + + fireController->Init( infoDict, this, owner.GetEntity() ); + } + + //if it failed, use generic empty controller + if ( !fireController ) { + const idDict* infoDict = gameLocal.FindEntityDefDict( spawnArgs.GetString("def_fireInfo"), false ); + if( infoDict ) { + fireController = CreateFireController(); + fireController->Init( infoDict, this, owner.GetEntity() ); + } + } + return true; +} + +//============================================================================= +// +// hhWeaponSoulStripper::GetAnimPostfix +// +// Appends to the passed in postfix. +//============================================================================= + +char hhWeaponSoulStripper::GetAnimPostfix() { + // Choose the anim postfix based upon ammo + float p=owner->inventory.AmmoPercentage(owner.GetEntity(), GetAmmoType()); + if(p > 0) + return (int)(2.9f*p) + 'A'; + else + return 0; +} + +//============================================================================= +// +// hhWeaponSoulStripper::Event_GetAnimPostFix +// +//============================================================================= + +void hhWeaponSoulStripper::Event_GetAnimPostFix() { + idStr postfix( GetAnimPostfix() ); + idThread::ReturnString( postfix.c_str() ); +} + +//============================================================================= +// +// hhWeaponSoulStripper::Event_PlayAnim +// +//============================================================================= + +void hhWeaponSoulStripper::Event_PlayAnim( int channel, const char *animname ) { + lastAnim = ""; + + idStr anim = animname; + anim.Append( GetAnimPostfix() ); + hhWeapon::Event_PlayAnim( channel, anim.c_str() ); +} + +//============================================================================= +// +// hhWeaponSoulStripper::Event_PlayCycle +// +//============================================================================= + +void hhWeaponSoulStripper::Event_PlayCycle( int channel, const char *animname ) { + lastAnim = animname; + + idStr anim = animname; + anim.Append( GetAnimPostfix() ); + hhWeapon::Event_PlayCycle( channel, anim.c_str() ); +} + +//============================================================================= +// +// hhWeaponSoulStripper::PresentWeapon +// +//============================================================================= + +void hhWeaponSoulStripper::PresentWeapon( bool showViewModel ) { + if ( IsHidden() || !owner->CanShowWeaponViewmodel() || pm_thirdPerson.GetBool() ) { + if ( beamCanA1.IsValid() ) beamCanA1->Activate( false ); + if ( beamCanB1.IsValid() ) beamCanB1->Activate( false ); + if ( beamCanC1.IsValid() ) beamCanC1->Activate( false ); + if ( beamCanA2.IsValid() ) beamCanA2->Activate( false ); + if ( beamCanB2.IsValid() ) beamCanB2->Activate( false ); + if ( beamCanC2.IsValid() ) beamCanC2->Activate( false ); + if ( beamCanA3.IsValid() ) beamCanA3->Activate( false ); + if ( beamCanB3.IsValid() ) beamCanB3->Activate( false ); + if ( beamCanC3.IsValid() ) beamCanC3->Activate( false ); + if ( fxCanA.IsValid() ) fxCanA->Hide(); + if ( fxCanB.IsValid() ) fxCanB->Hide(); + if ( fxCanC.IsValid() ) fxCanC->Hide(); + } else { + if ( beamCanA1.IsValid() ) beamCanA1->Activate( true ); + if ( beamCanB1.IsValid() ) beamCanB1->Activate( true ); + if ( beamCanC1.IsValid() ) beamCanC1->Activate( true ); + if ( beamCanA2.IsValid() ) beamCanA2->Activate( true ); + if ( beamCanB2.IsValid() ) beamCanB2->Activate( true ); + if ( beamCanC2.IsValid() ) beamCanC2->Activate( true ); + if ( beamCanA3.IsValid() ) beamCanA3->Activate( true ); + if ( beamCanB3.IsValid() ) beamCanB3->Activate( true ); + if ( beamCanC3.IsValid() ) beamCanC3->Activate( true ); + if ( fxCanA.IsValid() ) fxCanA->Show(); + if ( fxCanB.IsValid() ) fxCanB->Show(); + if ( fxCanC.IsValid() ) fxCanC->Show(); + } + + hhWeapon::PresentWeapon( showViewModel ); +} + +//============================================================================= +// +// hhWeaponSoulStripper::Show +// +//============================================================================= + +void hhWeaponSoulStripper::Show() { + if ( beamCanA1.IsValid() ) beamCanA1->Show(); + if ( beamCanB1.IsValid() ) beamCanB1->Show(); + if ( beamCanC1.IsValid() ) beamCanC1->Show(); + if ( beamCanA2.IsValid() ) beamCanA2->Show(); + if ( beamCanB2.IsValid() ) beamCanB2->Show(); + if ( beamCanC2.IsValid() ) beamCanC2->Show(); + if ( beamCanA3.IsValid() ) beamCanA3->Show(); + if ( beamCanB3.IsValid() ) beamCanB3->Show(); + if ( beamCanC3.IsValid() ) beamCanC3->Show(); + if ( fxCanA.IsValid() ) fxCanA->Show(); + if ( fxCanB.IsValid() ) fxCanB->Show(); + if ( fxCanC.IsValid() ) fxCanC->Show(); + + hhWeapon::Show(); +} + +//============================================================================= +// +// hhWeaponSoulStripper::Hide +// +//============================================================================= + +void hhWeaponSoulStripper::Hide() { + if ( beamCanA1.IsValid() ) beamCanA1->Hide(); + if ( beamCanB1.IsValid() ) beamCanB1->Hide(); + if ( beamCanC1.IsValid() ) beamCanC1->Hide(); + if ( beamCanA2.IsValid() ) beamCanA2->Hide(); + if ( beamCanB2.IsValid() ) beamCanB2->Hide(); + if ( beamCanC2.IsValid() ) beamCanC2->Hide(); + if ( beamCanA3.IsValid() ) beamCanA3->Hide(); + if ( beamCanB3.IsValid() ) beamCanB3->Hide(); + if ( beamCanC3.IsValid() ) beamCanC3->Hide(); + if ( fxCanA.IsValid() ) fxCanA->Hide(); + if ( fxCanB.IsValid() ) fxCanB->Hide(); + if ( fxCanC.IsValid() ) fxCanC->Hide(); + + hhWeapon::Hide(); +} + + +/* +================ +hhWeaponSoulStripper::Save +================ +*/ +void hhWeaponSoulStripper::Save( idSaveGame *savefile ) const { + savefile->WriteRenderLight( beamLight ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->WriteInt( beamLightHandle ); + + beam.Save( savefile ); + beamCanA1.Save( savefile ); + beamCanB1.Save( savefile ); + beamCanC1.Save( savefile ); + beamCanA2.Save( savefile ); + beamCanB2.Save( savefile ); + beamCanC2.Save( savefile ); + beamCanA3.Save( savefile ); + beamCanB3.Save( savefile ); + beamCanC3.Save( savefile ); + fxCanA.Save( savefile ); + fxCanB.Save( savefile ); + fxCanC.Save( savefile ); + savefile->WriteBool(cansValid); + + savefile->WriteFloat( beamLength ); + savefile->WriteFloat( maxBeamLength ); + savefile->WriteObject( targetNode ); + savefile->WriteVec3( targetOffset ); + + savefile->WriteString( lastAnim ); +// savefile->WriteInt( lastAltAmmo ); +} + +/* +================ +hhWeaponSoulStripper::Restore +================ +*/ +void hhWeaponSoulStripper::Restore( idRestoreGame *savefile ) { + savefile->ReadRenderLight( beamLight ); + //HUMANHEAD PCF mdl 05/04/06 - Don't save light handles + //savefile->ReadInt( beamLightHandle ); + + beam.Restore( savefile ); + beamCanA1.Restore( savefile ); + beamCanB1.Restore( savefile ); + beamCanC1.Restore( savefile ); + beamCanA2.Restore( savefile ); + beamCanB2.Restore( savefile ); + beamCanC2.Restore( savefile ); + beamCanA3.Restore( savefile ); + beamCanB3.Restore( savefile ); + beamCanC3.Restore( savefile ); + fxCanA.Restore( savefile ); + fxCanB.Restore( savefile ); + fxCanC.Restore( savefile ); + savefile->ReadBool(cansValid); + + savefile->ReadFloat( beamLength ); + savefile->ReadFloat( maxBeamLength ); + savefile->ReadObject( reinterpret_cast ( targetNode ) ); + savefile->ReadVec3( targetOffset ); + + savefile->ReadString( lastAnim );; +} + +/* +================= +hhWeaponSoulStripper::ReadFromSnapshot +rww - update the fire controller on the client +================= +*/ +void hhWeaponSoulStripper::ClientUpdateFC(int fcType, int fcDefNumber) { + SAFE_DELETE_PTR(fireController); + + idTypeInfo *typeInfo = idClass::GetType(fcType); + const idDeclEntityDef *fcDecl = static_cast(declManager->DeclByIndex(DECL_ENTITYDEF, fcDefNumber, false)); + if (typeInfo && fcDecl) { + int num = owner->GetWeaponNum("weaponobj_soulstripper"); + assert(num); + owner->weaponInfo[ num ].ammoMax = fcDecl->dict.GetInt("ammoAmount"); + owner->spawnArgs.SetInt( "max_ammo_energy", fcDecl->dict.GetInt("ammoAmount") ); + beam_canTop = fcDecl->dict.GetString( "beam_canTop" ); + beam_canBot = fcDecl->dict.GetString( "beam_canBot" ); + beam_canGlow = fcDecl->dict.GetString( "beam_canGlow" ); + fx_can = fcDecl->dict.GetString( "fx_can" ); + DestroyCans(); + SpawnCans(); + + fireController = static_cast(typeInfo->CreateInstance()); + if (fireController) { + fireController->Init(&fcDecl->dict, this, owner.GetEntity()); + } + } +} + +/* +================= +hhWeaponSoulStripper::WriteToSnapshot +rww - write applicable weapon values to snapshot +================= +*/ +void hhWeaponSoulStripper::WriteToSnapshot( idBitMsgDelta &msg ) const { + //target entity + if (targetNode) { + msg.WriteBits(1, 1); + msg.WriteBits(targetNode->entityNumber, GENTITYNUM_BITS); + } + else { + msg.WriteBits(0, 1); + } + + //write the fire controller type + if (fireController) { + msg.WriteBits(fireController->GetType()->typeNum, idClass::GetTypeNumBits()); + msg.WriteBits(gameLocal.ServerRemapDecl(-1, DECL_ENTITYDEF, fcDeclNum ), gameLocal.entityDefBits); + } + else { + msg.WriteBits(0, idClass::GetTypeNumBits()); + msg.WriteBits(0, gameLocal.entityDefBits); + } + + hhWeapon::WriteToSnapshot(msg); +} + +/* +================= +hhWeaponSoulStripper::ReadFromSnapshot +rww - read applicable weapon values from snapshot +================= +*/ +void hhWeaponSoulStripper::ReadFromSnapshot( const idBitMsgDelta &msg ) { + //target entity + bool hasEnt = !!msg.ReadBits(1); + if (hasEnt) { + int entNum = msg.ReadBits(GENTITYNUM_BITS); + targetNode = static_cast(gameLocal.entities[entNum]); + } + else { + targetNode = NULL; + } + + int fcType = msg.ReadBits(idClass::GetTypeNumBits()); + int fcDefNumber = gameLocal.ClientRemapDecl(DECL_ENTITYDEF, msg.ReadBits(gameLocal.entityDefBits)); + + //standard weapon stuff (primarily done before switch logic to get owner) + hhWeapon::ReadFromSnapshot(msg); + + if (owner.IsValid() && fcType && (!fireController || fcType != fireController->GetType()->typeNum || !netInitialized)) { + //if there is no fire controller on the client or the fire controller does not much the type given by the server, switch + ClientUpdateFC(fcType, fcDefNumber); + netInitialized = true; + } +} + +/* +================ +hhWeaponSoulStripper::GetClipBits +================ +*/ +int hhWeaponSoulStripper::GetClipBits(void) const { + return 12; //0-4096 +} + +void hhWeaponSoulStripper::Event_LightFadeIn( float fadetime ) { + if (gameLocal.isMultiplayer) { //rww - none of this in mp + return; + } + idEntity *ent; + idEntity *next; + idLight *light; + idVec3 color; + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = next ) { + next = ent->spawnNode.Next(); + if( !gameLocal.InPlayerPVS(ent) ) + continue; + if ( !ent->IsType( idLight::Type ) ) + continue; + light = static_cast( ent ); + if ( light->GetMaterial()->IsFogLight() ) + continue; + light->GetColor( color ); + if ( color == idVec3(0.001f,0.f,0.f) ) + light->FadeIn( fadetime ); + } +} + +void hhWeaponSoulStripper::Event_LightFadeOut( float fadetime ) { + if (gameLocal.isMultiplayer) { //rww - none of this in mp + return; + } + idEntity *ent; + idEntity *next; + idLight *light; + idVec3 color; + for( ent = gameLocal.spawnedEntities.Next(); ent != NULL; ent = next ) { + next = ent->spawnNode.Next(); + if( !gameLocal.InPlayerPVS(ent) ) + continue; + if ( !ent->IsType( idLight::Type ) ) + continue; + light = static_cast( ent ); + if ( light->GetMaterial()->IsFogLight() ) + continue; + light->GetColor( color ); + if( light->spawnArgs.GetVector("_color", "1 1 1") == color && light->GetCurrentLevel()==1 ) + light->Fade(idVec4(0.001f,0.f,0.f,0.f), fadetime ); + } +} + +void hhWeaponSoulStripper::Event_LightFade( float fadeOut, float pause, float fadeIn ) { + PostEventSec( &EV_LightFadeOut, 0, fadeOut ); + PostEventSec( &EV_LightFadeIn, pause, fadeIn ); +} + +void hhWeaponSoulStripper::Event_GetFireFunction() { + if ( fireController ) { + idThread::ReturnString( fireController->GetScriptFunction() ); + } +} + +void hhWeaponSoulStripper::Event_KillBeam() { + if ( fireController && fireController->IsType(hhBeamBasedFireController::Type) ) { + hhBeamBasedFireController *beamController = static_cast(fireController); + beamController->KillBeam(); + } +} + +/*void hhWeaponSoulStripper::Event_GetFireSound() { + if ( fireController ) { + idThread::ReturnString( fireController->GetScriptFunction() ); + } +}*/ \ No newline at end of file diff --git a/src/Prey/prey_weaponsoulstripper.h b/src/Prey/prey_weaponsoulstripper.h new file mode 100644 index 0000000..1201e3d --- /dev/null +++ b/src/Prey/prey_weaponsoulstripper.h @@ -0,0 +1,182 @@ +#ifndef __HH_WEAPON_SOULSTRIPPER_H +#define __HH_WEAPON_SOULSTRIPPER_H + + +/*********************************************************************** + + hhSoulStripperAltFireController + +***********************************************************************/ +class hhSoulStripperAltFireController : public hhWeaponFireController { + CLASS_PROTOTYPE(hhSoulStripperAltFireController); +public: + virtual void LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ); +}; + + +/*********************************************************************** + + hhBeamBasedFireController + +***********************************************************************/ +class hhBeamBasedFireController : public hhWeaponFireController { + CLASS_PROTOTYPE(hhBeamBasedFireController); +public: + virtual ~hhBeamBasedFireController(); + virtual void Init( const idDict* viewDict, hhWeapon* self, hhPlayer* owner ); + virtual void LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ); + + void KillBeam(); + void Think(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + +protected: + idEntityPtr shotbeam; + int projTime; + idEntityPtr impactFx; +}; + +/*********************************************************************** + + hhSunbeamFireController + +***********************************************************************/ +class hhSunbeamFireController : public hhBeamBasedFireController { + CLASS_PROTOTYPE(hhSunbeamFireController); +public: + virtual void LaunchProjectiles( const idVec3& launchOrigin, const idMat3& aimAxis, const idVec3& pushVelocity, idEntity* projOwner ); + void Think(); +}; + +/*********************************************************************** + + hhPlasmaFireController + +***********************************************************************/ +class hhPlasmaFireController : public hhWeaponFireController { + CLASS_PROTOTYPE(hhPlasmaFireController); +public: + virtual bool LaunchProjectiles( const idVec3& pushVelocity ); +}; + +/*********************************************************************** + + hhWeaponSoulStripper + +***********************************************************************/ +#define MAX_SOUL_AMMO 3 + +typedef enum +{ + CAP_NONE = 0, + CAP_ENTITY, + CAP_INTAKE +} captureType_t; + +class hhWeaponSoulStripper : public hhWeapon { + CLASS_PROTOTYPE( hhWeaponSoulStripper ); + + public: + hhWeaponSoulStripper(); + void Spawn(); + virtual ~hhWeaponSoulStripper(); + + virtual void ParseDef( const char* objectname ); + + virtual void UpdateGUI(); + + virtual void Show(); + virtual void Hide(); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network friendliness + virtual void ClientUpdateFC(int fcType, int fcDefNumber); + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + virtual int GetClipBits(void) const; + + // beam light + renderLight_t beamLight; + int beamLightHandle; + + bool GiveEnergy( const char *energyType, bool fill ); + + void CheckCans(void); //rww + protected: + virtual void Ticker(); + char GetAnimPostfix(); + virtual void PresentWeapon( bool showViewModel ); + virtual hhWeaponFireController* CreateAltFireController(); + + void UpdateBeam( idVec3 start, bool struckEntity ); + void UpdateCanisterBeam( hhBeamSystem *system, const char *top, const char *bottom ); + + int CaptureEnergy( trace_t &results ); + + void Event_PostSpawn(); + void Event_GetAnimPostFix(); + void Event_Leech(); + void Event_EndLeech(); + + void Event_PlayAnim( int channel, const char *animname ); + void Event_PlayCycle( int channel, const char *animname ); + + void SpawnCans(); + void DestroyCans(); + + hhBeamSystem *SpawnCanisterBeam( const char *bottom, const char *top, const idStr &beamName ); + idEntity *SpawnCanisterSprite( const char *attach, const char *spriteName ); + hhEntityFx *SpawnCanisterFx( const char *attach, const idStr &name ); + + void Event_LightFadeIn( float fadetime ); + void Event_LightFadeOut( float fadetime ); + void Event_LightFade( float fadeOut, float pause, float fadeIn ); + void Event_GetFireFunction(); + void Event_KillBeam(); + + protected: + idEntityPtr beam; + + // Beams for each canister + idEntityPtr beamCanA1; // bottom to center + idEntityPtr beamCanB1; // bottom to center + idEntityPtr beamCanC1; // bottom to center + idEntityPtr beamCanA2; // top to center + idEntityPtr beamCanB2; // top to center + idEntityPtr beamCanC2; // top to center + idEntityPtr beamCanA3; // top to bottom + idEntityPtr beamCanB3; // top to bottom + idEntityPtr beamCanC3; // top to bottom + + idEntityPtr fxCanA; + idEntityPtr fxCanB; + idEntityPtr fxCanC; + + float beamLength; + float maxBeamLength; + + hhEnergyNode *targetNode; // Target struck by the beam + idVec3 targetOffset; // Offset from the origin of target + int targetTime; + + idStr lastAnim; // For smoothing the animation when picking up an ammo_soul -mdl + char lastCanState; + + bool empty; + + int fcDeclNum; //rww - needed for networking (and a good idea to have around anyway) + + idStr beam_canTop; + idStr beam_canBot; + idStr beam_canGlow; + idStr fx_can; + bool netInitialized; //rww + bool cansValid; //rww +}; + +#endif \ No newline at end of file diff --git a/src/Prey/prey_weaponspiritbow.cpp b/src/Prey/prey_weaponspiritbow.cpp new file mode 100644 index 0000000..a833a05 --- /dev/null +++ b/src/Prey/prey_weaponspiritbow.cpp @@ -0,0 +1,378 @@ +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/*********************************************************************** + + hhSpiritBowFireController + +***********************************************************************/ +CLASS_DECLARATION( hhWeaponFireController, hhSpiritBowFireController ) +END_CLASS + +/* +================= +hhSpiritBowFireController::AmmoRequired +================= +*/ +int hhSpiritBowFireController::AmmoRequired() const { + if( owner->IsDeathWalking() || owner->IsPossessed() ) { + return 0; + } + else { // CJR: will use any available ammo until the player is at zero ammo + int ammoAmount = owner->inventory.ammo[ GetAmmoType() ]; + if ( ammoAmount > 0 && ammoAmount < ammoRequired ) { + return ammoAmount; + } + } + + return ammoRequired; +} + +/* +================= +hhSpiritBowFireController::GetProjectileDict +================= +*/ +const idDict* hhSpiritBowFireController::GetProjectileDict() const +{ + //if( owner->inventory.maxHealth > 100 && !owner->IsDeathWalking() && !gameLocal.isMultiplayer ) + // return gameLocal.FindEntityDefDict( dict->GetString("def_projectileSuper"), false ); + + return hhWeaponFireController::GetProjectileDict(); +} + +/*********************************************************************** + + hhWeaponSpiritBow + +***********************************************************************/ +const idEventDef EV_UpdateBowVision( "updateBowVision" ); +const idEventDef EV_StartSeeThroughWalls( "startBowVision" ); +const idEventDef EV_FadeOutSeeThroughWalls( "fadeOutBowVision" ); +const idEventDef EV_StopSeeThroughWalls( "stopBowVision" ); +const idEventDef EV_BowVisionIsEnabled( "visionIsEnabled", NULL, 'd' ); + +CLASS_DECLARATION( hhWeaponZoomable, hhWeaponSpiritBow ) + EVENT( EV_UpdateBowVision, hhWeaponSpiritBow::Event_UpdateBowVision ) + EVENT( EV_StartSeeThroughWalls, hhWeaponSpiritBow::Event_StartSeeThroughWalls ) + EVENT( EV_FadeOutSeeThroughWalls, hhWeaponSpiritBow::Event_FadeOutSeeThroughWalls ) + EVENT( EV_StopSeeThroughWalls, hhWeaponSpiritBow::Event_StopSeeThroughWalls ) + EVENT( EV_BowVisionIsEnabled, hhWeaponSpiritBow::Event_BowVisionIsEnabled ) +END_CLASS + +/* +================= +hhWeaponSpiritBow::~hhWeaponSpiritBow +================= +*/ +hhWeaponSpiritBow::~hhWeaponSpiritBow() { + if( BowVisionIsEnabled() ) { + BowVisionIsEnabled( false ); + StopBowVision(); + } +} + +/* +================= +hhWeaponSpiritBow::Spawn +================= +*/ +void hhWeaponSpiritBow::Spawn() { + updateRover = 0; //rww - must be initialized! (sync'd over net) + BowVisionIsEnabled( false ); +} + +/* +================= +hhWeaponSpiritBow::BeginAltAttack +================= +*/ +void hhWeaponSpiritBow::BeginAltAttack( void ) { + if (owner->inventory.requirements.bCanUseBowVision) { + hhWeapon::BeginAltAttack(); + } +} + +/* +================= +hhWeaponSpiritBow::Ticker +================= +*/ +void hhWeaponSpiritBow::Ticker() { + // Fade out the alt-mode effect + if (!fadeAlpha.IsDone(gameLocal.GetTime())) { + float alpha = fadeAlpha.GetCurrentValue(gameLocal.GetTime()); + owner->playerView.SetViewOverlayColor(idVec4(alpha, alpha, alpha, alpha)); + } +} + +/* +================= +hhWeaponSpiritBow::GetProxyOf +================= +*/ +hhTargetProxy *hhWeaponSpiritBow::GetProxyOf( const idEntity *target ) { + hhTargetProxy *proxy; + // Determine whether this target is proxied by searching their bind list + for( idEntity* entity = target->GetTeamChain(); entity; entity = entity->GetTeamChain() ) { + if( entity && entity->IsType(hhTargetProxy::Type) ) { + proxy = static_cast(entity); + if( owner == proxy->GetOwner() ) { + return proxy; + } + } + } + + return NULL; +} + +/* +================= +hhWeaponSpiritBow::ProxyShouldBeVisible +================= +*/ +#define NOTVISIBLE 0 +#define ISVISIBLE 1 +#define FORCEVISIBILITY 2 +bool hhWeaponSpiritBow::ProxyShouldBeVisible( const idEntity* ent ) { + int visibilityType = ent->spawnArgs.GetInt("bowVisibilityType", "1"); + + if( visibilityType == FORCEVISIBILITY ) { + return true; + } + + return ent->fl.takedamage && visibilityType >= ISVISIBLE; +} + +/* +================= +hhWeaponSpiritBow::StopBowVision +================= +*/ +void hhWeaponSpiritBow::StopBowVision() { + if( !owner.IsValid() || !owner.GetEntity() || !owner->IsType(hhPlayer::Type) ) { + return; + } + + // Remove overlay + if( owner->IsSpiritWalking() ) { + owner->playerView.SetViewOverlayMaterial( declManager->FindMaterial(owner->spawnArgs.GetString("mtr_Spiritwalk")) ); + } + else { + owner->playerView.SetViewOverlayMaterial( NULL ); + } +} + +/* +================= +hhWeaponSpiritBow::Event_UpdateBowVision +================= +*/ +void hhWeaponSpiritBow::Event_UpdateBowVision() { + hhTargetProxy *proxy = NULL; + idEntity *ent = NULL; + + // In order to hit all possible entities (active or inactive) at low cost, we use a rover + // to traverse the entity list a little each tick, making a complete traversal about once + // a second. + + float fuse = altFireController->GetProjectileDict()->GetFloat( "fuse" ); + idVec3 velocity = altFireController->GetProjectileDict()->GetVector( "velocity" ); + float maxDistance = velocity.Length() * fuse; + float distSquared = maxDistance * maxDistance; + + // for all damageable active entities in the fuse radius + for (int ix=0; ix= MAX_GENTITIES) { + updateRover = 0; + } + ent = gameLocal.entities[updateRover]; + if (!ent || owner==ent ) { + continue; + } + + if( !ProxyShouldBeVisible(ent) ) { + continue; + } + + // PVS Check? + + if ((ent->GetOrigin() - GetOrigin()).LengthSqr() > distSquared) { + continue; + } + + // if already has proxy, continue + proxy = GetProxyOf(ent); + if (proxy) { + proxy->StayAlive(); + continue; + } + + if (!gameLocal.isClient) { + proxy = static_cast( gameLocal.SpawnObject(dict->GetString("def_targetproxy")) ); + if (proxy) { + proxy->SetOriginal(ent); + proxy->SetOwner(owner.GetEntity()); + proxy->UpdateVisualState(); + } + } + } +} + +/* +================= +hhWeaponSpiritBow::Event_StartSeeThroughWalls +================= +*/ +void hhWeaponSpiritBow::Event_StartSeeThroughWalls() { + // Cancel any pending removals/fades + CancelEvents( &EV_StopSeeThroughWalls ); + + updateRover = 0; // start at begining of list + + fadeAlpha.Init(gameLocal.GetTime(), BOWVISION_FADEIN_DURATION, 0.0f, 1.0f); + + StartSound( "snd_altmodefadein", SND_CHANNEL_ANY, 0, true, NULL ); + + // Apply overlay material + const idMaterial *mat = declManager->FindMaterial( spawnArgs.GetString("mtr_overlay") ); + owner->playerView.SetViewOverlayMaterial( mat ); + owner->playerView.SetViewOverlayColor( idVec4(1.0f, 1.0f, 1.0f, 0.0f) ); + + BowVisionIsEnabled( true ); +} + +/* +================= +hhWeaponSpiritBow::Event_FadeOutSeeThroughWalls + Fading the effect out, notify proxies to start fading too +================= +*/ +void hhWeaponSpiritBow::Event_FadeOutSeeThroughWalls() { + idEntity *ent; + hhTargetProxy *proxy; + + fadeAlpha.Init(gameLocal.GetTime(), BOWVISION_FADEOUT_DURATION, 1.0f, 0.0f); + owner->playerView.SetViewOverlayColor(colorWhite); + + StartSound( "snd_altmodefadeout", SND_CHANNEL_ANY, 0, true, NULL ); + +/*FIXME: Should use this format instead for speed +for ( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if ( ent->IsType( idLight::Type ) ) { + idLight *light = static_cast(ent); + } +}*/ + //TODO: Could store all created(owned) proxies in a list, for faster freeing here + // Unmark all proxies that were set for this owner + for (int ix=0; ixIsType(hhTargetProxy::Type)) { + continue; + } + + proxy = static_cast(ent); + if (owner==proxy->GetOwner()) { + proxy->ProxyFinished(); + } + } + + PostEventMS( &EV_StopSeeThroughWalls, BOWVISION_FADEOUT_DURATION ); + + BowVisionIsEnabled( false ); +} + +/* +================= +hhWeaponSpiritBow::Event_StopSeeThroughWalls +================= +*/ +void hhWeaponSpiritBow::Event_StopSeeThroughWalls() { + BowVisionIsEnabled( false ); + StopBowVision(); +} + +/* +================= +hhWeaponSpiritBow::Event_BowVisionIsEnabled +================= +*/ +void hhWeaponSpiritBow::Event_BowVisionIsEnabled() { + idThread::ReturnInt( BowVisionIsEnabled() ); +} + +/* +================ +hhWeaponSpiritBow::Save +================ +*/ +void hhWeaponSpiritBow::Save( idSaveGame *savefile ) const { + savefile->WriteFloat( fadeAlpha.GetStartTime() ); // idInterpolate + savefile->WriteFloat( fadeAlpha.GetDuration() ); + savefile->WriteFloat( fadeAlpha.GetStartValue() ); + savefile->WriteFloat( fadeAlpha.GetEndValue() ); + + savefile->WriteInt( updateRover ); + savefile->WriteBool( visionEnabled ); +} + +/* +================ +hhWeaponSpiritBow::Restore +================ +*/ +void hhWeaponSpiritBow::Restore( idRestoreGame *savefile ) { + float set; + + savefile->ReadFloat( set ); // idInterpolate + fadeAlpha.SetStartTime( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetDuration( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetStartValue( set ); + savefile->ReadFloat( set ); + fadeAlpha.SetEndValue( set ); + + savefile->ReadInt( updateRover ); + savefile->ReadBool( visionEnabled ); +} + +/* +================= +hhWeaponSpiritBow::WriteToSnapshot +rww - write applicable weapon values to snapshot +================= +*/ +void hhWeaponSpiritBow::WriteToSnapshot( idBitMsgDelta &msg ) const { + msg.WriteBits(visionEnabled, 1); + msg.WriteBits(updateRover, GENTITYNUM_BITS); + +// msg.WriteFloat(fadeAlpha.GetCurrentValue(gameLocal.time)); + msg.WriteFloat(fadeAlpha.GetDuration()); +// msg.WriteFloat(fadeAlpha.GetEndTime()); + msg.WriteFloat(fadeAlpha.GetEndValue()); + msg.WriteFloat(fadeAlpha.GetStartTime()); + msg.WriteFloat(fadeAlpha.GetStartValue()); + + hhWeapon::WriteToSnapshot(msg); +} + +/* +================= +hhWeaponSpiritBow::ReadFromSnapshot +rww - read applicable weapon values from snapshot +================= +*/ +void hhWeaponSpiritBow::ReadFromSnapshot( const idBitMsgDelta &msg ) { + visionEnabled = !!msg.ReadBits(1); + updateRover = msg.ReadBits(GENTITYNUM_BITS); + + fadeAlpha.SetDuration(msg.ReadFloat()); + fadeAlpha.SetEndValue(msg.ReadFloat()); + fadeAlpha.SetStartTime(msg.ReadFloat()); + fadeAlpha.SetStartValue(msg.ReadFloat()); + + hhWeapon::ReadFromSnapshot(msg); +} diff --git a/src/Prey/prey_weaponspiritbow.h b/src/Prey/prey_weaponspiritbow.h new file mode 100644 index 0000000..ef2122a --- /dev/null +++ b/src/Prey/prey_weaponspiritbow.h @@ -0,0 +1,63 @@ +#ifndef __HH_WEAPON_SPIRITBOW_H +#define __HH_WEAPON_SPIRITBOW_H + +/*********************************************************************** + + hhWeaponSpiritBow + +***********************************************************************/ + +class hhSpiritBowFireController : public hhWeaponFireController { + CLASS_PROTOTYPE(hhSpiritBowFireController); +public: + int AmmoRequired() const; + virtual const idDict* GetProjectileDict() const; +}; + +class hhWeaponSpiritBow : public hhWeaponZoomable { + CLASS_PROTOTYPE( hhWeaponSpiritBow ); + + public: + ~hhWeaponSpiritBow(); + + void Spawn(); + + bool BowVisionIsEnabled() const { return visionEnabled; } + void BowVisionIsEnabled( bool enabled ) { visionEnabled = enabled; } + void StopBowVision(); + virtual void BeginAltAttack( void ); + + void Save( idSaveGame *savefile ) const; + void Restore( idRestoreGame *savefile ); + + //rww - network friendliness + virtual void WriteToSnapshot( idBitMsgDelta &msg ) const; + virtual void ReadFromSnapshot( const idBitMsgDelta &msg ); + + protected: + virtual void Ticker(); + hhTargetProxy * GetProxyOf( const idEntity *target ); + bool ProxyShouldBeVisible( const idEntity* ent ); + ID_INLINE virtual hhWeaponFireController* CreateFireController(); + + protected: + void Event_StartSeeThroughWalls(); + void Event_FadeOutSeeThroughWalls(); + void Event_StopSeeThroughWalls(); + void Event_UpdateBowVision(); + + void Event_BowVisionIsEnabled(); + + protected: + idInterpolate fadeAlpha; + int updateRover; + + bool visionEnabled; +}; + +ID_INLINE hhWeaponFireController* hhWeaponSpiritBow::CreateFireController() { + return new hhSpiritBowFireController; +} + + +#endif \ No newline at end of file diff --git a/src/Prey/sys_debugger.cpp b/src/Prey/sys_debugger.cpp new file mode 100644 index 0000000..ffbce3c --- /dev/null +++ b/src/Prey/sys_debugger.cpp @@ -0,0 +1,1102 @@ +// sys_debugger.cpp +// +// HUMANHEAD: debugger functions +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + +/* TODO: + make dormant filter? + make physics filter? + Optimize: + preallocate all the string ptrs in DisplayCell and don't free them every frame +*/ + +#if INGAME_DEBUGGER_ENABLED + +hhDebugger debugger; + +int hhDebugger::sortColumn = 1; +bool hhDebugger::sortAscending = false; + +//========================================================================= +// +// hhDisplayCell +// +//========================================================================= + +hhDisplayCell::hhDisplayCell() { + string = NULL; + type = COLUMNTYPE_NUMERIC; + value = 0.0f; +} +hhDisplayCell::~hhDisplayCell() { + if (string) { + delete string; + string = NULL; + } +} +inline void hhDisplayCell::AllocateString() { + if (!string) { + string = new idStr; + } +} +inline void hhDisplayCell::operator=( const hhDisplayCell &cell ) { + if (cell.string) { + AllocateString(); + *string = *cell.string; + } + type = cell.type; + value = cell.value; +} +inline void hhDisplayCell::operator=(const idStr &text) { + AllocateString(); + *string = text; + type = COLUMNTYPE_ALPHA; +} +inline void hhDisplayCell::operator=(const char *text) { + AllocateString(); + *string = text; + type = COLUMNTYPE_ALPHA; +} +inline void hhDisplayCell::operator=(const int i) { + value = (float)i; + type = COLUMNTYPE_NUMERIC; +} +inline void hhDisplayCell::operator=(const float f) { + value = f; + type = COLUMNTYPE_NUMERIC; +} +inline hhDisplayCell & hhDisplayCell::operator+=(const int i) { + if (type == COLUMNTYPE_ALPHA) { + AllocateString(); + *this = atoi(String()) + i; + } + else { + value += (float)i; + } + type = COLUMNTYPE_NUMERIC; + return *this; +} +inline hhDisplayCell & hhDisplayCell::operator+=(const float f) { + if (type == COLUMNTYPE_ALPHA) { + AllocateString(); + *this = (float)atof(String()) + f; + } + else { + value += f; + } + type = COLUMNTYPE_NUMERIC; + return *this; +} +inline bool hhDisplayCell::IsInt() const { + return ((type == COLUMNTYPE_NUMERIC) && (value == (int)value) ); +} +inline int hhDisplayCell::StringLength() { + if (type == COLUMNTYPE_NUMERIC) { + return strlen(String()); + } + AllocateString(); + return string->Length(); +} +inline EColumnDataType hhDisplayCell::Type() const { + return type; +} +inline float hhDisplayCell::Float() const { + if (type == COLUMNTYPE_ALPHA) { + return (float)atof(String()); + } + return value; +} +inline int hhDisplayCell::Int() const { + if (type == COLUMNTYPE_ALPHA) { + return atoi(String()); + } + return (int)value; +} +const char *hhDisplayCell::String() const { + static idStr workStr; + if (type == COLUMNTYPE_NUMERIC) { + if (IsInt()) { + workStr = idStr((int)value); + } + else { + workStr = idStr(value); + } + return workStr.c_str(); + } + if (!string) { + return ""; + } + return string->c_str(); +} + +//========================================================================= +// +// hhDebugger +// +//========================================================================= + +hhDebugger::hhDebugger() { + bInitialized = false; + bClassCollapse = true; + bDrawEntities = false; + bPointer = false; + includeFilters = excludeFilters = 0; + filterClass = NULL; + potentialSet.Clear(); + displayList.Clear(); + selectedEntity = NULL; + sortColumn = 1; + sortAscending = false; + debugMode = DEBUGMODE_NONE; + gui=NULL; + displayColumnsUsed = 0; + + xaxis.Set(25, 0, 0); + yaxis.Set( 0, 25, 0); + zaxis.Set( 0, 0, 25); +} + +hhDebugger::~hhDebugger() { +} + +void hhDebugger::Shutdown() { + // Debugger must be shutdown manually because of the dict memeber which + // must be Cleared before the global string pool is emptied + dict.Clear(); + potentialSet.Clear(); + displayList.Clear(); + // release gui resources + bInitialized = false; +} + +void hhDebugger::Initialize() { + if (!gui) { + gui = uiManager->FindGui("guis/debugger.gui", true); + } + + SetMode(DEBUGMODE_STATS); + SetClassCollapse(true); + SetDrawEntities(false); + SetPointer(false); + ClearAllFilters(); + ExcludeFilter(FILTER_CLOSE); + ExcludeFilter(FILTER_DISTANT); + + bInitialized = true; +} + +void hhDebugger::Reset() { + selectedEntity = NULL; + filterClass = NULL; + Initialize(); +} + + +//========================================================================= +// +// hhDebugger::CaptureInput +// +//========================================================================= +void hhDebugger::CaptureInput(bool bCapture) { + if (bCapture && !bInteractive) { + // Just becoming interactive + const char *cmds = gui->Activate(true, gameLocal.time); + HandleGuiCommands(cmds); + } + else if (bInteractive && !bCapture) { + // Just becoming non-interactive + const char *cmds = gui->Activate(false, gameLocal.time); + HandleGuiCommands(cmds); + } + + bInteractive = bCapture; +} + +//========================================================================= +// +// hhDebugger::ColorForIndex +// +//========================================================================= +idVec4 hhDebugger::ColorForIndex(int index, float alpha) { + idVec4 color; + int bitmask = (index % 7) + 1; + color.x = (bitmask & 1) ? 1.0f : 0.0f; + color.y = (bitmask & 2) ? 1.0f : 0.0f; + color.z = (bitmask & 4) ? 1.0f : 0.0f; + color.w = alpha; + return color; +} + +//========================================================================= +// +// hhDebugger::ColorForEntity +// +//========================================================================= +idVec4 hhDebugger::ColorForEntity(idEntity *ent, float alpha) { + // Choose color based on editor_color if possible + idVec3 color3; + idVec4 color; + if (ent->spawnArgs.GetVector("editor_color", "0 0 1", color3)) { + color.x = color3.x; + color.y = color3.y; + color.z = color3.z; + color.w = alpha; + } + else { + color = ColorForIndex(ent->entityNumber, alpha); + } + return color; +} + +//========================================================================= +// +// hhDebugger::SetSortToColumn +// +//========================================================================= +void hhDebugger::SetSortToColumn(int col) { + if (hhDebugger::sortColumn == col) { + hhDebugger::sortAscending ^= 1; + } + else { + hhDebugger::sortColumn = col; + } +} + +//========================================================================= +// +// Filter routines +// +//========================================================================= +int hhDebugger::IndexForFilter(int filter) { //OBS + int index = hhMath::ILog2(filter); + return index; +} + +int hhDebugger::FilterForIndex(int index) { + return 1<SetStateInt(va("filter%i", filterIndex), FILTERSTATE_INCLUDED); + gui->HandleNamedEvent("SetFilter"); + } +} + +void hhDebugger::ExcludeFilter(int filterIndex) { + int filter = FilterForIndex(filterIndex); + excludeFilters |= filter; + includeFilters &= ~filter; + if (gui) { + gui->SetStateInt(va("filter%i", filterIndex), FILTERSTATE_EXCLUDED); + gui->HandleNamedEvent("SetFilter"); + } +} + +void hhDebugger::ClearFilter(int filterIndex) { + int filter = FilterForIndex(filterIndex); + includeFilters &= (~filter); + excludeFilters &= (~filter); + if (gui) { + gui->SetStateInt(va("filter%i", filterIndex), FILTERSTATE_NONE); + gui->HandleNamedEvent("SetFilter"); + } +} + +void hhDebugger::ClearAllFilters() { + for (int ix=0; ixDeleteStateVar( va( "listStats_item_%i", row ) ); + } + + gui->SetStateInt("mode", debugMode); + gui->HandleNamedEvent("SetMode"); + } +} + +void hhDebugger::SetClassCollapse(bool on) { + bClassCollapse = on; + if (gui) { + gui->SetStateBool("classcollapse", bClassCollapse); + gui->HandleNamedEvent("SetClassCollapse"); + } +} + +void hhDebugger::SetDrawEntities(bool on) { + bDrawEntities = on; + if (gui) { + gui->SetStateBool("drawEntities", bDrawEntities); + gui->HandleNamedEvent("SetDrawEntities"); + } +} + +void hhDebugger::SetPointer(bool on) { + bPointer = on; + if (gui) { + gui->SetStateBool("pointer", bPointer); + gui->HandleNamedEvent("SetPointer"); + } +} + +void hhDebugger::SetSelectedEntity(idEntity *ent) { + selectedEntity = ent; +} + + +//========================================================================= +// +// hhDebugger::HandleGuiCommands +// +//========================================================================= +bool hhDebugger::HandleGuiCommands(const char *commands) { + bool ret = false; + + if (commands && commands[0]) { + idLexer src; + src.LoadMemory( commands, strlen( commands ), "guiCommands" ); + + if (HandleSingleGuiCommand(gameLocal.GetLocalPlayer(), &src)) { + ret = true; + } + } + return ret; +} + +//========================================================================= +// +// hhDebugger::HandleSingleGuiCommand +// +//========================================================================= +bool hhDebugger::HandleSingleGuiCommand(idEntity *entityGui, idLexer *src) { + idToken token; + + if (!src->ReadToken(&token)) { + return false; + } + else if (token == ";") { + return false; + } + else if (token.IcmpPrefix("guicmd_title_") == 0) { // "title_cNN" + token.ToLower(); + token.Strip("guicmd_title_c"); // "NN" + int col = atoi(token.c_str()); + SetSortToColumn(col); + return true; + } + else if (token.IcmpPrefix("guicmd_select") == 0) { // "guicmd_select" + int row = gui->State().GetInt("listStats_sel_0", "-1"); + if (row >= 0) { + TranslateRowCommand(row); + } + } + else if (token.IcmpPrefix("guicmd_cyclemode") == 0) { // "guicmd_cyclemode" + SetMode( (EDebugMode)((GetMode() + 1) % NUM_DEBUGMODES) ); + } + else if (token.IcmpPrefix("guicmd_drawentities") == 0) { // "guicmd_drawentities" + SetDrawEntities(!GetDrawEntities()); + } + else if (token.IcmpPrefix("guicmd_classcollapse") == 0) { // "guicmd_classcollapse" + SetClassCollapse(!GetClassCollapse()); + } + else if (token.IcmpPrefix("guicmd_pointer") == 0) { // "guicmd_pointer" + SetPointer(!GetPointer()); + } + else if (token.IcmpPrefix("guicmd_filter") == 0) { // "guicmd_filter" + token.ToLower(); + token.Strip("guicmd_filter"); // "NN" + int filterIndex = atoi(token.c_str()); + + // toggle 3-way state + Toggle3wayFilter(filterIndex); + + // If turning off the class filter, revert back to ClassCollapse + if (filterIndex == FILTER_CLASS && !FilterIncluded(FILTER_CLASS)) { + SetClassCollapse(true); + } + } + + src->UnreadToken(&token); + return false; +} + +//========================================================================= +// +// hhDebugger::TranslateRowCommand +// +//========================================================================= +void hhDebugger::TranslateRowCommand(int row) { + if (GetMode() != DEBUGMODE_STATS) { + return; + } + + // clicked on a row + if (GetClassCollapse()) { + idTypeInfo *clickedClass = ClassForRow(row); + if (clickedClass) { + IncludeFilter(FILTER_CLASS); + filterClass = clickedClass; + SetClassCollapse(false); + } + } + else { + idEntity *clickedEntity = EntityForRow(row); + if (clickedEntity) { + if (!selectedEntity.IsValid() || clickedEntity != selectedEntity.GetEntity()) { + selectedEntity = clickedEntity; + } + else { + if (GetMode()==DEBUGMODE_STATS) { + SetMode(DEBUGMODE_GAMEINFO1); + } + } + } + } +} + +//========================================================================= +// +// hhDebugger::DeterminePotentialSet +// +//========================================================================= +void hhDebugger::DeterminePotentialSet() { + + float maxDistSquared = g_maxShowDistance.GetFloat()*g_maxShowDistance.GetFloat(); + float minDistSquared = DEBUG_MIN_ENT_DIST*DEBUG_MIN_ENT_DIST; + idPlayer *player = gameLocal.GetLocalPlayer(); + idVec3 playerPosition = player->GetEyePosition(); + idEntity *ent; + + potentialSet.Clear(); +/*FIXME: Should use this format instead for speed +for ( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) { + if ( ent->IsType( idLight::Type ) ) { + idLight *light = static_cast(ent); + } +}*/ + //FIXME: Use gameEdit->FindEntity ??? + for (int ix=0; ixGetType() != filterClass) ) { + continue; + } + if (FilterExcluded(FILTER_CLASS) && (ent->GetType() == filterClass) ) { + continue; + } + + if (FilterIncluded(FILTER_VISIBLE) && ent->IsHidden()) { // worthless, replace with more specific active filters (THINK,ANIM,UPDATEVISUALS,PHYSICS) + continue; + } + if (FilterExcluded(FILTER_VISIBLE) && !ent->IsHidden()) { + continue; + } + + if (FilterIncluded(FILTER_ACTIVE) && !ent->IsActive()) { + continue; + } + if (FilterExcluded(FILTER_ACTIVE) && ent->IsActive()) { + continue; + } + + if (FilterIncluded(FILTER_MONSTERS) && !ent->IsType(idAI::Type)) { + continue; + } + if (FilterExcluded(FILTER_MONSTERS) && ent->IsType(idAI::Type)) { + continue; + } + + if (FilterIncluded(FILTER_HASMODEL) && ent->GetModelDefHandle() == -1) { + continue; + } + if (FilterExcluded(FILTER_HASMODEL) && ent->GetModelDefHandle() != -1) { + continue; + } + + if (FilterIncluded(FILTER_EXPENSIVE) && ent->thinkMS < g_expensiveMS.GetFloat()) { + continue; + } + if (FilterExcluded(FILTER_EXPENSIVE) && ent->thinkMS > g_expensiveMS.GetFloat()) { + continue; + } + + if (FilterIsActive(FILTER_ANIMATING)) { + bool isAnimating = ent->GetAnimator() && ent->GetAnimator()->IsAnimating( gameLocal.time ); + if (FilterIncluded(FILTER_ANIMATING) && !isAnimating) { + continue; + } + if (FilterExcluded(FILTER_ANIMATING) && isAnimating) { + continue; + } + } + + if (FilterIsActive(FILTER_DORMANT)) { + + bool isDormant = ent->fl.isDormant; + if (FilterIncluded(FILTER_DORMANT) && !isDormant) { + continue; + } + if (FilterExcluded(FILTER_DORMANT) && isDormant) { + continue; + } + } + + if (FilterIsActive(FILTER_ACTIVESOUNDS)) { + bool playingSounds = ent->GetSoundEmitter() && ent->GetSoundEmitter()->CurrentlyPlaying(); + if (FilterIncluded(FILTER_ACTIVESOUNDS) && !playingSounds) { + continue; + } + if (FilterExcluded(FILTER_ACTIVESOUNDS) && playingSounds) { + continue; + } + } + + if (FilterIsActive(FILTER_CLOSE) || FilterIsActive(FILTER_DISTANT)) { + idVec3 entPos = ent->GetPhysics()->GetOrigin(); + idVec3 toEnt = entPos - playerPosition; + float distsqr = toEnt.LengthSqr(); + if (FilterIncluded(FILTER_CLOSE) && (distsqr > minDistSquared)) { + continue; + } + if (FilterExcluded(FILTER_CLOSE) && (distsqr < minDistSquared)) { + continue; + } + if (FilterIncluded(FILTER_DISTANT) && (distsqr < maxDistSquared)) { + continue; + } + if (FilterExcluded(FILTER_DISTANT) && (distsqr > maxDistSquared)) { + continue; + } + } + + potentialSet.Append(ent); + } +} + +//========================================================================= +// +// hhDebugger::DetermineSelectionSet +// +//========================================================================= +void hhDebugger::DetermineSelectionSet() { + + // Clear selectedEntity if it has been removed +// if (selectedEntity && !gameLocal.entities[selectedEntity->entityNumber]) { +// selectedEntity = NULL; +// } + + if (!GetPointer()) { + return; + } + + idPlayer *player = gameLocal.GetLocalPlayer(); + idVec3 playerPosition = player->GetEyePosition(); + idVec4 color; + idVec3 entPos; + idVec3 toEnt; + idEntity *ent = NULL; + idEntity *bestEnt = NULL; + float bestScore = 0.0f; + + int num = potentialSet.Num(); + for (int ix=0; ixGetPhysics()->GetOrigin(); + toEnt = entPos - playerPosition; + float dist = toEnt.Length(); + float score = (toEnt * (player->viewAngles.ToMat3()[0])) / dist; + if (score > bestScore) { + bestScore = score; + bestEnt = ent; + } + } + + selectedEntity = bestEnt; +} + +//========================================================================= +// +// idListDefaultCompare +// +// Compares two pointers to hhDisplayItem. Used to sort. +//========================================================================= +template<> +ID_INLINE int idListSortCompare( const hhDisplayItem *a, const hhDisplayItem *b ) { + int sortColumn = hhDebugger::sortColumn; + + if (hhDebugger::sortAscending) { + if (a->column[sortColumn].Type() == COLUMNTYPE_ALPHA) { + return ( idStr::Icmp(b->column[sortColumn].String(), a->column[sortColumn].String() ) ); + } + else { // Sort numerically - need to multiply up because we're comparing integers + return (int)(1000.0 * ( a->column[sortColumn].Float() - + b->column[sortColumn].Float() )); // sort by percentage + } + } + else { + if (a->column[sortColumn].Type() == COLUMNTYPE_ALPHA) { + return ( idStr::Icmp(a->column[sortColumn].String(), b->column[sortColumn].String() ) ); + } + else { // Sort numerically - need to multiply up because we're comparing integers + return (int)(1000.0 * ( b->column[sortColumn].Float() - + a->column[sortColumn].Float() )); // sort by percentage + } + } +} + +const char *GetThinkFlags(idEntity *ent) { + static char buffer[20]; + + buffer[0] = 0; + if (ent->thinkFlags & TH_THINK) { + strcat(buffer, "T"); + } + if (ent->thinkFlags & TH_ANIMATE) { + strcat(buffer, "A"); + } + if (ent->thinkFlags & TH_PHYSICS) { + strcat(buffer, "P"); + } + if (ent->thinkFlags & TH_UPDATEVISUALS) { + strcat(buffer, "R"); + } + if (ent->thinkFlags & TH_UPDATEPARTICLES) { + strcat(buffer, "U"); + } + if (ent->thinkFlags & TH_TICKER) { + strcat(buffer, "K"); + } + + return buffer; +} + +//========================================================================= +// +// hhDebugger::FillDisplayList_Stats +// +//========================================================================= +void hhDebugger::FillDisplayList_Stats() { + idEntity *ent; + int slot, ix, num; + float totalThinkMS = 0.0f; + + num = potentialSet.Num(); + for (ix=0; ixGetClassname(); + slot = displayList.AddUnique(item); + } + else { + item.column[0] = ent->name; + slot = displayList.Append(item); // All entity names should be unique but just in case + } + + // Fill columns with appropriate stats + hhDisplayItem *row = &displayList[slot]; + if (GetClassCollapse()) { + //class count dormant (unused) active time % events + row->column[1] += 1; // Count - number in this class + if (ent->fl.isDormant) { // HUMANHEAD JRM - changed to fl.isDormant + row->column[2] += 1; // dormant + } + + // Column 3 is unused + row->column[3] = 0; + + if (ent->IsActive()) { + row->column[4] += 1; // active - number that are active + row->column[5] += ent->thinkMS; // thinkMS + totalThinkMS += ent->thinkMS; // column 6 is percentage + } + row->column[7] += idEvent::NumQueuedEvents(ent); + displayColumnsUsed = 8; + } + else { + //entity entitynum thFlags/active time % events + row->column[1] = ent->entityNumber; + if (ent->fl.isDormant) { // HUMANHEAD JRM - Changed to fl.isDormant + row->column[2] = 1; // dormant + } + + // Column 3 is unused + row->column[3] = 0; + + if (ent->IsActive()) { + row->column[4] = GetThinkFlags(ent); // active - number that are active + row->column[5] = ent->thinkMS; // thinkMS + totalThinkMS += ent->thinkMS; // column 6 is percentage + } + row->column[7] += idEvent::NumQueuedEvents(ent); + displayColumnsUsed = 8; + } + } + + // divide by totalMS to get percentage + num = displayList.Num(); + float recip = totalThinkMS > 0.0f ? (1.0f / totalThinkMS) : 0.0f; + for (ix=0; ixspawnArgs.GetNumKeyVals(); + for (int ix=0; ixspawnArgs.GetKeyVal(ix); + if (!kv->GetKey().IcmpPrefix("editor_")) { + continue; + } + + // Create a row with these two columns + hhDisplayItem item; + item.column[0] = kv->GetKey(); + item.column[1] = kv->GetValue(); + displayList.Append(item); + } + displayColumnsUsed = 2; + } +} + +//========================================================================= +// +// hhDebugger::FillDisplayList_GameInfo +// +//========================================================================= +void hhDebugger::FillDisplayList_GameInfo(int page) { + const idKeyValue *kv; + + if (selectedEntity.IsValid()) { + // Retrieve dictionary of variables from entity + dict.Clear(); + + selectedEntity->FillDebugVars(&dict, page); + + // Create a cleared item + hhDisplayItem item; + + // Fill item with dictionary entries + int num = dict.GetNumKeyVals(); + for (int ix=0; ixGetKey(); + item.column[1] = kv->GetValue(); + displayList.Append(item); + } + + displayColumnsUsed = 2; + } +} + +//========================================================================= +// +// hhDebugger::UpdateGUI_Entity +// +//========================================================================= +void hhDebugger::UpdateGUI_Entity() { + + // Fill display list depending on mode + displayList.Clear(); + displayColumnsUsed = 0; + switch(GetMode()) { + case DEBUGMODE_STATS: FillDisplayList_Stats(); break; + case DEBUGMODE_SPAWNARGS: FillDisplayList_SpawnArgs(); break; + case DEBUGMODE_GAMEINFO1: FillDisplayList_GameInfo(1); break; + case DEBUGMODE_GAMEINFO2: FillDisplayList_GameInfo(2); break; + case DEBUGMODE_GAMEINFO3: FillDisplayList_GameInfo(3); break; + case DEBUGMODE_NONE: break; + } + + // Sort display list based on sortColumn + displayList.Sort(); + + FillGUI(); + gui->StateChanged(gameLocal.time); // Notify the gui that variables have changed +} + +//========================================================================= +// +// hhDebugger::FillGUI +// +//========================================================================= +void hhDebugger::FillGUI() { + + int row,col; + int numrows = displayList.Num(); + int numcols = displayColumnsUsed; + char rowText[1024]; + + for (row=0; rowSetStateString( va("listStats_item_%i", row ), rowText ); + } + else { + gui->DeleteStateVar( va( "listStats_item_%i", row ) ); + } + } +} + +//========================================================================= +// +// hhDebugger::DrawPotentialSet +// +//========================================================================= +void hhDebugger::DrawPotentialSet() { + idVec4 color; + idVec3 entPos; + idEntity *ent; + + if (!GetDrawEntities()) { + return; + } + + // Draw potential entities as crosses + int num = potentialSet.Num(); + for (int ix=0; ixGetPhysics()) { + continue; + } + + // Draw cross at all entities and label + color = ColorForEntity(ent, 0.5f); + entPos = ent->GetPhysics()->GetOrigin(); + + hhUtils::DebugCross(color, entPos, 25); + gameRenderWorld->DrawText(ent->name.c_str(), entPos, 0.15f, color, gameLocal.GetLocalPlayer()->viewAngles.ToMat3(), 1, 0); + } +} + +//========================================================================= +// +// hhDebugger::DrawSelectionSet +// +//========================================================================= +void hhDebugger::DrawSelectionSet() { + if (selectedEntity.IsValid() && selectedEntity->GetPhysics()) { + idVec3 entPos = selectedEntity->GetPhysics()->GetOrigin(); + idVec4 color = ColorForEntity(selectedEntity.GetEntity(), 1.0f); + + // Draw selected entities as highlighted + if (GetMode() != DEBUGMODE_GAMEINFO2) { + hhUtils::DebugCross(color*2, entPos, 10, 0); + } + + gameRenderWorld->DebugBox(color*2, + idBox(selectedEntity->GetPhysics()->GetBounds(), entPos, selectedEntity->GetPhysics()->GetAxis()), 0); + + // Allow entity to do custom debug drawing + switch(GetMode()) { + case DEBUGMODE_GAMEINFO1: + selectedEntity->DrawDebug(1); + break; + case DEBUGMODE_GAMEINFO2: + selectedEntity->DrawDebug(2); + break; + case DEBUGMODE_GAMEINFO3: + selectedEntity->DrawDebug(3); + break; + default: + selectedEntity->DrawDebug(0); + break; + } + } +} + +//========================================================================= +// +// hhDebugger::DrawGUI +// +//========================================================================= +void hhDebugger::DrawGUI() { + PROFILE_SCOPE("Profilers", PROFMASK_NORMAL); + if (gui) { + gui->Redraw(gameLocal.time); + } +} + +//========================================================================= +// +// hhDebugger::UpdateDebugger +// +//========================================================================= +void hhDebugger::UpdateDebugger() { + PROFILE_SCOPE("Profilers", PROFMASK_NORMAL); + + if (!gameLocal.GetLocalPlayer()) { + return; + } + + uiManager->SetDebuggerInteractive(IsInteractive()); + + // HUMANHEAD PCF pdm 05/14/06: Don't initialize/load the gui unless it's actually being used. + if (g_debugger.GetInteger() && !bInitialized) { + Initialize(); + } + + CaptureInput(gui && g_debugger.GetInteger() == 2); + + if (gui && g_debugger.GetInteger()) { + HandleFrameEvents(); + + DeterminePotentialSet(); + DetermineSelectionSet(); + + UpdateGUI_Entity(); + + DrawPotentialSet(); + DrawSelectionSet(); + + DrawGUI(); + + if (IsInteractive()) { + gui->DrawCursor(); + } + } +} + +// This code stolen from GuiFrameEvents() +void hhDebugger::HandleFrameEvents() { + const char *cmd; + sysEvent_t ev; + static int oldMouseX=0; + static int oldMouseY=0; + static int oldButton=0; + static bool oldDown=false; + int newX, newY; + int newButton; + bool buttonDown; + + memset( &ev, 0, sizeof( ev ) ); + + if (bInteractive) { + // fake up a mouse event based on the deltas tracked + // by the async usercmd generation + uiManager->GetMouseState( &newX, &newY, &newButton, &buttonDown ); + if ( newX != oldMouseX || newY != oldMouseY ) { + ev.evType = SE_MOUSE; + ev.evValue = newX - oldMouseX; + ev.evValue2 = newY - oldMouseY; + cmd = gui->HandleEvent( &ev, gameLocal.time ); + if ( cmd && cmd[0] ) { + HandleGuiCommands( cmd ); + } + + oldMouseX = newX; + oldMouseY = newY; + } + + if ( newButton != oldButton || buttonDown != oldDown) { + ev.evType = SE_KEY; + ev.evValue = newButton; + ev.evValue2 = buttonDown; + cmd = gui->HandleEvent( &ev, gameLocal.time ); + if ( cmd && cmd[0] ) { + HandleGuiCommands( cmd ); + } + oldButton = newButton; + oldDown = buttonDown; + } + } + + ev.evType = SE_NONE; + cmd = gui->HandleEvent( &ev, gameLocal.time ); + if ( cmd && cmd[0] ) { + gameLocal.Printf( "frame event returned: '%s'\n", cmd ); + } +} + +#endif //INGAME_DEBUGGER_ENABLED \ No newline at end of file diff --git a/src/Prey/sys_debugger.h b/src/Prey/sys_debugger.h new file mode 100644 index 0000000..03d9dcb --- /dev/null +++ b/src/Prey/sys_debugger.h @@ -0,0 +1,187 @@ +#ifndef __SYS_DEBUGGER_H__ +#define __SYS_DEBUGGER_H__ + +#if INGAME_DEBUGGER_ENABLED + +#define DEBUG_MIN_ENT_DIST 75 // Minimum distance to entity for FILTER_DISTANCE +#define DEBUG_NUM_COLS 10 // Number of cols in debug_columns.gui (max supported) +#define DEBUG_NUM_ROWS 200 // Number of rows in debug_columns.gui (max supported) +#define NUM_DISPLAYLIST_COLUMNS 10 + +class idTypeInfo; + +enum EDebugMode { + DEBUGMODE_NONE=0, + DEBUGMODE_STATS, + DEBUGMODE_SPAWNARGS, + DEBUGMODE_GAMEINFO1, + DEBUGMODE_GAMEINFO2, + DEBUGMODE_GAMEINFO3, + NUM_DEBUGMODES +}; + +enum EColumnDataType { + COLUMNTYPE_ALPHA, + COLUMNTYPE_NUMERIC +}; + +// Filter bitmasks +#define FILTER_NONE 0 +#define FILTER_CLASS 1 +#define FILTER_ACTIVE 2 +#define FILTER_DISTANT 3 +#define FILTER_MONSTERS 4 +#define FILTER_ACTIVESOUNDS 5 +#define FILTER_HASMODEL 6 +#define FILTER_ANIMATING 7 +#define FILTER_EXPENSIVE 8 +#define FILTER_VISIBLE 9 +#define FILTER_CLOSE 10 +#define FILTER_DORMANT 11 +#define NUM_FILTERS 12 + +#define FILTERSTATE_NONE 0 +#define FILTERSTATE_INCLUDED 1 +#define FILTERSTATE_EXCLUDED 2 + +class hhDisplayCell { +public: + hhDisplayCell(); + ~hhDisplayCell(); + + inline void operator=( const idStr &text ); + inline void operator=( const char *text ); + inline void operator=( const int i ); + inline void operator=( const float f ); + inline void operator=( const hhDisplayCell &cell ); + + inline hhDisplayCell & operator+=( const int i ); + inline hhDisplayCell & operator+=( const float f ); + + inline bool IsInt() const; + inline int StringLength(); + inline EColumnDataType Type() const; + inline float Float() const; + inline int Int() const; + const char * String() const; + +private: + inline void AllocateString(); + + idStr *string; + EColumnDataType type; + float value; +}; + + + +class hhDisplayItem { +public: + hhDisplayCell column[NUM_DISPLAYLIST_COLUMNS]; + friend bool operator==( const hhDisplayItem &a, const hhDisplayItem &b ) { + return !idStr::Icmp(a.column[0].String(), b.column[0].String()); + }; + // idList<>::Append() uses operator= on it's objects, so we need to override the + // default memcpy() functionality, with something that constructs a new idStr in hhDisplayCell + // Force idList<> to copy each element individually, so hhDisplayCell::operator=() is used + inline void operator=( const hhDisplayItem &item ) { + for (int ix=0; ix selectedEntity; // Entity currently selected by GUI choice or pointer + + idVec3 xaxis; + idVec3 yaxis; + idVec3 zaxis; + + idTypeInfo *filterClass; // Class to be used when FILTER_CLASS is active + int includeFilters; // Bitmask of current inclusion filters used to cull potentialSet + int excludeFilters; // Bitmask of current exclusion filters used to cull potentialSet + + idList potentialSet; // All entities that pass the filters + idList displayList; + int displayColumnsUsed; // Number of columns of display list actually used + + EDebugMode debugMode; // Mode for debugger display + idUserInterface *gui; + idDict dict; // Scratch pad dictionary, needs to be persistent over + // the time of UpdateDebugger() +}; + + +extern hhDebugger debugger; + +#endif // INGAME_DEBUGGER_ENABLED + +#endif // __SYS_DEBUGGER_H__ diff --git a/src/Prey/sys_preycmds.cpp b/src/Prey/sys_preycmds.cpp new file mode 100644 index 0000000..19bd278 --- /dev/null +++ b/src/Prey/sys_preycmds.cpp @@ -0,0 +1,1121 @@ +// sys_preycmds.cpp +// + +#include "../idlib/precompiled.h" +#pragma hdrstop + +#include "prey_local.h" + + +#if INGAME_PROFILER_ENABLED +void Profiler_ZoomOut_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_TRAVERSEOUT); } +void Profiler_Mode_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_TOGGLEMODE); } +void Profiler_MSMode_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_TOGGLEMS); } +void Profiler_Smooth_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_TOGGLESMOOTHING); } +void Profiler_ToggleCapture_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_TOGGLECAPTURE); } +void Profiler_Toggle_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_TOGGLE); } +void Profiler_Release_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_RELEASE); } +void Profiler_In0_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN0); } +void Profiler_In1_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN1); } +void Profiler_In2_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN2); } +void Profiler_In3_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN3); } +void Profiler_In4_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN4); } +void Profiler_In5_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN5); } +void Profiler_In6_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN6); } +void Profiler_In7_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN7); } +void Profiler_In8_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN8); } +void Profiler_In9_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN9); } +void Profiler_In10_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN10); } +void Profiler_In11_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN11); } +void Profiler_In12_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN12); } +void Profiler_In13_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN13); } +void Profiler_In14_f( const idCmdArgs &args ) { profiler->SubmitFrameCommand(PROFCOMMAND_IN14); } +#endif + +void Cmd_GetMapName_f( const idCmdArgs &args ) { + gameLocal.Printf( "%s\n", gameLocal.GetMapName() ); +} + +void Cmd_TestText_f( const idCmdArgs &args ) { + char translated[1024]; + + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: testtext \n"); + return; + } + common->FixupKeyTranslations(args.Argv(1), translated, 1024); // No passing idStr between game and engine + gameLocal.Printf("%s\n", translated); +} + +void Cmd_GetBinds_f( const idCmdArgs &args ) { + const char *entityname; + + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: getbinds \n"); + return; + } + + if ( !gameLocal.CheatsOk() ) { + return; + } + + // get name of entity + entityname = args.Argv( 1 ); + + // find entity by name or classname + idEntity *master = gameLocal.FindEntity(entityname); + if (master) { + int count = 0; + for (int i = 0; i < gameLocal.num_entities; i++) { + idEntity *ent = gameLocal.entities[i]; + if ( ent && ent->GetBindMaster() == master ) { + gameLocal.Printf(" %s bound (%s)\n", ent->GetName(), ent->GetClassname()); + count++; + } + } + gameLocal.Printf("%d entities bound to %s\n", count, entityname); + } +} + +void Cmd_SpiritWalkMode_f( const idCmdArgs &args ) { + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: envirosuit <1|0>\n"); + return; + } + + bool on = atoi(args.Argv( 1 )) != 0; + gameLocal.SpiritWalkSoundMode( on ); +} + +void Cmd_DialogMode_f( const idCmdArgs &args ) { + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: dialogmode <1|0>\n"); + return; + } + + bool on = atoi(args.Argv( 1 )) != 0; + idPlayer *player = gameLocal.GetLocalPlayer(); + if (player && player->IsType(hhPlayer::Type)) { + if (on) { + static_cast(player)->DialogStart(0, 1, 1); + } + else { + static_cast(player)->DialogStop(); + } + } +} + +void Cmd_EntitySize_f( const idCmdArgs &args ) { + //TEMP: For memory optimization + gameLocal.Printf("Sizeof(idDict): %d\n", (int)sizeof(idDict)); + gameLocal.Printf("Sizeof(idList): %d\n", 16); + gameLocal.Printf("Sizeof(idMat3): %d\n", (int)sizeof(idMat3)); + gameLocal.Printf("Sizeof(idVec3): %d\n", (int)sizeof(idVec3)); + + gameLocal.Printf("Sizeof(renderEntity_t): %d\n", (int)sizeof(renderEntity_t)); + gameLocal.Printf("Sizeof(refSound_t): %d\n", (int)sizeof(refSound_t)); + gameLocal.Printf("Sizeof(idAnimator): %d\n", (int)sizeof(idAnimator)); + + gameLocal.Printf("Sizeof(idClass): %d\n", (int)sizeof(idClass)); + gameLocal.Printf("sizeof(idEntity): %d\n", (int)sizeof(idEntity)); + gameLocal.Printf("Sizeof(idAI): %d\n", (int)sizeof(idAI)); + gameLocal.Printf("Sizeof(hhPlayer): %d\n", (int)sizeof(hhPlayer)); + + gameLocal.Printf("Number entities: %d\n", gameLocal.num_entities); + gameLocal.Printf("sizeof(idEntity): %d\n", (int)sizeof(idEntity)); + gameLocal.Printf("Total mem cost: %d\n", gameLocal.num_entities * (int)sizeof(idEntity)); +} + +void Cmd_Assert_f( const idCmdArgs &args ) { + assert(0); +} + +void Cmd_Dormant_f( const idCmdArgs &args ) { + const char *classname; + + if ( !gameLocal.CheatsOk() ) { + return; + } + + if ( args.Argc() != 2 ) { + gameLocal.Printf("Usage: dormant \n"); + return; + } + + classname = args.Argv( 1 ); + const idTypeInfo *searchType = idClass::GetClass(classname); + + if (searchType) { + // make all entities of this class: neverdormant + int count = 0; + for (int i = 0; i < gameLocal.num_entities; i++) { + idEntity *ent = gameLocal.entities[i]; + if (ent && ent->IsType(*searchType)) { + ent->fl.neverDormant = false; + count++; + } + } + gameLocal.Printf("%d %s entities unmarked neverdormant\n", count, classname); + } + else { + gameLocal.Printf("class not found, make sure capitalization is correct\n"); + } +} + +void Cmd_UnDormant_f( const idCmdArgs &args ) { + const char *classname; + + if ( !gameLocal.CheatsOk() ) { + return; + } + + if ( args.Argc() != 2 ) { + gameLocal.Printf("Usage: undormant \n"); + return; + } + + classname = args.Argv( 1 ); + const idTypeInfo *searchType = idClass::GetClass(classname); + + if (searchType) { + // make all entities of this class: neverdormant + int count = 0; + for (int i = 0; i < gameLocal.num_entities; i++) { + idEntity *ent = gameLocal.entities[i]; + // if (ent && !idStr::Icmp(ent->GetClassname(), classname)) { + if (ent && ent->IsType(*searchType)) { + ent->fl.neverDormant = true; + count++; + } + } + + gameLocal.Printf("%d %s entities marked neverdormant\n", count, classname); + } + else { + gameLocal.Printf("class not found, make sure capitalization is correct\n"); + } +} + +void Cmd_TestHealthPulse_f( const idCmdArgs &args ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if (player && player->hud) { + player->healthPulse = true; + } +} +void Cmd_TestSpiritPulse_f( const idCmdArgs &args ) { + idPlayer *player = gameLocal.GetLocalPlayer(); + if (player && player->hud) { + player->spiritPulse = true; + } +} + +void Cmd_SpawnABunch_f( const idCmdArgs &args ) { + const char *key, *value; + float yaw; + idVec3 org; + idPlayer *player; + idDict dict; + int i; + const char *classname; + + player = gameLocal.GetLocalPlayer(); + if ( !player || !gameLocal.CheatsOk( false ) ) { + return; + } + + if ( args.Argc() < 2 || !args.Argc() & 1 ) { // must always have an even number of arguments + gameLocal.Printf( "usage: spawn classname []\n" ); + return; + } + + int numberToSpawn = atoi(args.Argv(2)); + + classname = args.Argv( 1 ); + dict.Set( "classname", classname ); + + for (int ix=0; ixviewAngles.yaw + gameLocal.random.CRandomFloat()*10; + dict.Set( "angle", va( "%f", yaw + 180 ) ); + org = player->GetPhysics()->GetOrigin() + idAngles( 0, yaw, 0 ).ToForward() * 80 + idVec3( 0, 0, 1 )*(10+gameLocal.random.RandomFloat()*50); + dict.Set( "origin", org.ToString() ); + + for( i = 3; i < args.Argc() - 1; i += 2 ) { + key = args.Argv( i ); + value = args.Argv( i + 1 ); + dict.Set( key, value ); + } + + //HACK + if (!idStr::Icmpn(classname, "movable_", 8)) { + idVec3 vel = idAngles(0,yaw,0).ToForward() * gameLocal.random.RandomFloat()*150; + dict.SetVector("init_velocity", vel); + } + + gameLocal.SpawnEntityDef( dict ); + } +} + +void Cmd_ListDictionary_f( const idCmdArgs &args ) { + const idDict *dict=NULL; + idEntity *ent=NULL; + const char *classname=NULL; + idStr key; + const idKeyValue *kv=NULL; + + if ( !gameLocal.CheatsOk() ) { + return; + } + + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: Dict [key]\n"); + return; + } + + // get classname + classname = args.Argv( 1 ); + dict = gameLocal.FindEntityDefDict( classname ); + if (!dict) { + gameLocal.Printf("Unknown entity definition: %s\n", classname); + return; + } + + // handle specific key + if (args.Argc()==3) { + key = args.Argv( 2 ); + + kv = dict->FindKey(key); + if (kv) { + gameLocal.Printf(" %30s %s\n", kv->GetKey().c_str(), kv->GetValue().c_str()); + } + } + else { + int num = dict->GetNumKeyVals(); + for (int ix=0; ixGetKeyVal(ix); + if (kv) { + gameLocal.Printf(" %30s %s\n", kv->GetKey().c_str(), kv->GetValue().c_str()); + } + } + } +} + +// id has a version of this as well, but ours is better +// Trigger all entities matching given name or class +void Cmd_HHTrigger_f( const idCmdArgs &args ) { + idVec3 origin; + idAngles angles; + idPlayer *player; + const char *classname; + + player = gameLocal.GetLocalPlayer(); + if ( !player || !gameLocal.CheatsOk() ) { + return; + } + + if ( args.Argc() != 2 ) { + gameLocal.Printf( "usage: trigger \n" ); + return; + } + + // get name/class of entity + classname = args.Argv( 1 ); + + // find entity by name or classname + int count=0; + for (int i = 0; i < gameLocal.num_entities; i++) { + idEntity *ent = gameLocal.entities[i]; + if ( ent && (!idStr::Icmp(ent->name, classname) || !idStr::Icmp(ent->GetClassname(), classname)) ) { + ent->Signal( SIG_TRIGGER ); + ent->ProcessEvent( &EV_Activate, player ); + ent->TriggerGuis(); + count++; + } + } + + // failed by name. try by entity number + if ( count == 0 ) { + int entityNumber = atoi(args.Argv( 1 )); + if ( entityNumber < gameLocal.num_entities ) { + idEntity *ent = gameLocal.entities[entityNumber]; + if ( ent ) { + ent->Signal( SIG_TRIGGER ); + ent->ProcessEvent( &EV_Activate, player ); + ent->TriggerGuis(); + } + } + } + + gameLocal.Printf( "Triggered %d entities\n", count ); +} + +// CJRPERSISTENTMERGE: id has a version of this as well, but ours is better +// Remove all of a specified class from the level +void Cmd_HHRemove_f( const idCmdArgs &args ) { + idPlayer *player; + const char *classname; + + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: remove \n"); + return; + } + + player = gameLocal.GetLocalPlayer(); + if ( !player || !gameLocal.CheatsOk() ) { + return; + } + + // get name/class of entity + classname = args.Argv( 1 ); + + // find entity by name or classname + int count = 0; + for (int i = 0; i < gameLocal.num_entities; i++) { + idEntity *ent = gameLocal.entities[i]; + if ( ent && (!idStr::Icmp(ent->name, classname) || !idStr::Icmp(ent->GetClassname(), classname)) ) { + ent->PostEventMS( &EV_Remove, 0 ); + count++; + } + } + + gameLocal.Printf("%d %s entities removed\n", count, classname); +} + +// Hide all of a specified class or a specific name +void Cmd_Hide_f( const idCmdArgs &args ) { + idPlayer *player; + idEntity *ent = NULL; + const char *classname; + + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: hide \n"); + return; + } + + player = gameLocal.GetLocalPlayer(); + if ( !player || !gameLocal.CheatsOk() ) { + return; + } + + // get name/class of entity + classname = args.Argv( 1 ); + + // find entity by name or classname + int count = 0; + for (int i = 0; i < gameLocal.num_entities; i++) { + idEntity *ent = gameLocal.entities[i]; + if ( ent && (!idStr::Icmp(ent->name, classname) || !idStr::Icmp(ent->GetClassname(), classname)) ) { + ent->Hide(); + count++; + } + } + + gameLocal.Printf("%d %s entities hidden\n", count, classname); +} + +// Show all of a specified class or a specific name +void Cmd_Show_f( const idCmdArgs &args ) { + idPlayer *player; + idEntity *ent = NULL; + const char *classname; + + if ( args.Argc() < 2 ) { + gameLocal.Printf("Usage: show \n"); + return; + } + + player = gameLocal.GetLocalPlayer(); + if ( !player || !gameLocal.CheatsOk() ) { + return; + } + + // get name/class of entity + classname = args.Argv( 1 ); + + // find entity by name or classname + int count = 0; + for (int i = 0; i < gameLocal.num_entities; i++) { + idEntity *ent = gameLocal.entities[i]; + if ( ent && (!idStr::Icmp(ent->name, classname) || !idStr::Icmp(ent->GetClassname(), classname)) ) { + ent->Show(); + count++; + } + } + + gameLocal.Printf("%d %s entities shown\n", count, classname); +} + +/* +============ +Cmd_SpawnDebrisMass_f +============ +*/ +void Cmd_SpawnDebrisMass_f(const idCmdArgs &args) { + const char *temp; + + + if ( !gameLocal.CheatsOk() ) { + return; + } + + if ( args.Argc() < 2 ) { + gameLocal.Printf( "Usage: SpawnDebrisMass entityDef\n" ); + return; + } + + temp = args.Argv( 1 ); + + hhUtils::SpawnDebrisMass( temp, vec3_origin ); + + +} + +void Cmd_SetPlayerGravity_f( const idCmdArgs &args ) { + idVec3 gravDirection; + idStr temp; + hhPlayer *player; + int argIndex = 0; + + if ( args.Argc() < 3 ) { + gameLocal.Printf("Usage: SetPlayerGravity \n"); + return; + } + + player = static_cast( gameLocal.GetLocalPlayer() ); + if ( !player || !gameLocal.CheatsOk() ) { + return; + } + + for( int ix = 0; ix < 3; ++ix ) { + temp = args.Argv( ++argIndex ); + if( temp.Icmp("-") == 0 ) { + temp = args.Argv( ++argIndex ); + gravDirection[ix] = -1 * atof( temp.c_str() ); + } else { + gravDirection[ix] = atof( temp.c_str() ); + } + } + + if( player->GetPhysics() ) { + player->OrientToGravity( true ); + player->GetPhysics()->SetGravity( gravDirection ); + } +} + + +//========================================================================= +// Cmd_ClosePortal_f +// +// Closes a portal if it intersects my current bounds (for testing) +//========================================================================= +void Cmd_ClosePortal_f(const idCmdArgs &args) { + if (!gameLocal.CheatsOk()) { + return; + } + idPlayer *player = gameLocal.GetLocalPlayer(); + qhandle_t areaPortal = gameRenderWorld->FindPortal( player->GetPhysics()->GetAbsBounds() ); + if ( areaPortal ) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_ALL ); + gameLocal.Printf("Portal closed\n"); + } +} + +//========================================================================= +// Cmd_OpenPortal_f +// +// Opens a portal if it intersects my current bounds (for testing) +//========================================================================= +void Cmd_OpenPortal_f(const idCmdArgs &args) { + if (!gameLocal.CheatsOk()) { + return; + } + idPlayer *player = gameLocal.GetLocalPlayer(); + qhandle_t areaPortal = gameRenderWorld->FindPortal( player->GetPhysics()->GetAbsBounds() ); + if ( areaPortal ) { + gameLocal.SetPortalState( areaPortal, PS_BLOCK_NONE ); + gameLocal.Printf("Portal opened\n"); + } +} + +//========================================================================= +// +// Cmd_CallScriptFunc_f +// +//========================================================================= +void Cmd_CallScriptFunc_f( const idCmdArgs &args ) { + if( !gameLocal.CheatsOk() ) { + return; + } + + if( args.Argc() <= 1 ) { + gameLocal.Printf( "Usage: call