m_medic.cpp 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628
  1. // Copyright (c) ZeniMax Media Inc.
  2. // Licensed under the GNU General Public License 2.0.
  3. /*
  4. ==============================================================================
  5. MEDIC
  6. ==============================================================================
  7. */
  8. #include "g_local.h"
  9. #include "m_medic.h"
  10. #include "m_flash.h"
  11. constexpr float MEDIC_MIN_DISTANCE = 32;
  12. constexpr float MEDIC_MAX_HEAL_DISTANCE = 400;
  13. constexpr gtime_t MEDIC_TRY_TIME = 10_sec;
  14. // FIXME -
  15. //
  16. // owner moved to monsterinfo.healer instead
  17. //
  18. // For some reason, the healed monsters are rarely ending up in the floor
  19. //
  20. // 5/15/1998 I think I fixed these, keep an eye on them
  21. void M_SetEffects(edict_t *ent);
  22. bool FindTarget(edict_t *self);
  23. void FoundTarget(edict_t *self);
  24. void ED_CallSpawn(edict_t *ent);
  25. static cached_soundindex sound_idle1;
  26. static cached_soundindex sound_pain1;
  27. static cached_soundindex sound_pain2;
  28. static cached_soundindex sound_die;
  29. static cached_soundindex sound_sight;
  30. static cached_soundindex sound_search;
  31. static cached_soundindex sound_hook_launch;
  32. static cached_soundindex sound_hook_hit;
  33. static cached_soundindex sound_hook_heal;
  34. static cached_soundindex sound_hook_retract;
  35. // PMM - commander sounds
  36. static cached_soundindex commander_sound_idle1;
  37. static cached_soundindex commander_sound_pain1;
  38. static cached_soundindex commander_sound_pain2;
  39. static cached_soundindex commander_sound_die;
  40. static cached_soundindex commander_sound_sight;
  41. static cached_soundindex commander_sound_search;
  42. static cached_soundindex commander_sound_hook_launch;
  43. static cached_soundindex commander_sound_hook_hit;
  44. static cached_soundindex commander_sound_hook_heal;
  45. static cached_soundindex commander_sound_hook_retract;
  46. static cached_soundindex commander_sound_spawn;
  47. constexpr const char *default_reinforcements = "monster_soldier_light 1;monster_soldier 2;monster_soldier_ss 2;monster_infantry 3;monster_gunner 4;monster_medic 5;monster_gladiator 6";
  48. constexpr int32_t default_monster_slots_base = 3;
  49. static const float inverse_log_slots = pow(2, MAX_REINFORCEMENTS);
  50. constexpr std::array<vec3_t, MAX_REINFORCEMENTS> reinforcement_position = {
  51. vec3_t { 80, 0, 0 },
  52. vec3_t { 40, 60, 0 },
  53. vec3_t { 40, -60, 0 },
  54. vec3_t { 0, 80, 0 },
  55. vec3_t { 0, -80, 0 }
  56. };
  57. // filter out the reinforcement indices we can pick given the space we have left
  58. static void M_PickValidReinforcements(edict_t *self, int32_t space, std::vector<uint8_t> &output)
  59. {
  60. output.clear();
  61. for (uint8_t i = 0; i < self->monsterinfo.reinforcements.num_reinforcements; i++)
  62. if (self->monsterinfo.reinforcements.reinforcements[i].strength <= space)
  63. output.push_back(i);
  64. }
  65. // pick an array of reinforcements to use; note that this does not modify `self`
  66. std::array<uint8_t, MAX_REINFORCEMENTS> M_PickReinforcements(edict_t *self, int32_t &num_chosen, int32_t max_slots = 0)
  67. {
  68. static std::vector<uint8_t> available;
  69. std::array<uint8_t, MAX_REINFORCEMENTS> chosen;
  70. chosen.fill(255);
  71. // decide how many things we want to spawn;
  72. // this is on a logarithmic scale
  73. // so we don't spawn too much too often.
  74. int32_t num_slots = max(1, (int32_t) log2(frandom(inverse_log_slots)));
  75. // we only have this many slots left to use
  76. int32_t remaining = self->monsterinfo.monster_slots - self->monsterinfo.monster_used;
  77. for (num_chosen = 0; num_chosen < num_slots; num_chosen++)
  78. {
  79. // ran out of slots!
  80. if ((max_slots && num_chosen == max_slots) || !remaining)
  81. break;
  82. // get everything we could choose
  83. M_PickValidReinforcements(self, remaining, available);
  84. // can't pick any
  85. if (!available.size())
  86. break;
  87. // select monster, TODO fairly
  88. chosen[num_chosen] = random_element(available);
  89. remaining -= self->monsterinfo.reinforcements.reinforcements[chosen[num_chosen]].strength;
  90. }
  91. return chosen;
  92. }
  93. void M_SetupReinforcements(const char *reinforcements, reinforcement_list_t &list)
  94. {
  95. // count up the semicolons
  96. list.num_reinforcements = 0;
  97. if (!*reinforcements)
  98. return;
  99. list.num_reinforcements++;
  100. for (size_t i = 0; i < strlen(reinforcements); i++)
  101. if (reinforcements[i] == ';')
  102. list.num_reinforcements++;
  103. // allocate
  104. list.reinforcements = (reinforcement_t *) gi.TagMalloc(sizeof(reinforcement_t) * list.num_reinforcements, TAG_LEVEL);
  105. // parse
  106. const char *p = reinforcements;
  107. reinforcement_t *r = list.reinforcements;
  108. st = {};
  109. while (true)
  110. {
  111. const char *token = COM_ParseEx(&p, "; ");
  112. if (!*token || r == list.reinforcements + list.num_reinforcements)
  113. break;
  114. r->classname = G_CopyString(token, TAG_LEVEL);
  115. token = COM_ParseEx(&p, "; ");
  116. r->strength = atoi(token);
  117. edict_t *newEnt = G_Spawn();
  118. newEnt->classname = r->classname;
  119. newEnt->monsterinfo.aiflags |= AI_DO_NOT_COUNT;
  120. ED_CallSpawn(newEnt);
  121. r->mins = newEnt->mins;
  122. r->maxs = newEnt->maxs;
  123. G_FreeEdict(newEnt);
  124. r++;
  125. }
  126. }
  127. void cleanupHeal(edict_t *self, bool change_frame)
  128. {
  129. // clean up target, if we have one and it's legit
  130. if (self->enemy && self->enemy->inuse)
  131. cleanupHealTarget(self->enemy);
  132. if (self->oldenemy && self->oldenemy->inuse && self->oldenemy->health > 0)
  133. {
  134. self->enemy = self->oldenemy;
  135. HuntTarget(self, false);
  136. }
  137. else
  138. {
  139. self->enemy = self->goalentity = nullptr;
  140. self->oldenemy = nullptr;
  141. if (!FindTarget(self))
  142. {
  143. // no valid enemy, so stop acting
  144. self->monsterinfo.pausetime = HOLD_FOREVER;
  145. self->monsterinfo.stand(self);
  146. return;
  147. }
  148. }
  149. if (change_frame)
  150. self->monsterinfo.nextframe = FRAME_attack52;
  151. }
  152. void abortHeal(edict_t *self, bool change_frame, bool gib, bool mark)
  153. {
  154. int hurt;
  155. constexpr vec3_t pain_normal = { 0, 0, 1 };
  156. if (self->enemy && self->enemy->inuse)
  157. {
  158. cleanupHealTarget(self->enemy);
  159. // gib em!
  160. if (mark)
  161. {
  162. // if the first badMedic slot is filled by a medic, skip it and use the second one
  163. if ((self->enemy->monsterinfo.badMedic1) && (self->enemy->monsterinfo.badMedic1->inuse) && (!strncmp(self->enemy->monsterinfo.badMedic1->classname, "monster_medic", 13)))
  164. {
  165. self->enemy->monsterinfo.badMedic2 = self;
  166. }
  167. else
  168. {
  169. self->enemy->monsterinfo.badMedic1 = self;
  170. }
  171. }
  172. if (gib)
  173. {
  174. if (self->enemy->gib_health)
  175. hurt = -self->enemy->gib_health;
  176. else
  177. hurt = 500;
  178. T_Damage(self->enemy, self, self, vec3_origin, self->enemy->s.origin,
  179. pain_normal, hurt, 0, DAMAGE_NONE, MOD_UNKNOWN);
  180. }
  181. }
  182. // clean up self
  183. // clean up target
  184. cleanupHeal(self, change_frame);
  185. self->monsterinfo.aiflags &= ~AI_MEDIC;
  186. self->monsterinfo.medicTries = 0;
  187. }
  188. bool canReach(edict_t *self, edict_t *other)
  189. {
  190. vec3_t spot1;
  191. vec3_t spot2;
  192. trace_t trace;
  193. spot1 = self->s.origin;
  194. spot1[2] += self->viewheight;
  195. spot2 = other->s.origin;
  196. spot2[2] += other->viewheight;
  197. trace = gi.traceline(spot1, spot2, self, MASK_PROJECTILE | MASK_WATER);
  198. return trace.fraction == 1.0f || trace.ent == other;
  199. }
  200. edict_t *medic_FindDeadMonster(edict_t *self)
  201. {
  202. float radius;
  203. edict_t *ent = nullptr;
  204. edict_t *best = nullptr;
  205. if (self->monsterinfo.react_to_damage_time > level.time)
  206. return nullptr;
  207. if (self->monsterinfo.aiflags & AI_STAND_GROUND)
  208. radius = MEDIC_MAX_HEAL_DISTANCE;
  209. else
  210. radius = 1024;
  211. while ((ent = findradius(ent, self->s.origin, radius)) != nullptr)
  212. {
  213. if (ent == self)
  214. continue;
  215. if (!(ent->svflags & SVF_MONSTER))
  216. continue;
  217. if (ent->monsterinfo.aiflags & AI_GOOD_GUY)
  218. continue;
  219. // check to make sure we haven't bailed on this guy already
  220. if ((ent->monsterinfo.badMedic1 == self) || (ent->monsterinfo.badMedic2 == self))
  221. continue;
  222. if (ent->monsterinfo.healer)
  223. // FIXME - this is correcting a bug that is somewhere else
  224. // if the healer is a monster, and it's in medic mode .. continue .. otherwise
  225. // we will override the healer, if it passes all the other tests
  226. if ((ent->monsterinfo.healer->inuse) && (ent->monsterinfo.healer->health > 0) &&
  227. (ent->monsterinfo.healer->svflags & SVF_MONSTER) && (ent->monsterinfo.healer->monsterinfo.aiflags & AI_MEDIC))
  228. continue;
  229. if (ent->health > 0)
  230. continue;
  231. if ((ent->nextthink) && (ent->think != monster_dead_think))
  232. continue;
  233. if (!visible(self, ent))
  234. continue;
  235. if (!strncmp(ent->classname, "player", 6)) // stop it from trying to heal player_noise entities
  236. continue;
  237. // FIXME - there's got to be a better way ..
  238. // make sure we don't spawn people right on top of us
  239. if (realrange(self, ent) <= MEDIC_MIN_DISTANCE)
  240. continue;
  241. if (!best)
  242. {
  243. best = ent;
  244. continue;
  245. }
  246. if (ent->max_health <= best->max_health)
  247. continue;
  248. best = ent;
  249. }
  250. if (best)
  251. self->timestamp = level.time + MEDIC_TRY_TIME;
  252. return best;
  253. }
  254. MONSTERINFO_IDLE(medic_idle) (edict_t *self) -> void
  255. {
  256. edict_t *ent;
  257. // PMM - commander sounds
  258. if (self->mass == 400)
  259. gi.sound(self, CHAN_VOICE, sound_idle1, 1, ATTN_IDLE, 0);
  260. else
  261. gi.sound(self, CHAN_VOICE, commander_sound_idle1, 1, ATTN_IDLE, 0);
  262. if (!self->oldenemy)
  263. {
  264. ent = medic_FindDeadMonster(self);
  265. if (ent)
  266. {
  267. self->oldenemy = self->enemy;
  268. self->enemy = ent;
  269. self->enemy->monsterinfo.healer = self;
  270. self->monsterinfo.aiflags |= AI_MEDIC;
  271. FoundTarget(self);
  272. }
  273. }
  274. }
  275. MONSTERINFO_SEARCH(medic_search) (edict_t *self) -> void
  276. {
  277. edict_t *ent;
  278. // PMM - commander sounds
  279. if (self->mass == 400)
  280. gi.sound(self, CHAN_VOICE, sound_search, 1, ATTN_IDLE, 0);
  281. else
  282. gi.sound(self, CHAN_VOICE, commander_sound_search, 1, ATTN_IDLE, 0);
  283. if (!self->oldenemy)
  284. {
  285. ent = medic_FindDeadMonster(self);
  286. if (ent)
  287. {
  288. self->oldenemy = self->enemy;
  289. self->enemy = ent;
  290. self->enemy->monsterinfo.healer = self;
  291. self->monsterinfo.aiflags |= AI_MEDIC;
  292. FoundTarget(self);
  293. }
  294. }
  295. }
  296. MONSTERINFO_SIGHT(medic_sight) (edict_t *self, edict_t *other) -> void
  297. {
  298. // PMM - commander sounds
  299. if (self->mass == 400)
  300. gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0);
  301. else
  302. gi.sound(self, CHAN_VOICE, commander_sound_sight, 1, ATTN_NORM, 0);
  303. }
  304. mframe_t medic_frames_stand[] = {
  305. { ai_stand, 0, medic_idle },
  306. { ai_stand },
  307. { ai_stand },
  308. { ai_stand },
  309. { ai_stand },
  310. { ai_stand },
  311. { ai_stand },
  312. { ai_stand },
  313. { ai_stand },
  314. { ai_stand },
  315. { ai_stand },
  316. { ai_stand },
  317. { ai_stand },
  318. { ai_stand },
  319. { ai_stand },
  320. { ai_stand },
  321. { ai_stand },
  322. { ai_stand },
  323. { ai_stand },
  324. { ai_stand },
  325. { ai_stand },
  326. { ai_stand },
  327. { ai_stand },
  328. { ai_stand },
  329. { ai_stand },
  330. { ai_stand },
  331. { ai_stand },
  332. { ai_stand },
  333. { ai_stand },
  334. { ai_stand },
  335. { ai_stand },
  336. { ai_stand },
  337. { ai_stand },
  338. { ai_stand },
  339. { ai_stand },
  340. { ai_stand },
  341. { ai_stand },
  342. { ai_stand },
  343. { ai_stand },
  344. { ai_stand },
  345. { ai_stand },
  346. { ai_stand },
  347. { ai_stand },
  348. { ai_stand },
  349. { ai_stand },
  350. { ai_stand },
  351. { ai_stand },
  352. { ai_stand },
  353. { ai_stand },
  354. { ai_stand },
  355. { ai_stand },
  356. { ai_stand },
  357. { ai_stand },
  358. { ai_stand },
  359. { ai_stand },
  360. { ai_stand },
  361. { ai_stand },
  362. { ai_stand },
  363. { ai_stand },
  364. { ai_stand },
  365. { ai_stand },
  366. { ai_stand },
  367. { ai_stand },
  368. { ai_stand },
  369. { ai_stand },
  370. { ai_stand },
  371. { ai_stand },
  372. { ai_stand },
  373. { ai_stand },
  374. { ai_stand },
  375. { ai_stand },
  376. { ai_stand },
  377. { ai_stand },
  378. { ai_stand },
  379. { ai_stand },
  380. { ai_stand },
  381. { ai_stand },
  382. { ai_stand },
  383. { ai_stand },
  384. { ai_stand },
  385. { ai_stand },
  386. { ai_stand },
  387. { ai_stand },
  388. { ai_stand },
  389. { ai_stand },
  390. { ai_stand },
  391. { ai_stand },
  392. { ai_stand },
  393. { ai_stand },
  394. { ai_stand },
  395. };
  396. MMOVE_T(medic_move_stand) = { FRAME_wait1, FRAME_wait90, medic_frames_stand, nullptr };
  397. MONSTERINFO_STAND(medic_stand) (edict_t *self) -> void
  398. {
  399. M_SetAnimation(self, &medic_move_stand);
  400. }
  401. mframe_t medic_frames_walk[] = {
  402. { ai_walk, 6.2f },
  403. { ai_walk, 18.1f, monster_footstep },
  404. { ai_walk, 1 },
  405. { ai_walk, 9 },
  406. { ai_walk, 10 },
  407. { ai_walk, 9 },
  408. { ai_walk, 11 },
  409. { ai_walk, 11.6f, monster_footstep },
  410. { ai_walk, 2 },
  411. { ai_walk, 9.9f },
  412. { ai_walk, 14 },
  413. { ai_walk, 9.3f }
  414. };
  415. MMOVE_T(medic_move_walk) = { FRAME_walk1, FRAME_walk12, medic_frames_walk, nullptr };
  416. MONSTERINFO_WALK(medic_walk) (edict_t *self) -> void
  417. {
  418. M_SetAnimation(self, &medic_move_walk);
  419. }
  420. mframe_t medic_frames_run[] = {
  421. { ai_run, 18 },
  422. { ai_run, 22.5f, monster_footstep },
  423. { ai_run, 25.4f, monster_done_dodge },
  424. { ai_run, 23.4f, monster_footstep },
  425. { ai_run, 24 },
  426. { ai_run, 35.6f }
  427. };
  428. MMOVE_T(medic_move_run) = { FRAME_run1, FRAME_run6, medic_frames_run, nullptr };
  429. MONSTERINFO_RUN(medic_run) (edict_t *self) -> void
  430. {
  431. monster_done_dodge(self);
  432. if (!(self->monsterinfo.aiflags & AI_MEDIC))
  433. {
  434. edict_t *ent;
  435. ent = medic_FindDeadMonster(self);
  436. if (ent)
  437. {
  438. self->oldenemy = self->enemy;
  439. self->enemy = ent;
  440. self->enemy->monsterinfo.healer = self;
  441. self->monsterinfo.aiflags |= AI_MEDIC;
  442. FoundTarget(self);
  443. return;
  444. }
  445. }
  446. if (self->monsterinfo.aiflags & AI_STAND_GROUND)
  447. M_SetAnimation(self, &medic_move_stand);
  448. else
  449. M_SetAnimation(self, &medic_move_run);
  450. }
  451. mframe_t medic_frames_pain1[] = {
  452. { ai_move },
  453. { ai_move },
  454. { ai_move },
  455. { ai_move },
  456. { ai_move }
  457. };
  458. MMOVE_T(medic_move_pain1) = { FRAME_paina2, FRAME_paina6, medic_frames_pain1, medic_run };
  459. mframe_t medic_frames_pain2[] = {
  460. { ai_move },
  461. { ai_move },
  462. { ai_move },
  463. { ai_move, 0, monster_footstep },
  464. { ai_move },
  465. { ai_move },
  466. { ai_move },
  467. { ai_move },
  468. { ai_move },
  469. { ai_move },
  470. { ai_move },
  471. { ai_move, 0, monster_footstep }
  472. };
  473. MMOVE_T(medic_move_pain2) = { FRAME_painb2, FRAME_painb13, medic_frames_pain2, medic_run };
  474. PAIN(medic_pain) (edict_t *self, edict_t *other, float kick, int damage, const mod_t &mod) -> void
  475. {
  476. monster_done_dodge(self);
  477. if (level.time < self->pain_debounce_time)
  478. return;
  479. self->pain_debounce_time = level.time + 3_sec;
  480. float r = frandom();
  481. if (self->mass > 400)
  482. {
  483. if (damage < 35)
  484. {
  485. gi.sound(self, CHAN_VOICE, commander_sound_pain1, 1, ATTN_NORM, 0);
  486. if (mod.id != MOD_CHAINFIST)
  487. return;
  488. }
  489. gi.sound(self, CHAN_VOICE, commander_sound_pain2, 1, ATTN_NORM, 0);
  490. }
  491. else if (r < 0.5f)
  492. gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0);
  493. else
  494. gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0);
  495. if (!M_ShouldReactToPain(self, mod))
  496. return; // no pain anims in nightmare
  497. // if we're healing someone, we ignore pain
  498. if (mod.id != MOD_CHAINFIST && (self->monsterinfo.aiflags & AI_MEDIC))
  499. return;
  500. if (self->mass > 400)
  501. {
  502. self->monsterinfo.aiflags &= ~AI_MANUAL_STEERING;
  503. self->monsterinfo.aiflags &= ~AI_HOLD_FRAME;
  504. if (r < (min(((float) damage * 0.005f), 0.5f))) // no more than 50% chance of big pain
  505. M_SetAnimation(self, &medic_move_pain2);
  506. else
  507. M_SetAnimation(self, &medic_move_pain1);
  508. }
  509. else if (r < 0.5f)
  510. M_SetAnimation(self, &medic_move_pain1);
  511. else
  512. M_SetAnimation(self, &medic_move_pain2);
  513. // PMM - clear duck flag
  514. if (self->monsterinfo.aiflags & AI_DUCKED)
  515. monster_duck_up(self);
  516. abortHeal(self, false, false, false);
  517. }
  518. MONSTERINFO_SETSKIN(medic_setskin) (edict_t *self) -> void
  519. {
  520. if ((self->health < (self->max_health / 2)))
  521. self->s.skinnum |= 1;
  522. else
  523. self->s.skinnum &= ~1;
  524. }
  525. void medic_fire_blaster(edict_t *self)
  526. {
  527. vec3_t start;
  528. vec3_t forward, right;
  529. vec3_t end;
  530. vec3_t dir;
  531. effects_t effect;
  532. int damage = 2;
  533. monster_muzzleflash_id_t mz;
  534. // paranoia checking
  535. if (!(self->enemy && self->enemy->inuse))
  536. return;
  537. if ((self->s.frame == FRAME_attack9) || (self->s.frame == FRAME_attack12))
  538. {
  539. effect = EF_BLASTER;
  540. damage = 6;
  541. mz = (self->mass > 400) ? MZ2_MEDIC_BLASTER_2 : MZ2_MEDIC_BLASTER_1;
  542. }
  543. else
  544. {
  545. effect = (self->s.frame % 4) ? EF_NONE : EF_HYPERBLASTER;
  546. mz = static_cast<monster_muzzleflash_id_t>(((self->mass > 400) ? MZ2_MEDIC_HYPERBLASTER2_1 : MZ2_MEDIC_HYPERBLASTER1_1) + (self->s.frame - FRAME_attack19));
  547. }
  548. AngleVectors(self->s.angles, forward, right, nullptr);
  549. const vec3_t &offset = monster_flash_offset[mz];
  550. start = M_ProjectFlashSource(self, offset, forward, right);
  551. end = self->enemy->s.origin;
  552. end[2] += self->enemy->viewheight;
  553. dir = end - start;
  554. dir.normalize();
  555. if ( !strcmp( self->enemy->classname, "tesla_mine" ) )
  556. damage = 3;
  557. // medic commander shoots blaster2
  558. if (self->mass > 400)
  559. monster_fire_blaster2(self, start, dir, damage, 1000, mz, effect);
  560. else
  561. monster_fire_blaster(self, start, dir, damage, 1000, mz, effect);
  562. }
  563. void medic_dead(edict_t *self)
  564. {
  565. self->mins = { -16, -16, -24 };
  566. self->maxs = { 16, 16, -8 };
  567. monster_dead(self);
  568. }
  569. static void medic_shrink(edict_t *self)
  570. {
  571. self->maxs[2] = -2;
  572. self->svflags |= SVF_DEADMONSTER;
  573. gi.linkentity(self);
  574. }
  575. mframe_t medic_frames_death[] = {
  576. { ai_move },
  577. { ai_move },
  578. { ai_move },
  579. { ai_move, -18.f, monster_footstep },
  580. { ai_move, -10.f, medic_shrink },
  581. { ai_move, -6.f },
  582. { ai_move },
  583. { ai_move },
  584. { ai_move },
  585. { ai_move },
  586. { ai_move },
  587. { ai_move },
  588. { ai_move },
  589. { ai_move },
  590. { ai_move },
  591. { ai_move },
  592. { ai_move, 0, monster_footstep },
  593. { ai_move },
  594. { ai_move },
  595. { ai_move },
  596. { ai_move },
  597. { ai_move },
  598. { ai_move },
  599. { ai_move },
  600. { ai_move },
  601. { ai_move },
  602. { ai_move },
  603. { ai_move },
  604. { ai_move }
  605. };
  606. MMOVE_T(medic_move_death) = { FRAME_death2, FRAME_death30, medic_frames_death, medic_dead };
  607. DIE(medic_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
  608. {
  609. // if we had a pending patient, he was already freed up in Killed
  610. // check for gib
  611. if (M_CheckGib(self, mod))
  612. {
  613. gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0);
  614. self->s.skinnum /= 2;
  615. ThrowGibs(self, damage, {
  616. { 2, "models/objects/gibs/bone/tris.md2" },
  617. { "models/objects/gibs/sm_meat/tris.md2" },
  618. { "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC },
  619. { "models/monsters/medic/gibs/chest.md2", GIB_SKINNED },
  620. { 2, "models/monsters/medic/gibs/leg.md2", GIB_SKINNED | GIB_UPRIGHT },
  621. { "models/monsters/medic/gibs/hook.md2", GIB_SKINNED | GIB_UPRIGHT },
  622. { "models/monsters/medic/gibs/gun.md2", GIB_SKINNED | GIB_UPRIGHT },
  623. { "models/monsters/medic/gibs/head.md2", GIB_SKINNED | GIB_HEAD }
  624. });
  625. self->deadflag = true;
  626. return;
  627. }
  628. if (self->deadflag)
  629. return;
  630. // regular death
  631. // PMM
  632. if (self->mass == 400)
  633. gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0);
  634. else
  635. gi.sound(self, CHAN_VOICE, commander_sound_die, 1, ATTN_NORM, 0);
  636. //
  637. self->deadflag = true;
  638. self->takedamage = true;
  639. M_SetAnimation(self, &medic_move_death);
  640. }
  641. mframe_t medic_frames_duck[] = {
  642. { ai_move, -1 },
  643. { ai_move, -1, monster_duck_down },
  644. { ai_move, -1, monster_duck_hold },
  645. { ai_move, -1 },
  646. { ai_move, -1 },
  647. { ai_move, -1 }, // PMM - duck up used to be here
  648. { ai_move, -1 },
  649. { ai_move, -1 },
  650. { ai_move, -1 },
  651. { ai_move, -1 },
  652. { ai_move, -1 },
  653. { ai_move, -1 },
  654. { ai_move, -1, monster_duck_up }
  655. };
  656. MMOVE_T(medic_move_duck) = { FRAME_duck2, FRAME_duck14, medic_frames_duck, medic_run };
  657. // PMM -- moved dodge code to after attack code so I can reference attack frames
  658. mframe_t medic_frames_attackHyperBlaster[] = {
  659. { ai_charge },
  660. { ai_charge },
  661. { ai_charge },
  662. { ai_charge },
  663. { ai_charge, 0, medic_fire_blaster },
  664. { ai_charge, 0, medic_fire_blaster },
  665. { ai_charge, 0, medic_fire_blaster },
  666. { ai_charge, 0, medic_fire_blaster },
  667. { ai_charge, 0, medic_fire_blaster },
  668. { ai_charge, 0, medic_fire_blaster },
  669. { ai_charge, 0, medic_fire_blaster },
  670. { ai_charge, 0, medic_fire_blaster },
  671. { ai_charge, 0, medic_fire_blaster },
  672. { ai_charge, 0, medic_fire_blaster },
  673. { ai_charge, 0, medic_fire_blaster },
  674. { ai_charge, 0, medic_fire_blaster },
  675. { ai_charge },
  676. { ai_charge },
  677. // [Paril-KEX] end on 36 as intended
  678. { ai_charge, 2.f }, // 33
  679. { ai_charge, 3.f, monster_footstep },
  680. };
  681. MMOVE_T(medic_move_attackHyperBlaster) = { FRAME_attack15, FRAME_attack34, medic_frames_attackHyperBlaster, medic_run };
  682. static void medic_quick_attack(edict_t *self)
  683. {
  684. if (frandom() < 0.5f)
  685. {
  686. M_SetAnimation(self, &medic_move_attackHyperBlaster, false);
  687. self->monsterinfo.nextframe = FRAME_attack16;
  688. }
  689. }
  690. void medic_continue(edict_t *self)
  691. {
  692. if (visible(self, self->enemy))
  693. if (frandom() <= 0.95f)
  694. M_SetAnimation(self, &medic_move_attackHyperBlaster, false);
  695. }
  696. mframe_t medic_frames_attackBlaster[] = {
  697. { ai_charge, 5 },
  698. { ai_charge, 3 },
  699. { ai_charge, 2 },
  700. { ai_charge, 0, medic_quick_attack },
  701. { ai_charge, 0, monster_footstep },
  702. { ai_charge },
  703. { ai_charge, 0, medic_fire_blaster },
  704. { ai_charge },
  705. { ai_charge },
  706. { ai_charge, 0, medic_fire_blaster },
  707. { ai_charge },
  708. { ai_charge, 0, medic_continue } // Change to medic_continue... Else, go to frame 32
  709. };
  710. MMOVE_T(medic_move_attackBlaster) = { FRAME_attack3, FRAME_attack14, medic_frames_attackBlaster, medic_run };
  711. void medic_hook_launch(edict_t *self)
  712. {
  713. // PMM - commander sounds
  714. if (self->mass == 400)
  715. gi.sound(self, CHAN_WEAPON, sound_hook_launch, 1, ATTN_NORM, 0);
  716. else
  717. gi.sound(self, CHAN_WEAPON, commander_sound_hook_launch, 1, ATTN_NORM, 0);
  718. }
  719. constexpr vec3_t medic_cable_offsets[] = {
  720. { 45.0f, -9.2f, 15.5f },
  721. { 48.4f, -9.7f, 15.2f },
  722. { 47.8f, -9.8f, 15.8f },
  723. { 47.3f, -9.3f, 14.3f },
  724. { 45.4f, -10.1f, 13.1f },
  725. { 41.9f, -12.7f, 12.0f },
  726. { 37.8f, -15.8f, 11.2f },
  727. { 34.3f, -18.4f, 10.7f },
  728. { 32.7f, -19.7f, 10.4f },
  729. { 32.7f, -19.7f, 10.4f }
  730. };
  731. void medic_cable_attack(edict_t *self)
  732. {
  733. vec3_t offset, start, end, f, r;
  734. trace_t tr;
  735. vec3_t dir;
  736. float distance;
  737. if ((!self->enemy) || (!self->enemy->inuse) || (self->enemy->s.effects & EF_GIB))
  738. {
  739. abortHeal(self, false, false, false);
  740. return;
  741. }
  742. // we switched back to a player; let the animation finish
  743. if (self->enemy->client)
  744. return;
  745. // see if our enemy has changed to a client, or our target has more than 0 health,
  746. // abort it .. we got switched to someone else due to damage
  747. if (self->enemy->health > 0)
  748. {
  749. abortHeal(self, false, false, false);
  750. return;
  751. }
  752. AngleVectors(self->s.angles, f, r, nullptr);
  753. offset = medic_cable_offsets[self->s.frame - FRAME_attack42];
  754. start = M_ProjectFlashSource(self, offset, f, r);
  755. // check for max distance
  756. // not needed, done in checkattack
  757. // check for min distance
  758. dir = start - self->enemy->s.origin;
  759. distance = dir.length();
  760. if (distance < MEDIC_MIN_DISTANCE)
  761. {
  762. abortHeal(self, true, true, false);
  763. return;
  764. }
  765. tr = gi.traceline(start, self->enemy->s.origin, self, MASK_SOLID);
  766. if (tr.fraction != 1.0f && tr.ent != self->enemy)
  767. {
  768. if (tr.ent == world)
  769. {
  770. // give up on second try
  771. if (self->monsterinfo.medicTries > 1)
  772. {
  773. abortHeal(self, true, false, true);
  774. return;
  775. }
  776. self->monsterinfo.medicTries++;
  777. cleanupHeal(self, 1);
  778. return;
  779. }
  780. abortHeal(self, true, false, false);
  781. return;
  782. }
  783. if (self->s.frame == FRAME_attack43)
  784. {
  785. // PMM - commander sounds
  786. if (self->mass == 400)
  787. gi.sound(self->enemy, CHAN_AUTO, sound_hook_hit, 1, ATTN_NORM, 0);
  788. else
  789. gi.sound(self->enemy, CHAN_AUTO, commander_sound_hook_hit, 1, ATTN_NORM, 0);
  790. self->enemy->monsterinfo.aiflags |= AI_RESURRECTING;
  791. self->enemy->takedamage = false;
  792. M_SetEffects(self->enemy);
  793. }
  794. else if (self->s.frame == FRAME_attack50)
  795. {
  796. vec3_t maxs;
  797. self->enemy->spawnflags = SPAWNFLAG_NONE;
  798. self->enemy->monsterinfo.aiflags &= AI_STINKY | AI_SPAWNED_MASK;
  799. self->enemy->target = nullptr;
  800. self->enemy->targetname = nullptr;
  801. self->enemy->combattarget = nullptr;
  802. self->enemy->deathtarget = nullptr;
  803. self->enemy->healthtarget = nullptr;
  804. self->enemy->itemtarget = nullptr;
  805. self->enemy->monsterinfo.healer = self;
  806. maxs = self->enemy->maxs;
  807. maxs[2] += 48; // compensate for change when they die
  808. tr = gi.trace(self->enemy->s.origin, self->enemy->mins, maxs, self->enemy->s.origin, self->enemy, MASK_MONSTERSOLID);
  809. if (tr.startsolid || tr.allsolid)
  810. {
  811. abortHeal(self, true, true, false);
  812. return;
  813. }
  814. else if (tr.ent != world)
  815. {
  816. abortHeal(self, true, true, false);
  817. return;
  818. }
  819. else
  820. {
  821. self->enemy->monsterinfo.aiflags |= AI_DO_NOT_COUNT;
  822. // backup & restore health stuff, because of multipliers
  823. int32_t old_max_health = self->enemy->max_health;
  824. item_id_t old_power_armor_type = self->enemy->monsterinfo.initial_power_armor_type;
  825. int32_t old_power_armor_power = self->enemy->monsterinfo.max_power_armor_power;
  826. int32_t old_base_health = self->enemy->monsterinfo.base_health;
  827. int32_t old_health_scaling = self->enemy->monsterinfo.health_scaling;
  828. auto reinforcements = self->enemy->monsterinfo.reinforcements;
  829. int32_t monster_slots = self->enemy->monsterinfo.monster_slots;
  830. int32_t monster_used = self->enemy->monsterinfo.monster_used;
  831. int32_t old_gib_health = self->enemy->gib_health;
  832. st = {};
  833. st.keys_specified.emplace("reinforcements");
  834. st.reinforcements = "";
  835. ED_CallSpawn(self->enemy);
  836. self->enemy->monsterinfo.reinforcements = reinforcements;
  837. self->enemy->monsterinfo.monster_slots = monster_slots;
  838. self->enemy->monsterinfo.monster_used = monster_used;
  839. self->enemy->gib_health = old_gib_health / 2;
  840. self->enemy->health = self->enemy->max_health = old_max_health;
  841. self->enemy->monsterinfo.power_armor_power = self->enemy->monsterinfo.max_power_armor_power = old_power_armor_power;
  842. self->enemy->monsterinfo.power_armor_type = self->enemy->monsterinfo.initial_power_armor_type = old_power_armor_type;
  843. self->enemy->monsterinfo.base_health = old_base_health;
  844. self->enemy->monsterinfo.health_scaling = old_health_scaling;
  845. if (self->enemy->monsterinfo.setskin)
  846. self->enemy->monsterinfo.setskin(self->enemy);
  847. if (self->enemy->think)
  848. {
  849. self->enemy->nextthink = level.time;
  850. self->enemy->think(self->enemy);
  851. }
  852. self->enemy->monsterinfo.aiflags &= ~AI_RESURRECTING;
  853. self->enemy->monsterinfo.aiflags |= AI_IGNORE_SHOTS | AI_DO_NOT_COUNT;
  854. // turn off flies
  855. self->enemy->s.effects &= ~EF_FLIES;
  856. self->enemy->monsterinfo.healer = nullptr;
  857. if ((self->oldenemy) && (self->oldenemy->inuse) && (self->oldenemy->health > 0))
  858. {
  859. self->enemy->enemy = self->oldenemy;
  860. FoundTarget(self->enemy);
  861. }
  862. else
  863. {
  864. self->enemy->enemy = nullptr;
  865. if (!FindTarget(self->enemy))
  866. {
  867. // no valid enemy, so stop acting
  868. self->enemy->monsterinfo.pausetime = HOLD_FOREVER;
  869. self->enemy->monsterinfo.stand(self->enemy);
  870. }
  871. self->enemy = nullptr;
  872. self->oldenemy = nullptr;
  873. if (!FindTarget(self))
  874. {
  875. // no valid enemy, so stop acting
  876. self->monsterinfo.pausetime = HOLD_FOREVER;
  877. self->monsterinfo.stand(self);
  878. return;
  879. }
  880. }
  881. cleanupHeal(self, false);
  882. return;
  883. }
  884. }
  885. else
  886. {
  887. if (self->s.frame == FRAME_attack44)
  888. {
  889. // PMM - medic commander sounds
  890. if (self->mass == 400)
  891. gi.sound(self, CHAN_WEAPON, sound_hook_heal, 1, ATTN_NORM, 0);
  892. else
  893. gi.sound(self, CHAN_WEAPON, commander_sound_hook_heal, 1, ATTN_NORM, 0);
  894. }
  895. }
  896. // adjust start for beam origin being in middle of a segment
  897. start += (f * 8);
  898. // adjust end z for end spot since the monster is currently dead
  899. end = self->enemy->s.origin;
  900. end[2] = (self->enemy->absmin[2] + self->enemy->absmax[2]) / 2;
  901. gi.WriteByte(svc_temp_entity);
  902. gi.WriteByte(TE_MEDIC_CABLE_ATTACK);
  903. gi.WriteEntity(self);
  904. gi.WritePosition(start);
  905. gi.WritePosition(end);
  906. gi.multicast(self->s.origin, MULTICAST_PVS, false);
  907. }
  908. void medic_hook_retract(edict_t *self)
  909. {
  910. if (self->mass == 400)
  911. gi.sound(self, CHAN_WEAPON, sound_hook_retract, 1, ATTN_NORM, 0);
  912. else
  913. gi.sound(self, CHAN_WEAPON, sound_hook_retract, 1, ATTN_NORM, 0);
  914. self->monsterinfo.aiflags &= ~AI_MEDIC;
  915. if (self->oldenemy && self->oldenemy->inuse && self->oldenemy->health > 0)
  916. {
  917. self->enemy = self->oldenemy;
  918. HuntTarget(self, false);
  919. }
  920. else
  921. {
  922. self->enemy = self->goalentity = nullptr;
  923. self->oldenemy = nullptr;
  924. if (!FindTarget(self))
  925. {
  926. // no valid enemy, so stop acting
  927. self->monsterinfo.pausetime = HOLD_FOREVER;
  928. self->monsterinfo.stand(self);
  929. return;
  930. }
  931. }
  932. }
  933. mframe_t medic_frames_attackCable[] = {
  934. // ROGUE - negated 36-40 so he scoots back from his target a little
  935. // ROGUE - switched 33-36 to ai_charge
  936. // ROGUE - changed frame 52 to 60 to compensate for changes in 36-40
  937. // [Paril-KEX] started on 36 as they intended
  938. { ai_charge, -4.7f }, // 37
  939. { ai_charge, -5.f },
  940. { ai_charge, -6.f },
  941. { ai_charge, -4.f }, // 40
  942. { ai_charge, 0, monster_footstep },
  943. { ai_move, 0, medic_hook_launch }, // 42
  944. { ai_move, 0, medic_cable_attack }, // 43
  945. { ai_move, 0, medic_cable_attack },
  946. { ai_move, 0, medic_cable_attack },
  947. { ai_move, 0, medic_cable_attack },
  948. { ai_move, 0, medic_cable_attack },
  949. { ai_move, 0, medic_cable_attack },
  950. { ai_move, 0, medic_cable_attack },
  951. { ai_move, 0, medic_cable_attack },
  952. { ai_move, 0, medic_cable_attack }, // 51
  953. { ai_move, 0, medic_hook_retract }, // 52
  954. { ai_move, -1.5f },
  955. { ai_move, -1.2f, monster_footstep },
  956. { ai_move, -3.f }
  957. };
  958. MMOVE_T(medic_move_attackCable) = { FRAME_attack37, FRAME_attack55, medic_frames_attackCable, medic_run };
  959. void medic_start_spawn(edict_t *self)
  960. {
  961. gi.sound(self, CHAN_WEAPON, commander_sound_spawn, 1, ATTN_NORM, 0);
  962. self->monsterinfo.nextframe = FRAME_attack48;
  963. }
  964. void medic_determine_spawn(edict_t *self)
  965. {
  966. vec3_t f, r, offset, startpoint, spawnpoint;
  967. int count;
  968. int num_success = 0;
  969. AngleVectors(self->s.angles, f, r, nullptr);
  970. int num_summoned;
  971. self->monsterinfo.chosen_reinforcements = M_PickReinforcements(self, num_summoned);
  972. for (count = 0; count < num_summoned; count++)
  973. {
  974. offset = reinforcement_position[count];
  975. if (self->s.scale)
  976. offset *= self->s.scale;
  977. startpoint = M_ProjectFlashSource(self, offset, f, r);
  978. // a little off the ground
  979. startpoint[2] += 10 * (self->s.scale ? self->s.scale : 1.0f);
  980. auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]];
  981. if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32))
  982. {
  983. if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, -1))
  984. {
  985. num_success++;
  986. // we found a spot, we're done here
  987. count = num_summoned;
  988. }
  989. }
  990. }
  991. // see if we have any success by spinning around
  992. if (num_success == 0)
  993. {
  994. for (count = 0; count < num_summoned; count++)
  995. {
  996. offset = reinforcement_position[count];
  997. if (self->s.scale)
  998. offset *= self->s.scale;
  999. // check behind
  1000. offset[0] *= -1.0f;
  1001. offset[1] *= -1.0f;
  1002. startpoint = M_ProjectFlashSource(self, offset, f, r);
  1003. // a little off the ground
  1004. startpoint[2] += 10;
  1005. auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]];
  1006. if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32))
  1007. {
  1008. if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, -1))
  1009. {
  1010. num_success++;
  1011. // we found a spot, we're done here
  1012. count = num_summoned;
  1013. }
  1014. }
  1015. }
  1016. if (num_success)
  1017. {
  1018. self->monsterinfo.aiflags |= AI_MANUAL_STEERING;
  1019. self->ideal_yaw = anglemod(self->s.angles[YAW]) + 180;
  1020. if (self->ideal_yaw > 360.0f)
  1021. self->ideal_yaw -= 360.0f;
  1022. }
  1023. }
  1024. if (num_success == 0)
  1025. self->monsterinfo.nextframe = FRAME_attack53;
  1026. }
  1027. void medic_spawngrows(edict_t *self)
  1028. {
  1029. vec3_t f, r, offset, startpoint, spawnpoint;
  1030. int count;
  1031. int num_summoned; // should be 1, 3, or 5
  1032. int num_success = 0;
  1033. float current_yaw;
  1034. // if we've been directed to turn around
  1035. if (self->monsterinfo.aiflags & AI_MANUAL_STEERING)
  1036. {
  1037. current_yaw = anglemod(self->s.angles[YAW]);
  1038. if (fabsf(current_yaw - self->ideal_yaw) > 0.1f)
  1039. {
  1040. self->monsterinfo.aiflags |= AI_HOLD_FRAME;
  1041. return;
  1042. }
  1043. // done turning around
  1044. self->monsterinfo.aiflags &= ~AI_HOLD_FRAME;
  1045. self->monsterinfo.aiflags &= ~AI_MANUAL_STEERING;
  1046. }
  1047. AngleVectors(self->s.angles, f, r, nullptr);
  1048. num_summoned = 0;
  1049. for (int32_t i = 0; i < MAX_REINFORCEMENTS; i++, num_summoned++)
  1050. if (self->monsterinfo.chosen_reinforcements[i] == 255)
  1051. break;
  1052. for (count = 0; count < num_summoned; count++)
  1053. {
  1054. offset = reinforcement_position[count];
  1055. startpoint = M_ProjectFlashSource(self, offset, f, r);
  1056. // a little off the ground
  1057. startpoint[2] += 10 * (self->s.scale ? self->s.scale : 1.0f);
  1058. auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]];
  1059. if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32))
  1060. {
  1061. if (CheckGroundSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs, 256, -1))
  1062. {
  1063. num_success++;
  1064. float radius = (reinforcement.maxs - reinforcement.mins).length() * 0.5f;
  1065. SpawnGrow_Spawn(spawnpoint + (reinforcement.mins + reinforcement.maxs), radius, radius * 2.f);
  1066. }
  1067. }
  1068. }
  1069. if (num_success == 0)
  1070. self->monsterinfo.nextframe = FRAME_attack53;
  1071. }
  1072. void medic_finish_spawn(edict_t *self)
  1073. {
  1074. edict_t *ent;
  1075. vec3_t f, r, offset, startpoint, spawnpoint;
  1076. int count;
  1077. int num_summoned; // should be 1, 3, or 5
  1078. edict_t *designated_enemy;
  1079. AngleVectors(self->s.angles, f, r, nullptr);
  1080. num_summoned = 0;
  1081. for (int32_t i = 0; i < MAX_REINFORCEMENTS; i++, num_summoned++)
  1082. if (self->monsterinfo.chosen_reinforcements[i] == 255)
  1083. break;
  1084. for (count = 0; count < num_summoned; count++)
  1085. {
  1086. auto &reinforcement = self->monsterinfo.reinforcements.reinforcements[self->monsterinfo.chosen_reinforcements[count]];
  1087. offset = reinforcement_position[count];
  1088. startpoint = M_ProjectFlashSource(self, offset, f, r);
  1089. // a little off the ground
  1090. startpoint[2] += 10 * (self->s.scale ? self->s.scale : 1.0f);
  1091. ent = nullptr;
  1092. if (FindSpawnPoint(startpoint, reinforcement.mins, reinforcement.maxs, spawnpoint, 32))
  1093. {
  1094. if (CheckSpawnPoint(spawnpoint, reinforcement.mins, reinforcement.maxs))
  1095. ent = CreateGroundMonster(spawnpoint, self->s.angles, reinforcement.mins, reinforcement.maxs, reinforcement.classname, 256);
  1096. }
  1097. if (!ent)
  1098. continue;
  1099. if (ent->think)
  1100. {
  1101. ent->nextthink = level.time;
  1102. ent->think(ent);
  1103. }
  1104. ent->monsterinfo.aiflags |= AI_IGNORE_SHOTS | AI_DO_NOT_COUNT | AI_SPAWNED_MEDIC_C;
  1105. ent->monsterinfo.commander = self;
  1106. ent->monsterinfo.monster_slots = reinforcement.strength;
  1107. self->monsterinfo.monster_used += reinforcement.strength;
  1108. if (self->monsterinfo.aiflags & AI_MEDIC)
  1109. designated_enemy = self->oldenemy;
  1110. else
  1111. designated_enemy = self->enemy;
  1112. if (coop->integer)
  1113. {
  1114. designated_enemy = PickCoopTarget(ent);
  1115. if (designated_enemy)
  1116. {
  1117. // try to avoid using my enemy
  1118. if (designated_enemy == self->enemy)
  1119. {
  1120. designated_enemy = PickCoopTarget(ent);
  1121. if (!designated_enemy)
  1122. designated_enemy = self->enemy;
  1123. }
  1124. }
  1125. else
  1126. designated_enemy = self->enemy;
  1127. }
  1128. if ((designated_enemy) && (designated_enemy->inuse) && (designated_enemy->health > 0))
  1129. {
  1130. ent->enemy = designated_enemy;
  1131. FoundTarget(ent);
  1132. }
  1133. else
  1134. {
  1135. ent->enemy = nullptr;
  1136. ent->monsterinfo.stand(ent);
  1137. }
  1138. }
  1139. }
  1140. mframe_t medic_frames_callReinforcements[] = {
  1141. // ROGUE - 33-36 now ai_charge
  1142. { ai_charge, 2 }, // 33
  1143. { ai_charge, 3 },
  1144. { ai_charge, 5 },
  1145. { ai_charge, 4.4f }, // 36
  1146. { ai_charge, 4.7f },
  1147. { ai_charge, 5 },
  1148. { ai_charge, 6 },
  1149. { ai_charge, 4 }, // 40
  1150. { ai_charge, 0, monster_footstep },
  1151. { ai_move, 0, medic_start_spawn }, // 42
  1152. { ai_move }, // 43 -- 43 through 47 are skipped
  1153. { ai_move },
  1154. { ai_move },
  1155. { ai_move },
  1156. { ai_move },
  1157. { ai_move, 0, medic_determine_spawn }, // 48
  1158. { ai_charge, 0, medic_spawngrows }, // 49
  1159. { ai_move }, // 50
  1160. { ai_move }, // 51
  1161. { ai_move, -15, medic_finish_spawn }, // 52
  1162. { ai_move, -1.5f },
  1163. { ai_move, -1.2f },
  1164. { ai_move, -3, monster_footstep }
  1165. };
  1166. MMOVE_T(medic_move_callReinforcements) = { FRAME_attack33, FRAME_attack55, medic_frames_callReinforcements, medic_run };
  1167. MONSTERINFO_ATTACK(medic_attack) (edict_t *self) -> void
  1168. {
  1169. monster_done_dodge(self);
  1170. float enemy_range = range_to(self, self->enemy);
  1171. // signal from checkattack to spawn
  1172. if (self->monsterinfo.aiflags & AI_BLOCKED)
  1173. {
  1174. M_SetAnimation(self, &medic_move_callReinforcements);
  1175. self->monsterinfo.aiflags &= ~AI_BLOCKED;
  1176. }
  1177. float r = frandom();
  1178. if (self->monsterinfo.aiflags & AI_MEDIC)
  1179. {
  1180. if ((self->mass > 400) && (r > 0.8f) && M_SlotsLeft(self))
  1181. M_SetAnimation(self, &medic_move_callReinforcements);
  1182. else
  1183. M_SetAnimation(self, &medic_move_attackCable);
  1184. }
  1185. else
  1186. {
  1187. if (self->monsterinfo.attack_state == AS_BLIND)
  1188. {
  1189. M_SetAnimation(self, &medic_move_callReinforcements);
  1190. return;
  1191. }
  1192. if ((self->mass > 400) && (r > 0.2f) && (enemy_range > RANGE_MELEE) && M_SlotsLeft(self))
  1193. M_SetAnimation(self, &medic_move_callReinforcements);
  1194. else
  1195. M_SetAnimation(self, &medic_move_attackBlaster);
  1196. }
  1197. }
  1198. MONSTERINFO_CHECKATTACK(medic_checkattack) (edict_t *self) -> bool
  1199. {
  1200. if (self->monsterinfo.aiflags & AI_MEDIC)
  1201. {
  1202. // if our target went away
  1203. if ((!self->enemy) || (!self->enemy->inuse))
  1204. {
  1205. abortHeal(self, true, false, false);
  1206. return false;
  1207. }
  1208. // if we ran out of time, give up
  1209. if (self->timestamp < level.time)
  1210. {
  1211. abortHeal(self, true, false, true);
  1212. self->timestamp = 0_ms;
  1213. return false;
  1214. }
  1215. if (realrange(self, self->enemy) < MEDIC_MAX_HEAL_DISTANCE + 10)
  1216. {
  1217. medic_attack(self);
  1218. return true;
  1219. }
  1220. else
  1221. {
  1222. self->monsterinfo.attack_state = AS_STRAIGHT;
  1223. return false;
  1224. }
  1225. }
  1226. if (self->enemy->client && !visible(self, self->enemy) && M_SlotsLeft(self))
  1227. {
  1228. self->monsterinfo.attack_state = AS_BLIND;
  1229. return true;
  1230. }
  1231. // give a LARGE bias to spawning things when we have room
  1232. // use AI_BLOCKED as a signal to attack to spawn
  1233. if (self->monsterinfo.monster_slots && (frandom() < 0.8f) && (M_SlotsLeft(self) > self->monsterinfo.monster_slots * 0.8f) && (realrange(self, self->enemy) > 150))
  1234. {
  1235. self->monsterinfo.aiflags |= AI_BLOCKED;
  1236. self->monsterinfo.attack_state = AS_MISSILE;
  1237. return true;
  1238. }
  1239. // ROGUE
  1240. // since his idle animation looks kinda bad in combat, always attack
  1241. // when he's on a combat point
  1242. if (self->monsterinfo.aiflags & AI_STAND_GROUND)
  1243. {
  1244. self->monsterinfo.attack_state = AS_MISSILE;
  1245. return true;
  1246. }
  1247. return M_CheckAttack(self);
  1248. }
  1249. void MedicCommanderCache()
  1250. {
  1251. gi.modelindex("models/items/spawngro3/tris.md2");
  1252. }
  1253. MONSTERINFO_DUCK(medic_duck) (edict_t *self, gtime_t eta) -> bool
  1254. {
  1255. // don't dodge if you're healing
  1256. if (self->monsterinfo.aiflags & AI_MEDIC)
  1257. return false;
  1258. if ((self->monsterinfo.active_move == &medic_move_attackHyperBlaster) ||
  1259. (self->monsterinfo.active_move == &medic_move_attackCable) ||
  1260. (self->monsterinfo.active_move == &medic_move_attackBlaster) ||
  1261. (self->monsterinfo.active_move == &medic_move_callReinforcements))
  1262. {
  1263. // he ignores skill
  1264. self->monsterinfo.unduck(self);
  1265. return false;
  1266. }
  1267. M_SetAnimation(self, &medic_move_duck);
  1268. return true;
  1269. }
  1270. MONSTERINFO_SIDESTEP(medic_sidestep) (edict_t *self) -> bool
  1271. {
  1272. if ((self->monsterinfo.active_move == &medic_move_attackHyperBlaster) ||
  1273. (self->monsterinfo.active_move == &medic_move_attackCable) ||
  1274. (self->monsterinfo.active_move == &medic_move_attackBlaster) ||
  1275. (self->monsterinfo.active_move == &medic_move_callReinforcements))
  1276. {
  1277. // if we're shooting, don't dodge
  1278. return false;
  1279. }
  1280. if (self->monsterinfo.active_move != &medic_move_run)
  1281. M_SetAnimation(self, &medic_move_run);
  1282. return true;
  1283. }
  1284. //===========
  1285. // PGM
  1286. MONSTERINFO_BLOCKED(medic_blocked) (edict_t *self, float dist) -> bool
  1287. {
  1288. if (blocked_checkplat(self, dist))
  1289. return true;
  1290. return false;
  1291. }
  1292. // PGM
  1293. //===========
  1294. /*QUAKED monster_medic_commander (1 .5 0) (-16 -16 -24) (16 16 32) Ambush Trigger_Spawn Sight
  1295. */
  1296. /*QUAKED monster_medic (1 .5 0) (-16 -16 -24) (16 16 32) Ambush Trigger_Spawn Sight
  1297. model="models/monsters/medic/tris.md2"
  1298. */
  1299. void SP_monster_medic(edict_t *self)
  1300. {
  1301. if ( !M_AllowSpawn( self ) ) {
  1302. G_FreeEdict( self );
  1303. return;
  1304. }
  1305. self->movetype = MOVETYPE_STEP;
  1306. self->solid = SOLID_BBOX;
  1307. self->s.modelindex = gi.modelindex("models/monsters/medic/tris.md2");
  1308. gi.modelindex("models/monsters/medic/gibs/chest.md2");
  1309. gi.modelindex("models/monsters/medic/gibs/gun.md2");
  1310. gi.modelindex("models/monsters/medic/gibs/head.md2");
  1311. gi.modelindex("models/monsters/medic/gibs/hook.md2");
  1312. gi.modelindex("models/monsters/medic/gibs/leg.md2");
  1313. self->mins = { -24, -24, -24 };
  1314. self->maxs = { 24, 24, 32 };
  1315. // PMM
  1316. if (strcmp(self->classname, "monster_medic_commander") == 0)
  1317. {
  1318. self->health = 600 * st.health_multiplier;
  1319. self->gib_health = -130;
  1320. self->mass = 600;
  1321. self->yaw_speed = 40; // default is 20
  1322. MedicCommanderCache();
  1323. }
  1324. else
  1325. {
  1326. // PMM
  1327. self->health = 300 * st.health_multiplier;
  1328. self->gib_health = -130;
  1329. self->mass = 400;
  1330. }
  1331. self->pain = medic_pain;
  1332. self->die = medic_die;
  1333. self->monsterinfo.stand = medic_stand;
  1334. self->monsterinfo.walk = medic_walk;
  1335. self->monsterinfo.run = medic_run;
  1336. // pmm
  1337. self->monsterinfo.dodge = M_MonsterDodge;
  1338. self->monsterinfo.duck = medic_duck;
  1339. self->monsterinfo.unduck = monster_duck_up;
  1340. self->monsterinfo.sidestep = medic_sidestep;
  1341. self->monsterinfo.blocked = medic_blocked;
  1342. // pmm
  1343. self->monsterinfo.attack = medic_attack;
  1344. self->monsterinfo.melee = nullptr;
  1345. self->monsterinfo.sight = medic_sight;
  1346. self->monsterinfo.idle = medic_idle;
  1347. self->monsterinfo.search = medic_search;
  1348. self->monsterinfo.checkattack = medic_checkattack;
  1349. self->monsterinfo.setskin = medic_setskin;
  1350. gi.linkentity(self);
  1351. M_SetAnimation(self, &medic_move_stand);
  1352. self->monsterinfo.scale = MODEL_SCALE;
  1353. walkmonster_start(self);
  1354. // PMM
  1355. self->monsterinfo.aiflags |= AI_IGNORE_SHOTS;
  1356. if (self->mass > 400)
  1357. {
  1358. self->s.skinnum = 2;
  1359. // commander sounds
  1360. commander_sound_idle1.assign("medic_commander/medidle.wav");
  1361. commander_sound_pain1.assign("medic_commander/medpain1.wav");
  1362. commander_sound_pain2.assign("medic_commander/medpain2.wav");
  1363. commander_sound_die.assign("medic_commander/meddeth.wav");
  1364. commander_sound_sight.assign("medic_commander/medsght.wav");
  1365. commander_sound_search.assign("medic_commander/medsrch.wav");
  1366. commander_sound_hook_launch.assign("medic_commander/medatck2c.wav");
  1367. commander_sound_hook_hit.assign("medic_commander/medatck3a.wav");
  1368. commander_sound_hook_heal.assign("medic_commander/medatck4a.wav");
  1369. commander_sound_hook_retract.assign("medic_commander/medatck5a.wav");
  1370. commander_sound_spawn.assign("medic_commander/monsterspawn1.wav");
  1371. gi.soundindex("tank/tnkatck3.wav");
  1372. const char *reinforcements = default_reinforcements;
  1373. if (!st.was_key_specified("monster_slots"))
  1374. self->monsterinfo.monster_slots = default_monster_slots_base;
  1375. if (st.was_key_specified("reinforcements"))
  1376. reinforcements = st.reinforcements;
  1377. if (self->monsterinfo.monster_slots && reinforcements && *reinforcements)
  1378. {
  1379. if (skill->integer)
  1380. self->monsterinfo.monster_slots += floor(self->monsterinfo.monster_slots * (skill->value / 2.f));
  1381. M_SetupReinforcements(reinforcements, self->monsterinfo.reinforcements);
  1382. }
  1383. }
  1384. else
  1385. {
  1386. sound_idle1.assign("medic/idle.wav");
  1387. sound_pain1.assign("medic/medpain1.wav");
  1388. sound_pain2.assign("medic/medpain2.wav");
  1389. sound_die.assign("medic/meddeth1.wav");
  1390. sound_sight.assign("medic/medsght1.wav");
  1391. sound_search.assign("medic/medsrch1.wav");
  1392. sound_hook_launch.assign("medic/medatck2.wav");
  1393. sound_hook_hit.assign("medic/medatck3.wav");
  1394. sound_hook_heal.assign("medic/medatck4.wav");
  1395. sound_hook_retract.assign("medic/medatck5.wav");
  1396. gi.soundindex("medic/medatck1.wav");
  1397. self->s.skinnum = 0;
  1398. }
  1399. // pmm
  1400. }