// Copyright (c) ZeniMax Media Inc. // Licensed under the GNU General Public License 2.0. /* fixbot.c */ #include "../g_local.h" #include "m_xatrix_fixbot.h" #include "../m_flash.h" bool infront(edict_t *self, edict_t *other); bool FindTarget(edict_t *self); static int sound_pain1; static int sound_die; static int sound_weld1; static int sound_weld2; static int sound_weld3; void fixbot_run(edict_t *self); void fixbot_attack(edict_t *self); void fixbot_dead(edict_t *self); void fixbot_fire_blaster(edict_t *self); void fixbot_fire_welder(edict_t *self); void use_scanner(edict_t *self); void change_to_roam(edict_t *self); void fly_vertical(edict_t *self); void fixbot_stand(edict_t *self); extern const mmove_t fixbot_move_forward; extern const mmove_t fixbot_move_stand; extern const mmove_t fixbot_move_stand2; extern const mmove_t fixbot_move_roamgoal; extern const mmove_t fixbot_move_weld_start; extern const mmove_t fixbot_move_weld; extern const mmove_t fixbot_move_weld_end; extern const mmove_t fixbot_move_takeoff; extern const mmove_t fixbot_move_landing; extern const mmove_t fixbot_move_turn; void roam_goal(edict_t *self); // [Paril-KEX] clean up bot goals if we get interrupted THINK(bot_goal_check) (edict_t *self) -> void { if (!self->owner || !self->owner->inuse || self->owner->goalentity != self) { G_FreeEdict(self); return; } self->nextthink = level.time + 1_ms; } void ED_CallSpawn(edict_t *ent); edict_t *fixbot_FindDeadMonster(edict_t *self) { edict_t *ent = nullptr; edict_t *best = nullptr; while ((ent = findradius(ent, self->s.origin, 1024)) != nullptr) { if (ent == self) continue; if (!(ent->svflags & SVF_MONSTER)) continue; if (ent->monsterinfo.aiflags & AI_GOOD_GUY) continue; // check to make sure we haven't bailed on this guy already if ((ent->monsterinfo.badMedic1 == self) || (ent->monsterinfo.badMedic2 == self)) continue; if (ent->monsterinfo.healer) // FIXME - this is correcting a bug that is somewhere else // if the healer is a monster, and it's in medic mode .. continue .. otherwise // we will override the healer, if it passes all the other tests if ((ent->monsterinfo.healer->inuse) && (ent->monsterinfo.healer->health > 0) && (ent->monsterinfo.healer->svflags & SVF_MONSTER) && (ent->monsterinfo.healer->monsterinfo.aiflags & AI_MEDIC)) continue; if (ent->health > 0) continue; if ((ent->nextthink) && (ent->think != monster_dead_think)) continue; if (!visible(self, ent)) continue; if (!best) { best = ent; continue; } if (ent->max_health <= best->max_health) continue; best = ent; } return best; } static void fixbot_set_fly_parameters(edict_t *self, bool heal, bool weld) { self->monsterinfo.fly_position_time = 0_sec; self->monsterinfo.fly_acceleration = 5.f; self->monsterinfo.fly_speed = 110.f; self->monsterinfo.fly_buzzard = false; if (heal) { self->monsterinfo.fly_min_distance = 100.f; self->monsterinfo.fly_max_distance = 100.f; self->monsterinfo.fly_thrusters = true; } else if (weld) { self->monsterinfo.fly_min_distance = 24.f; self->monsterinfo.fly_max_distance = 24.f; } else { // timid bot self->monsterinfo.fly_min_distance = 300.f; self->monsterinfo.fly_max_distance = 500.f; } } int fixbot_search(edict_t *self) { edict_t *ent; if (!self->enemy) { ent = fixbot_FindDeadMonster(self); if (ent) { self->oldenemy = self->enemy; self->enemy = ent; self->enemy->monsterinfo.healer = self; self->monsterinfo.aiflags |= AI_MEDIC; FoundTarget(self); fixbot_set_fly_parameters(self, true, false); return (1); } } return (0); } void landing_goal(edict_t *self) { trace_t tr; vec3_t forward, right, up; vec3_t end; edict_t *ent; ent = G_Spawn(); ent->classname = "bot_goal"; ent->solid = SOLID_BBOX; ent->owner = self; ent->think = bot_goal_check; gi.linkentity(ent); ent->mins = { -32, -32, -24 }; ent->maxs = { 32, 32, 24 }; AngleVectors(self->s.angles, forward, right, up); end = self->s.origin + (forward * 32); end = self->s.origin + (up * -8096); tr = gi.trace(self->s.origin, ent->mins, ent->maxs, end, self, MASK_MONSTERSOLID); ent->s.origin = tr.endpos; self->goalentity = self->enemy = ent; M_SetAnimation(self, &fixbot_move_landing); } void takeoff_goal(edict_t *self) { trace_t tr; vec3_t forward, right, up; vec3_t end; edict_t *ent; ent = G_Spawn(); ent->classname = "bot_goal"; ent->solid = SOLID_BBOX; ent->owner = self; ent->think = bot_goal_check; gi.linkentity(ent); ent->mins = { -32, -32, -24 }; ent->maxs = { 32, 32, 24 }; AngleVectors(self->s.angles, forward, right, up); end = self->s.origin + (forward * 32); end = self->s.origin + (up * 128); tr = gi.trace(self->s.origin, ent->mins, ent->maxs, end, self, MASK_MONSTERSOLID); ent->s.origin = tr.endpos; self->goalentity = self->enemy = ent; M_SetAnimation(self, &fixbot_move_takeoff); } void change_to_roam(edict_t *self) { if (fixbot_search(self)) return; M_SetAnimation(self, &fixbot_move_roamgoal); if (self->spawnflags.has(SPAWNFLAG_FIXBOT_LANDING)) { landing_goal(self); M_SetAnimation(self, &fixbot_move_landing); self->spawnflags &= ~SPAWNFLAG_FIXBOT_LANDING; self->spawnflags = SPAWNFLAG_FIXBOT_WORKING; } if (self->spawnflags.has(SPAWNFLAG_FIXBOT_TAKEOFF)) { takeoff_goal(self); M_SetAnimation(self, &fixbot_move_takeoff); self->spawnflags &= ~SPAWNFLAG_FIXBOT_TAKEOFF; self->spawnflags = SPAWNFLAG_FIXBOT_WORKING; } if (self->spawnflags.has(SPAWNFLAG_FIXBOT_FIXIT)) { M_SetAnimation(self, &fixbot_move_roamgoal); self->spawnflags &= ~SPAWNFLAG_FIXBOT_FIXIT; self->spawnflags = SPAWNFLAG_FIXBOT_WORKING; } if (!self->spawnflags) { M_SetAnimation(self, &fixbot_move_stand2); } } void roam_goal(edict_t *self) { trace_t tr; vec3_t forward, right, up; vec3_t end; edict_t *ent; vec3_t dang; float len, oldlen; int i; vec3_t vec; vec3_t whichvec {}; ent = G_Spawn(); ent->classname = "bot_goal"; ent->solid = SOLID_BBOX; ent->owner = self; ent->think = bot_goal_check; ent->nextthink = level.time + 1_ms; gi.linkentity(ent); oldlen = 0; for (i = 0; i < 12; i++) { dang = self->s.angles; if (i < 6) dang[YAW] += 30 * i; else dang[YAW] -= 30 * (i - 6); AngleVectors(dang, forward, right, up); end = self->s.origin + (forward * 8192); tr = gi.traceline(self->s.origin, end, self, MASK_PROJECTILE); vec = self->s.origin - tr.endpos; len = vec.normalize(); if (len > oldlen) { oldlen = len; whichvec = tr.endpos; } } ent->s.origin = whichvec; self->goalentity = self->enemy = ent; M_SetAnimation(self, &fixbot_move_turn); } void use_scanner(edict_t *self) { edict_t *ent = nullptr; float radius = 1024; vec3_t vec; float len; while ((ent = findradius(ent, self->s.origin, radius)) != nullptr) { if (ent->health >= 100) { if (strcmp(ent->classname, "object_repair") == 0) { if (visible(self, ent)) { // remove the old one if (strcmp(self->goalentity->classname, "bot_goal") == 0) { self->goalentity->nextthink = level.time + 100_ms; self->goalentity->think = G_FreeEdict; } self->goalentity = self->enemy = ent; vec = self->s.origin - self->goalentity->s.origin; len = vec.normalize(); fixbot_set_fly_parameters(self, false, true); if (len < 32) { M_SetAnimation(self, &fixbot_move_weld_start); return; } return; } } } } if (!self->goalentity) { M_SetAnimation(self, &fixbot_move_stand); return; } vec = self->s.origin - self->goalentity->s.origin; len = vec.length(); if (len < 32) { if (strcmp(self->goalentity->classname, "object_repair") == 0) { M_SetAnimation(self, &fixbot_move_weld_start); } else { self->goalentity->nextthink = level.time + 100_ms; self->goalentity->think = G_FreeEdict; self->goalentity = self->enemy = nullptr; M_SetAnimation(self, &fixbot_move_stand); } return; } vec = self->s.origin - self->s.old_origin; len = vec.length(); /* bot is stuck get new goalentity */ if (len == 0) { if (strcmp(self->goalentity->classname, "object_repair") == 0) { M_SetAnimation(self, &fixbot_move_stand); } else { self->goalentity->nextthink = level.time + 100_ms; self->goalentity->think = G_FreeEdict; self->goalentity = self->enemy = nullptr; M_SetAnimation(self, &fixbot_move_stand); } } } /* when the bot has found a landing pad it will proceed to its goalentity just above the landing pad and decend translated along the z the current frames are at 10fps */ void blastoff(edict_t *self, const vec3_t &start, const vec3_t &aimdir, int damage, int kick, int te_impact, int hspread, int vspread) { trace_t tr; vec3_t dir; vec3_t forward, right, up; vec3_t end; float r; float u; vec3_t water_start; bool water = false; contents_t content_mask = MASK_PROJECTILE | MASK_WATER; hspread += (self->s.frame - FRAME_takeoff_01); vspread += (self->s.frame - FRAME_takeoff_01); tr = gi.traceline(self->s.origin, start, self, MASK_PROJECTILE); if (!(tr.fraction < 1.0f)) { dir = vectoangles(aimdir); AngleVectors(dir, forward, right, up); r = crandom() * hspread; u = crandom() * vspread; end = start + (forward * 8192); end += (right * r); end += (up * u); if (gi.pointcontents(start) & MASK_WATER) { water = true; water_start = start; content_mask &= ~MASK_WATER; } tr = gi.traceline(start, end, self, content_mask); // see if we hit water if (tr.contents & MASK_WATER) { int color; water = true; water_start = tr.endpos; if (start != tr.endpos) { if (tr.contents & CONTENTS_WATER) { if (strcmp(tr.surface->name, "*brwater") == 0) color = SPLASH_BROWN_WATER; else color = SPLASH_BLUE_WATER; } else if (tr.contents & CONTENTS_SLIME) color = SPLASH_SLIME; else if (tr.contents & CONTENTS_LAVA) color = SPLASH_LAVA; else color = SPLASH_UNKNOWN; if (color != SPLASH_UNKNOWN) { gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_SPLASH); gi.WriteByte(8); gi.WritePosition(tr.endpos); gi.WriteDir(tr.plane.normal); gi.WriteByte(color); gi.multicast(tr.endpos, MULTICAST_PVS, false); } // change bullet's course when it enters water dir = end - start; dir = vectoangles(dir); AngleVectors(dir, forward, right, up); r = crandom() * hspread * 2; u = crandom() * vspread * 2; end = water_start + (forward * 8192); end += (right * r); end += (up * u); } // re-trace ignoring water this time tr = gi.traceline(water_start, end, self, MASK_PROJECTILE); } } // send gun puff / flash if (!((tr.surface) && (tr.surface->flags & SURF_SKY))) { if (tr.fraction < 1.0f) { if (tr.ent->takedamage) { T_Damage(tr.ent, self, self, aimdir, tr.endpos, tr.plane.normal, damage, kick, DAMAGE_BULLET, MOD_BLASTOFF); } else { if (!(tr.surface->flags & SURF_SKY)) { gi.WriteByte(svc_temp_entity); gi.WriteByte(te_impact); gi.WritePosition(tr.endpos); gi.WriteDir(tr.plane.normal); gi.multicast(tr.endpos, MULTICAST_PVS, false); if (self->client) PlayerNoise(self, tr.endpos, PNOISE_IMPACT); } } } } // if went through water, determine where the end and make a bubble trail if (water) { vec3_t pos; dir = tr.endpos - water_start; dir.normalize(); pos = tr.endpos + (dir * -2); if (gi.pointcontents(pos) & MASK_WATER) tr.endpos = pos; else tr = gi.traceline(pos, water_start, tr.ent, MASK_WATER); pos = water_start + tr.endpos; pos *= 0.5f; gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_BUBBLETRAIL); gi.WritePosition(water_start); gi.WritePosition(tr.endpos); gi.multicast(pos, MULTICAST_PVS, false); } } void fly_vertical(edict_t *self) { int i; vec3_t v; vec3_t forward, right, up; vec3_t start; vec3_t tempvec; v = self->goalentity->s.origin - self->s.origin; self->ideal_yaw = vectoyaw(v); M_ChangeYaw(self); if (self->s.frame == FRAME_landing_58 || self->s.frame == FRAME_takeoff_16) { self->goalentity->nextthink = level.time + 100_ms; self->goalentity->think = G_FreeEdict; M_SetAnimation(self, &fixbot_move_stand); self->goalentity = self->enemy = nullptr; } // kick up some particles tempvec = self->s.angles; tempvec[PITCH] += 90; AngleVectors(tempvec, forward, right, up); start = self->s.origin; for (i = 0; i < 10; i++) blastoff(self, start, forward, 2, 1, TE_SHOTGUN, DEFAULT_SHOTGUN_HSPREAD, DEFAULT_SHOTGUN_VSPREAD); // needs sound } void fly_vertical2(edict_t *self) { vec3_t v; float len; v = self->goalentity->s.origin - self->s.origin; len = v.length(); self->ideal_yaw = vectoyaw(v); M_ChangeYaw(self); if (len < 32) { self->goalentity->nextthink = level.time + 100_ms; self->goalentity->think = G_FreeEdict; M_SetAnimation(self, &fixbot_move_stand); self->goalentity = self->enemy = nullptr; } // needs sound } mframe_t fixbot_frames_landing[] = { { ai_move }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 }, { ai_move, 0, fly_vertical2 } }; MMOVE_T(fixbot_move_landing) = { FRAME_landing_01, FRAME_landing_58, fixbot_frames_landing, nullptr }; /* generic ambient stand */ mframe_t fixbot_frames_stand[] = { { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move, 0, change_to_roam } }; MMOVE_T(fixbot_move_stand) = { FRAME_ambient_01, FRAME_ambient_19, fixbot_frames_stand, nullptr }; mframe_t fixbot_frames_stand2[] = { { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand }, { ai_stand, 0, change_to_roam } }; MMOVE_T(fixbot_move_stand2) = { FRAME_ambient_01, FRAME_ambient_19, fixbot_frames_stand2, nullptr }; #if 0 /* will need the pickup offset for the front pincers object will need to stop forward of the object and take the object with it ( this may require a variant of liftoff and landing ) */ mframe_t fixbot_frames_pickup[] = { { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move } }; MMOVE_T(fixbot_move_pickup) = { FRAME_pickup_01, FRAME_pickup_27, fixbot_frames_pickup, nullptr }; #endif /* generic frame to move bot */ mframe_t fixbot_frames_roamgoal[] = { { ai_move, 0, roam_goal } }; MMOVE_T(fixbot_move_roamgoal) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_roamgoal, nullptr }; void ai_facing(edict_t *self, float dist) { if (!self->goalentity) { fixbot_stand(self); return; } vec3_t v; if (infront(self, self->goalentity)) M_SetAnimation(self, &fixbot_move_forward); else { v = self->goalentity->s.origin - self->s.origin; self->ideal_yaw = vectoyaw(v); M_ChangeYaw(self); } }; mframe_t fixbot_frames_turn[] = { { ai_facing } }; MMOVE_T(fixbot_move_turn) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_turn, nullptr }; void go_roam(edict_t *self) { M_SetAnimation(self, &fixbot_move_stand); } /* takeoff */ mframe_t fixbot_frames_takeoff[] = { { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical }, { ai_move, 0.01f, fly_vertical } }; MMOVE_T(fixbot_move_takeoff) = { FRAME_takeoff_01, FRAME_takeoff_16, fixbot_frames_takeoff, nullptr }; /* findout what this is */ mframe_t fixbot_frames_paina[] = { { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move } }; MMOVE_T(fixbot_move_paina) = { FRAME_paina_01, FRAME_paina_06, fixbot_frames_paina, fixbot_run }; /* findout what this is */ mframe_t fixbot_frames_painb[] = { { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move }, { ai_move } }; MMOVE_T(fixbot_move_painb) = { FRAME_painb_01, FRAME_painb_08, fixbot_frames_painb, fixbot_run }; /* backup from pain call a generic painsound some spark effects */ mframe_t fixbot_frames_pain3[] = { { ai_move, -1 } }; MMOVE_T(fixbot_move_pain3) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_pain3, fixbot_run }; #if 0 /* bot has compleated landing and is now on the grownd ( may need second land if the bot is releasing jib into jib vat ) */ mframe_t fixbot_frames_land[] = { { ai_move } }; MMOVE_T(fixbot_move_land) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_land, nullptr }; #endif void M_MoveToGoal(edict_t *ent, float dist); void ai_movetogoal(edict_t *self, float dist) { M_MoveToGoal(self, dist); } /* */ mframe_t fixbot_frames_forward[] = { { ai_movetogoal, 5, use_scanner } }; MMOVE_T(fixbot_move_forward) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_forward, nullptr }; /* */ mframe_t fixbot_frames_walk[] = { { ai_walk, 5 } }; MMOVE_T(fixbot_move_walk) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_walk, nullptr }; /* */ mframe_t fixbot_frames_run[] = { { ai_run, 10 } }; MMOVE_T(fixbot_move_run) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_run, nullptr }; #if 0 /* raf note to self they could have a timer that will cause the bot to explode on countdown */ mframe_t fixbot_frames_death1[] = { { ai_move } }; MMOVE_T(fixbot_move_death1) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_death1, fixbot_dead }; // mframe_t fixbot_frames_backward[] = { { ai_move } }; MMOVE_T(fixbot_move_backward) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_backward, nullptr }; #endif // mframe_t fixbot_frames_start_attack[] = { { ai_charge } }; MMOVE_T(fixbot_move_start_attack) = { FRAME_freeze_01, FRAME_freeze_01, fixbot_frames_start_attack, fixbot_attack }; #if 0 /* TBD: need to get laser attack anim attack with the laser blast */ mframe_t fixbot_frames_attack1[] = { { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge, -10, fixbot_fire_blaster } }; MMOVE_T(fixbot_move_attack1) = { FRAME_shoot_01, FRAME_shoot_06, fixbot_frames_attack1, nullptr }; #endif void abortHeal(edict_t *self, bool change_frame, bool gib, bool mark); PRETHINK(fixbot_laser_update) (edict_t *laser) -> void { edict_t *self = laser->owner; vec3_t start, dir; AngleVectors(self->s.angles, dir, nullptr, nullptr); start = self->s.origin + (dir * 16); if (self->enemy && self->health > 0) { vec3_t point; point = (self->enemy->absmin + self->enemy->absmax) * 0.5f; if (self->monsterinfo.aiflags & AI_MEDIC) point[0] += sinf(level.time.seconds()) * 8; dir = point - self->s.origin; dir.normalize(); } laser->s.origin = start; laser->movedir = dir; gi.linkentity(laser); dabeam_update(laser, true); } void fixbot_fire_laser(edict_t *self) { // critter dun got blown up while bein' fixed if (!self->enemy || !self->enemy->inuse || self->enemy->health <= self->enemy->gib_health) { M_SetAnimation(self, &fixbot_move_stand); self->monsterinfo.aiflags &= ~AI_MEDIC; return; } monster_fire_dabeam(self, -1, false, fixbot_laser_update); if (self->enemy->health > (self->enemy->mass / 10)) { vec3_t maxs; self->enemy->spawnflags = SPAWNFLAG_NONE; self->enemy->monsterinfo.aiflags &= AI_STINKY | AI_SPAWNED_MASK; self->enemy->target = nullptr; self->enemy->targetname = nullptr; self->enemy->combattarget = nullptr; self->enemy->deathtarget = nullptr; self->enemy->healthtarget = nullptr; self->enemy->itemtarget = nullptr; self->enemy->monsterinfo.healer = self; maxs = self->enemy->maxs; maxs[2] += 48; // compensate for change when they die trace_t tr = gi.trace(self->enemy->s.origin, self->enemy->mins, maxs, self->enemy->s.origin, self->enemy, MASK_MONSTERSOLID); if (tr.startsolid || tr.allsolid) { abortHeal(self, false, true, false); return; } else if (tr.ent != world) { abortHeal(self, false, true, false); return; } else { self->enemy->monsterinfo.aiflags |= AI_IGNORE_SHOTS | AI_DO_NOT_COUNT; // backup & restore health stuff, because of multipliers int32_t old_max_health = self->enemy->max_health; item_id_t old_power_armor_type = self->enemy->monsterinfo.initial_power_armor_type; int32_t old_power_armor_power = self->enemy->monsterinfo.max_power_armor_power; int32_t old_base_health = self->enemy->monsterinfo.base_health; int32_t old_health_scaling = self->enemy->monsterinfo.health_scaling; auto reinforcements = self->enemy->monsterinfo.reinforcements; int32_t monster_slots = self->enemy->monsterinfo.monster_slots; int32_t monster_used = self->enemy->monsterinfo.monster_used; int32_t old_gib_health = self->enemy->gib_health; st = {}; st.keys_specified.emplace("reinforcements"); st.reinforcements = ""; ED_CallSpawn(self->enemy); self->enemy->monsterinfo.reinforcements = reinforcements; self->enemy->monsterinfo.monster_slots = monster_slots; self->enemy->monsterinfo.monster_used = monster_used; self->enemy->gib_health = old_gib_health / 2; self->enemy->health = self->enemy->max_health = old_max_health; self->enemy->monsterinfo.power_armor_power = self->enemy->monsterinfo.max_power_armor_power = old_power_armor_power; self->enemy->monsterinfo.power_armor_type = self->enemy->monsterinfo.initial_power_armor_type = old_power_armor_type; self->enemy->monsterinfo.base_health = old_base_health; self->enemy->monsterinfo.health_scaling = old_health_scaling; if (self->enemy->monsterinfo.setskin) self->enemy->monsterinfo.setskin(self->enemy); if (self->enemy->think) { self->enemy->nextthink = level.time; self->enemy->think(self->enemy); } self->enemy->monsterinfo.aiflags &= ~AI_RESURRECTING; self->enemy->monsterinfo.aiflags |= AI_IGNORE_SHOTS | AI_DO_NOT_COUNT; // turn off flies self->enemy->s.effects &= ~EF_FLIES; self->enemy->monsterinfo.healer = nullptr; // clean up target, if we have one and it's legit if (self->enemy && self->enemy->inuse) { cleanupHealTarget(self->enemy); if ((self->oldenemy) && (self->oldenemy->inuse) && (self->oldenemy->health > 0)) { self->enemy->enemy = self->oldenemy; FoundTarget(self->enemy); } else { self->enemy->enemy = nullptr; if (!FindTarget(self->enemy)) { // no valid enemy, so stop acting self->enemy->monsterinfo.pausetime = HOLD_FOREVER; self->enemy->monsterinfo.stand(self->enemy); } self->enemy = nullptr; self->oldenemy = nullptr; if (!FindTarget(self)) { // no valid enemy, so stop acting self->monsterinfo.pausetime = HOLD_FOREVER; self->monsterinfo.stand(self); return; } } } } M_SetAnimation(self, &fixbot_move_stand); } else self->enemy->monsterinfo.aiflags |= AI_RESURRECTING; } mframe_t fixbot_frames_laserattack[] = { { ai_charge, 0, fixbot_fire_laser }, { ai_charge, 0, fixbot_fire_laser }, { ai_charge, 0, fixbot_fire_laser }, { ai_charge, 0, fixbot_fire_laser }, { ai_charge, 0, fixbot_fire_laser }, { ai_charge, 0, fixbot_fire_laser } }; MMOVE_T(fixbot_move_laserattack) = { FRAME_shoot_01, FRAME_shoot_06, fixbot_frames_laserattack, nullptr }; /* need to get forward translation data for the charge attack */ mframe_t fixbot_frames_attack2[] = { { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, -10 }, { ai_charge, 0, fixbot_fire_blaster }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge }, { ai_charge } }; MMOVE_T(fixbot_move_attack2) = { FRAME_charging_01, FRAME_charging_31, fixbot_frames_attack2, fixbot_run }; void weldstate(edict_t *self) { if (self->s.frame == FRAME_weldstart_10) M_SetAnimation(self, &fixbot_move_weld); else if (self->goalentity && self->s.frame == FRAME_weldmiddle_07) { if (self->goalentity->health <= 0) { self->enemy->owner = nullptr; M_SetAnimation(self, &fixbot_move_weld_end); } else self->goalentity->health -= 10; } else { self->goalentity = self->enemy = nullptr; M_SetAnimation(self, &fixbot_move_stand); } } void ai_move2(edict_t *self, float dist) { if (!self->goalentity) { fixbot_stand(self); return; } vec3_t v; M_walkmove(self, self->s.angles[YAW], dist); v = self->goalentity->s.origin - self->s.origin; self->ideal_yaw = vectoyaw(v); M_ChangeYaw(self); }; mframe_t fixbot_frames_weld_start[] = { { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0 }, { ai_move2, 0, weldstate } }; MMOVE_T(fixbot_move_weld_start) = { FRAME_weldstart_01, FRAME_weldstart_10, fixbot_frames_weld_start, nullptr }; mframe_t fixbot_frames_weld[] = { { ai_move2, 0, fixbot_fire_welder }, { ai_move2, 0, fixbot_fire_welder }, { ai_move2, 0, fixbot_fire_welder }, { ai_move2, 0, fixbot_fire_welder }, { ai_move2, 0, fixbot_fire_welder }, { ai_move2, 0, fixbot_fire_welder }, { ai_move2, 0, weldstate } }; MMOVE_T(fixbot_move_weld) = { FRAME_weldmiddle_01, FRAME_weldmiddle_07, fixbot_frames_weld, nullptr }; mframe_t fixbot_frames_weld_end[] = { { ai_move2, -2 }, { ai_move2, -2 }, { ai_move2, -2 }, { ai_move2, -2 }, { ai_move2, -2 }, { ai_move2, -2 }, { ai_move2, -2, weldstate } }; MMOVE_T(fixbot_move_weld_end) = { FRAME_weldend_01, FRAME_weldend_07, fixbot_frames_weld_end, nullptr }; void fixbot_fire_welder(edict_t *self) { vec3_t start; vec3_t forward, right, up; vec3_t end; vec3_t dir; vec3_t vec; float r; if (!self->enemy) return; vec[0] = 24.0; vec[1] = -0.8f; vec[2] = -10.0; AngleVectors(self->s.angles, forward, right, up); start = M_ProjectFlashSource(self, vec, forward, right); end = self->enemy->s.origin; dir = end - start; gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_WELDING_SPARKS); gi.WriteByte(10); gi.WritePosition(start); gi.WriteDir(vec3_origin); gi.WriteByte(irandom(0xe0, 0xe8)); gi.multicast(self->s.origin, MULTICAST_PVS, false); if (frandom() > 0.8f) { r = frandom(); if (r < 0.33f) gi.sound(self, CHAN_VOICE, sound_weld1, 1, ATTN_IDLE, 0); else if (r < 0.66f) gi.sound(self, CHAN_VOICE, sound_weld2, 1, ATTN_IDLE, 0); else gi.sound(self, CHAN_VOICE, sound_weld3, 1, ATTN_IDLE, 0); } } void fixbot_fire_blaster(edict_t *self) { vec3_t start; vec3_t forward, right, up; vec3_t end; vec3_t dir; if (!visible(self, self->enemy)) { M_SetAnimation(self, &fixbot_move_run); } AngleVectors(self->s.angles, forward, right, up); start = M_ProjectFlashSource(self, monster_flash_offset[MZ2_HOVER_BLASTER_1], forward, right); end = self->enemy->s.origin; end[2] += self->enemy->viewheight; dir = end - start; dir.normalize(); monster_fire_blaster(self, start, dir, 15, 1000, MZ2_HOVER_BLASTER_1, EF_BLASTER); } MONSTERINFO_STAND(fixbot_stand) (edict_t *self) -> void { M_SetAnimation(self, &fixbot_move_stand); } MONSTERINFO_RUN(fixbot_run) (edict_t *self) -> void { if (self->monsterinfo.aiflags & AI_STAND_GROUND) M_SetAnimation(self, &fixbot_move_stand); else M_SetAnimation(self, &fixbot_move_run); } MONSTERINFO_WALK(fixbot_walk) (edict_t *self) -> void { vec3_t vec; float len; if (self->goalentity && strcmp(self->goalentity->classname, "object_repair") == 0) { vec = self->s.origin - self->goalentity->s.origin; len = vec.length(); if (len < 32) { M_SetAnimation(self, &fixbot_move_weld_start); return; } } M_SetAnimation(self, &fixbot_move_walk); } void fixbot_start_attack(edict_t *self) { M_SetAnimation(self, &fixbot_move_start_attack); } MONSTERINFO_ATTACK(fixbot_attack) (edict_t *self) -> void { vec3_t vec; float len; if (self->monsterinfo.aiflags & AI_MEDIC) { if (!visible(self, self->enemy)) return; vec = self->s.origin - self->enemy->s.origin; len = vec.length(); if (len > 128) return; else M_SetAnimation(self, &fixbot_move_laserattack); } else { fixbot_set_fly_parameters(self, false, false); M_SetAnimation(self, &fixbot_move_attack2); } } PAIN(fixbot_pain) (edict_t *self, edict_t *other, float kick, int damage, const mod_t &mod) -> void { if (level.time < self->pain_debounce_time) return; fixbot_set_fly_parameters(self, false, false); self->pain_debounce_time = level.time + 3_sec; gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); if (damage <= 10) M_SetAnimation(self, &fixbot_move_pain3); else if (damage <= 25) M_SetAnimation(self, &fixbot_move_painb); else M_SetAnimation(self, &fixbot_move_paina); } void fixbot_dead(edict_t *self) { self->mins = { -16, -16, -24 }; self->maxs = { 16, 16, -8 }; self->movetype = MOVETYPE_TOSS; self->svflags |= SVF_DEADMONSTER; self->nextthink = 0_ms; gi.linkentity(self); } DIE(fixbot_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); BecomeExplosion1(self); // shards } /*QUAKED monster_fixbot (1 .5 0) (-32 -32 -24) (32 32 24) Ambush Trigger_Spawn Fixit Takeoff Landing */ void SP_monster_fixbot(edict_t *self) { if ( !M_AllowSpawn( self ) ) { G_FreeEdict( self ); return; } sound_pain1 = gi.soundindex("flyer/flypain1.wav"); sound_die = gi.soundindex("flyer/flydeth1.wav"); sound_weld1 = gi.soundindex("misc/welder1.wav"); sound_weld2 = gi.soundindex("misc/welder2.wav"); sound_weld3 = gi.soundindex("misc/welder3.wav"); self->s.modelindex = gi.modelindex("models/monsters/fixbot/tris.md2"); self->mins = { -32, -32, -24 }; self->maxs = { 32, 32, 24 }; self->movetype = MOVETYPE_STEP; self->solid = SOLID_BBOX; self->health = 150 * st.health_multiplier; self->mass = 150; self->pain = fixbot_pain; self->die = fixbot_die; self->monsterinfo.stand = fixbot_stand; self->monsterinfo.walk = fixbot_walk; self->monsterinfo.run = fixbot_run; self->monsterinfo.attack = fixbot_attack; gi.linkentity(self); M_SetAnimation(self, &fixbot_move_stand); self->monsterinfo.scale = MODEL_SCALE; self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; fixbot_set_fly_parameters(self, false, false); flymonster_start(self); }