mirror of
https://github.com/id-Software/quake2-rerelease-dll.git
synced 2024-11-23 12:52:41 +00:00
2077 lines
No EOL
55 KiB
C++
2077 lines
No EOL
55 KiB
C++
// Copyright (c) ZeniMax Media Inc.
|
|
// Licensed under the GNU General Public License 2.0.
|
|
#include "g_local.h"
|
|
|
|
/*QUAKED target_temp_entity (1 0 0) (-8 -8 -8) (8 8 8)
|
|
Fire an origin based temp entity event to the clients.
|
|
"style" type byte
|
|
*/
|
|
USE(Use_Target_Tent) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(ent->style);
|
|
gi.WritePosition(ent->s.origin);
|
|
gi.multicast(ent->s.origin, MULTICAST_PVS, false);
|
|
}
|
|
|
|
void SP_target_temp_entity(edict_t *ent)
|
|
{
|
|
if (level.is_n64 && ent->style == 27)
|
|
ent->style = TE_TELEPORT_EFFECT;
|
|
|
|
ent->use = Use_Target_Tent;
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_speaker (1 0 0) (-8 -8 -8) (8 8 8) looped-on looped-off reliable
|
|
"noise" wav file to play
|
|
"attenuation"
|
|
-1 = none, send to whole level
|
|
1 = normal fighting sounds
|
|
2 = idle sound level
|
|
3 = ambient sound level
|
|
"volume" 0.0 to 1.0
|
|
|
|
Normal sounds play each time the target is used. The reliable flag can be set for crucial voiceovers.
|
|
|
|
[Paril-KEX] looped sounds are by default atten 3 / vol 1, and the use function toggles it on/off.
|
|
*/
|
|
|
|
constexpr spawnflags_t SPAWNFLAG_SPEAKER_LOOPED_ON = 1_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_SPEAKER_LOOPED_OFF = 2_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_SPEAKER_RELIABLE = 4_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_SPEAKER_NO_STEREO = 8_spawnflag;
|
|
|
|
USE(Use_Target_Speaker) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
soundchan_t chan;
|
|
|
|
if (ent->spawnflags.has(SPAWNFLAG_SPEAKER_LOOPED_ON | SPAWNFLAG_SPEAKER_LOOPED_OFF))
|
|
{ // looping sound toggles
|
|
if (ent->s.sound)
|
|
ent->s.sound = 0; // turn it off
|
|
else
|
|
ent->s.sound = ent->noise_index; // start it
|
|
}
|
|
else
|
|
{ // normal sound
|
|
if (ent->spawnflags.has(SPAWNFLAG_SPEAKER_RELIABLE))
|
|
chan = CHAN_VOICE | CHAN_RELIABLE;
|
|
else
|
|
chan = CHAN_VOICE;
|
|
// use a positioned_sound, because this entity won't normally be
|
|
// sent to any clients because it is invisible
|
|
gi.positioned_sound(ent->s.origin, ent, chan, ent->noise_index, ent->volume, ent->attenuation, 0);
|
|
}
|
|
}
|
|
|
|
void SP_target_speaker(edict_t *ent)
|
|
{
|
|
if (!st.noise)
|
|
{
|
|
gi.Com_PrintFmt("{}: no noise set\n", *ent);
|
|
return;
|
|
}
|
|
|
|
if (!strstr(st.noise, ".wav"))
|
|
ent->noise_index = gi.soundindex(G_Fmt("{}.wav", st.noise).data());
|
|
else
|
|
ent->noise_index = gi.soundindex(st.noise);
|
|
|
|
if (!ent->volume)
|
|
ent->volume = ent->s.loop_volume = 1.0;
|
|
|
|
if (!ent->attenuation)
|
|
{
|
|
if (ent->spawnflags.has(SPAWNFLAG_SPEAKER_LOOPED_OFF | SPAWNFLAG_SPEAKER_LOOPED_ON))
|
|
ent->attenuation = ATTN_STATIC;
|
|
else
|
|
ent->attenuation = ATTN_NORM;
|
|
}
|
|
else if (ent->attenuation == -1) // use -1 so 0 defaults to 1
|
|
{
|
|
if (ent->spawnflags.has(SPAWNFLAG_SPEAKER_LOOPED_OFF | SPAWNFLAG_SPEAKER_LOOPED_ON))
|
|
{
|
|
ent->attenuation = ATTN_LOOP_NONE;
|
|
ent->svflags |= SVF_NOCULL;
|
|
}
|
|
else
|
|
ent->attenuation = ATTN_NONE;
|
|
}
|
|
|
|
ent->s.loop_attenuation = ent->attenuation;
|
|
|
|
// check for prestarted looping sound
|
|
if (ent->spawnflags.has(SPAWNFLAG_SPEAKER_LOOPED_ON))
|
|
ent->s.sound = ent->noise_index;
|
|
|
|
if (ent->spawnflags.has(SPAWNFLAG_SPEAKER_NO_STEREO))
|
|
ent->s.renderfx |= RF_NO_STEREO;
|
|
|
|
ent->use = Use_Target_Speaker;
|
|
|
|
// must link the entity so we get areas and clusters so
|
|
// the server can determine who to send updates to
|
|
gi.linkentity(ent);
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
constexpr spawnflags_t SPAWNFLAG_HELP_HELP1 = 1_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_SET_POI = 2_spawnflag;
|
|
|
|
extern void target_poi_use(edict_t* ent, edict_t* other, edict_t* activator);
|
|
USE(Use_Target_Help) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
if (ent->spawnflags.has(SPAWNFLAG_HELP_HELP1))
|
|
{
|
|
if (strcmp(game.helpmessage1, ent->message))
|
|
{
|
|
Q_strlcpy(game.helpmessage1, ent->message, sizeof(game.helpmessage1));
|
|
game.help1changed++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (strcmp(game.helpmessage2, ent->message))
|
|
{
|
|
Q_strlcpy(game.helpmessage2, ent->message, sizeof(game.helpmessage2));
|
|
game.help2changed++;
|
|
}
|
|
}
|
|
|
|
if (ent->spawnflags.has(SPAWNFLAG_SET_POI))
|
|
{
|
|
target_poi_use(ent, other, activator);
|
|
}
|
|
}
|
|
|
|
/*QUAKED target_help (1 0 1) (-16 -16 -24) (16 16 24) help1 setpoi
|
|
When fired, the "message" key becomes the current personal computer string, and the message light will be set on all clients status bars.
|
|
*/
|
|
void SP_target_help(edict_t *ent)
|
|
{
|
|
if (deathmatch->integer)
|
|
{ // auto-remove for deathmatch
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
if (!ent->message)
|
|
{
|
|
gi.Com_PrintFmt("{}: no message\n", *ent);
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
ent->use = Use_Target_Help;
|
|
|
|
if (ent->spawnflags.has(SPAWNFLAG_SET_POI))
|
|
{
|
|
if (st.image)
|
|
ent->noise_index = gi.imageindex(st.image);
|
|
else
|
|
ent->noise_index = gi.imageindex("friend");
|
|
}
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_secret (1 0 1) (-8 -8 -8) (8 8 8)
|
|
Counts a secret found.
|
|
These are single use targets.
|
|
*/
|
|
USE(use_target_secret) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
gi.sound(ent, CHAN_VOICE, ent->noise_index, 1, ATTN_NORM, 0);
|
|
|
|
level.found_secrets++;
|
|
|
|
G_UseTargets(ent, activator);
|
|
G_FreeEdict(ent);
|
|
}
|
|
|
|
THINK(G_VerifyTargetted) (edict_t *ent) -> void
|
|
{
|
|
if (!ent->targetname || !*ent->targetname)
|
|
gi.Com_PrintFmt("WARNING: missing targetname on {}\n", *ent);
|
|
else if (!G_FindByString<&edict_t::target>(nullptr, ent->targetname))
|
|
gi.Com_PrintFmt("WARNING: doesn't appear to be anything targeting {}\n", *ent);
|
|
}
|
|
|
|
void SP_target_secret(edict_t *ent)
|
|
{
|
|
if (deathmatch->integer)
|
|
{ // auto-remove for deathmatch
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
ent->think = G_VerifyTargetted;
|
|
ent->nextthink = level.time + 10_ms;
|
|
|
|
ent->use = use_target_secret;
|
|
if (!st.noise)
|
|
st.noise = "misc/secret.wav";
|
|
ent->noise_index = gi.soundindex(st.noise);
|
|
ent->svflags = SVF_NOCLIENT;
|
|
level.total_secrets++;
|
|
}
|
|
|
|
//==========================================================
|
|
// [Paril-KEX] notify this player of a goal change
|
|
void G_PlayerNotifyGoal(edict_t *player)
|
|
{
|
|
// no goals in DM
|
|
if (deathmatch->integer)
|
|
return;
|
|
|
|
if (!player->client->pers.spawned)
|
|
return;
|
|
else if ((level.time - player->client->resp.entertime) < 300_ms)
|
|
return;
|
|
|
|
// N64 goals
|
|
if (level.goals)
|
|
{
|
|
// if the goal has updated, commit it first
|
|
if (game.help1changed != game.help2changed)
|
|
{
|
|
const char *current_goal = level.goals;
|
|
|
|
// skip ahead by the number of goals we've finished
|
|
for (int32_t i = 0; i < level.goal_num; i++)
|
|
{
|
|
while (*current_goal && *current_goal != '\t')
|
|
current_goal++;
|
|
|
|
if (!*current_goal)
|
|
gi.Com_Error("invalid n64 goals; tell Paril\n");
|
|
|
|
current_goal++;
|
|
}
|
|
|
|
// find the end of this goal
|
|
const char *goal_end = current_goal;
|
|
|
|
while (*goal_end && *goal_end != '\t')
|
|
goal_end++;
|
|
|
|
Q_strlcpy(game.helpmessage1, current_goal, min((size_t) (goal_end - current_goal + 1), sizeof(game.helpmessage1)));
|
|
|
|
game.help2changed = game.help1changed;
|
|
}
|
|
|
|
if (player->client->pers.game_help1changed != game.help1changed)
|
|
{
|
|
gi.LocClient_Print(player, PRINT_TYPEWRITER, game.helpmessage1);
|
|
gi.local_sound(player, player, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/talk.wav"), 1.0f, ATTN_NONE, 0.0f, GetUnicastKey());
|
|
|
|
player->client->pers.game_help1changed = game.help1changed;
|
|
}
|
|
|
|
// no regular goals
|
|
return;
|
|
}
|
|
|
|
if (player->client->pers.game_help1changed != game.help1changed)
|
|
{
|
|
player->client->pers.game_help1changed = game.help1changed;
|
|
player->client->pers.helpchanged = 1;
|
|
player->client->pers.help_time = level.time + 5_sec;
|
|
|
|
if (*game.helpmessage1)
|
|
// [Sam-KEX] Print objective to screen
|
|
gi.LocClient_Print(player, PRINT_TYPEWRITER, "$g_primary_mission_objective", game.helpmessage1);
|
|
}
|
|
|
|
if (player->client->pers.game_help2changed != game.help2changed)
|
|
{
|
|
player->client->pers.game_help2changed = game.help2changed;
|
|
player->client->pers.helpchanged = 1;
|
|
player->client->pers.help_time = level.time + 5_sec;
|
|
|
|
if (*game.helpmessage2)
|
|
// [Sam-KEX] Print objective to screen
|
|
gi.LocClient_Print(player, PRINT_TYPEWRITER, "$g_secondary_mission_objective", game.helpmessage2);
|
|
}
|
|
}
|
|
|
|
/*QUAKED target_goal (1 0 1) (-8 -8 -8) (8 8 8) KEEP_MUSIC
|
|
Counts a goal completed.
|
|
These are single use targets.
|
|
*/
|
|
constexpr spawnflags_t SPAWNFLAG_GOAL_KEEP_MUSIC = 1_spawnflag;
|
|
|
|
USE(use_target_goal) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
gi.sound(ent, CHAN_VOICE, ent->noise_index, 1, ATTN_NORM, 0);
|
|
|
|
level.found_goals++;
|
|
|
|
if (level.found_goals == level.total_goals && !ent->spawnflags.has(SPAWNFLAG_GOAL_KEEP_MUSIC))
|
|
{
|
|
if (ent->sounds)
|
|
gi.configstring (CS_CDTRACK, G_Fmt("{}", ent->sounds).data() );
|
|
else
|
|
gi.configstring(CS_CDTRACK, "0");
|
|
}
|
|
|
|
// [Paril-KEX] n64 goals
|
|
if (level.goals)
|
|
{
|
|
level.goal_num++;
|
|
game.help1changed++;
|
|
|
|
for (auto player : active_players())
|
|
G_PlayerNotifyGoal(player);
|
|
}
|
|
|
|
G_UseTargets(ent, activator);
|
|
G_FreeEdict(ent);
|
|
}
|
|
|
|
void SP_target_goal(edict_t *ent)
|
|
{
|
|
if (deathmatch->integer)
|
|
{ // auto-remove for deathmatch
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
ent->use = use_target_goal;
|
|
if (!st.noise)
|
|
st.noise = "misc/secret.wav";
|
|
ent->noise_index = gi.soundindex(st.noise);
|
|
ent->svflags = SVF_NOCLIENT;
|
|
level.total_goals++;
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_explosion (1 0 0) (-8 -8 -8) (8 8 8)
|
|
Spawns an explosion temporary entity when used.
|
|
|
|
"delay" wait this long before going off
|
|
"dmg" how much radius damage should be done, defaults to 0
|
|
*/
|
|
THINK(target_explosion_explode) (edict_t *self) -> void
|
|
{
|
|
float save;
|
|
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_EXPLOSION1);
|
|
gi.WritePosition(self->s.origin);
|
|
gi.multicast(self->s.origin, MULTICAST_PHS, false);
|
|
|
|
T_RadiusDamage(self, self->activator, (float) self->dmg, nullptr, (float) self->dmg + 40, DAMAGE_NONE, MOD_EXPLOSIVE);
|
|
|
|
save = self->delay;
|
|
self->delay = 0;
|
|
G_UseTargets(self, self->activator);
|
|
self->delay = save;
|
|
}
|
|
|
|
USE(use_target_explosion) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
self->activator = activator;
|
|
|
|
if (!self->delay)
|
|
{
|
|
target_explosion_explode(self);
|
|
return;
|
|
}
|
|
|
|
self->think = target_explosion_explode;
|
|
self->nextthink = level.time + gtime_t::from_sec(self->delay);
|
|
}
|
|
|
|
void SP_target_explosion(edict_t *ent)
|
|
{
|
|
ent->use = use_target_explosion;
|
|
ent->svflags = SVF_NOCLIENT;
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_changelevel (1 0 0) (-8 -8 -8) (8 8 8) END_OF_UNIT UNKNOWN UNKNOWN CLEAR_INVENTORY NO_END_OF_UNIT FADE_OUT IMMEDIATE_LEAVE
|
|
Changes level to "map" when fired
|
|
*/
|
|
USE(use_target_changelevel) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
if (level.intermissiontime)
|
|
return; // already activated
|
|
|
|
if (!deathmatch->integer && !coop->integer)
|
|
{
|
|
if (g_edicts[1].health <= 0)
|
|
return;
|
|
}
|
|
|
|
// if noexit, do a ton of damage to other
|
|
if (deathmatch->integer && !g_dm_allow_exit->integer && other != world)
|
|
{
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin, vec3_origin, 10 * other->max_health, 1000, DAMAGE_NONE, MOD_EXIT);
|
|
return;
|
|
}
|
|
|
|
// if multiplayer, let everyone know who hit the exit
|
|
if (deathmatch->integer)
|
|
{
|
|
if (level.time < 10_sec)
|
|
return;
|
|
|
|
if (activator && activator->client)
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_exited_level", activator->client->pers.netname);
|
|
}
|
|
|
|
// if going to a new unit, clear cross triggers
|
|
if (strstr(self->map, "*"))
|
|
game.cross_level_flags &= ~(SFL_CROSS_TRIGGER_MASK);
|
|
|
|
// if map has a landmark, store position instead of using spawn next map
|
|
if (activator && activator->client && !deathmatch->integer)
|
|
{
|
|
activator->client->landmark_name = nullptr;
|
|
activator->client->landmark_rel_pos = vec3_origin;
|
|
|
|
self->target_ent = G_PickTarget(self->target);
|
|
if (self->target_ent && activator && activator->client)
|
|
{
|
|
activator->client->landmark_name = G_CopyString(self->target_ent->targetname, TAG_GAME);
|
|
|
|
// get relative vector to landmark pos, and unrotate by the landmark angles in preparation to be
|
|
// rotated by the next map
|
|
activator->client->landmark_rel_pos = activator->s.origin - self->target_ent->s.origin;
|
|
|
|
activator->client->landmark_rel_pos = RotatePointAroundVector({ 1, 0, 0 }, activator->client->landmark_rel_pos, -self->target_ent->s.angles[0]);
|
|
activator->client->landmark_rel_pos = RotatePointAroundVector({ 0, 1, 0 }, activator->client->landmark_rel_pos, -self->target_ent->s.angles[2]);
|
|
activator->client->landmark_rel_pos = RotatePointAroundVector({ 0, 0, 1 }, activator->client->landmark_rel_pos, -self->target_ent->s.angles[1]);
|
|
|
|
activator->client->oldvelocity = RotatePointAroundVector({ 1, 0, 0 }, activator->client->oldvelocity, -self->target_ent->s.angles[0]);
|
|
activator->client->oldvelocity = RotatePointAroundVector({ 0, 1, 0 }, activator->client->oldvelocity, -self->target_ent->s.angles[2]);
|
|
activator->client->oldvelocity = RotatePointAroundVector({ 0, 0, 1 }, activator->client->oldvelocity, -self->target_ent->s.angles[1]);
|
|
|
|
// unrotate our view angles for the next map too
|
|
activator->client->oldviewangles = activator->client->ps.viewangles - self->target_ent->s.angles;
|
|
}
|
|
}
|
|
|
|
BeginIntermission(self);
|
|
}
|
|
|
|
void SP_target_changelevel(edict_t *ent)
|
|
{
|
|
if (!ent->map)
|
|
{
|
|
gi.Com_PrintFmt("{}: no map\n", *ent);
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
ent->use = use_target_changelevel;
|
|
ent->svflags = SVF_NOCLIENT;
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_splash (1 0 0) (-8 -8 -8) (8 8 8)
|
|
Creates a particle splash effect when used.
|
|
|
|
Set "sounds" to one of the following:
|
|
1) sparks
|
|
2) blue water
|
|
3) brown water
|
|
4) slime
|
|
5) lava
|
|
6) blood
|
|
|
|
"count" how many pixels in the splash
|
|
"dmg" if set, does a radius damage at this location when it splashes
|
|
useful for lava/sparks
|
|
*/
|
|
|
|
USE(use_target_splash) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_SPLASH);
|
|
gi.WriteByte(self->count);
|
|
gi.WritePosition(self->s.origin);
|
|
gi.WriteDir(self->movedir);
|
|
gi.WriteByte(self->sounds);
|
|
gi.multicast(self->s.origin, MULTICAST_PVS, false);
|
|
|
|
if (self->dmg)
|
|
T_RadiusDamage(self, activator, (float) self->dmg, nullptr, (float) self->dmg + 40, DAMAGE_NONE, MOD_SPLASH);
|
|
}
|
|
|
|
void SP_target_splash(edict_t *self)
|
|
{
|
|
self->use = use_target_splash;
|
|
G_SetMovedir(self->s.angles, self->movedir);
|
|
|
|
if (!self->count)
|
|
self->count = 32;
|
|
|
|
// N64 "sparks" are blue, not yellow.
|
|
if (level.is_n64 && self->sounds == 1)
|
|
self->sounds = 7;
|
|
|
|
self->svflags = SVF_NOCLIENT;
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_spawner (1 0 0) (-8 -8 -8) (8 8 8)
|
|
Set target to the type of entity you want spawned.
|
|
Useful for spawning monsters and gibs in the factory levels.
|
|
|
|
For monsters:
|
|
Set direction to the facing you want it to have.
|
|
|
|
For gibs:
|
|
Set direction if you want it moving and
|
|
speed how fast it should be moving otherwise it
|
|
will just be dropped
|
|
*/
|
|
void ED_CallSpawn(edict_t *ent);
|
|
|
|
USE(use_target_spawner) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
edict_t *ent;
|
|
|
|
ent = G_Spawn();
|
|
ent->classname = self->target;
|
|
// RAFAEL
|
|
ent->flags = self->flags;
|
|
// RAFAEL
|
|
ent->s.origin = self->s.origin;
|
|
ent->s.angles = self->s.angles;
|
|
st = {};
|
|
|
|
// [Paril-KEX] although I fixed these in our maps, this is just
|
|
// in case anybody else does this by accident. Don't count these monsters
|
|
// so they don't inflate the monster count.
|
|
ent->monsterinfo.aiflags |= AI_DO_NOT_COUNT;
|
|
|
|
ED_CallSpawn(ent);
|
|
gi.linkentity(ent);
|
|
|
|
KillBox(ent, false);
|
|
if (self->speed)
|
|
ent->velocity = self->movedir;
|
|
|
|
ent->s.renderfx |= RF_IR_VISIBLE; // PGM
|
|
}
|
|
|
|
void SP_target_spawner(edict_t *self)
|
|
{
|
|
self->use = use_target_spawner;
|
|
self->svflags = SVF_NOCLIENT;
|
|
if (self->speed)
|
|
{
|
|
G_SetMovedir(self->s.angles, self->movedir);
|
|
self->movedir *= self->speed;
|
|
}
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_blaster (1 0 0) (-8 -8 -8) (8 8 8) NOTRAIL NOEFFECTS
|
|
Fires a blaster bolt in the set direction when triggered.
|
|
|
|
dmg default is 15
|
|
speed default is 1000
|
|
*/
|
|
|
|
constexpr spawnflags_t SPAWNFLAG_BLASTER_NOTRAIL = 1_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_BLASTER_NOEFFECTS = 2_spawnflag;
|
|
|
|
USE(use_target_blaster) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
effects_t effect;
|
|
|
|
if (self->spawnflags.has(SPAWNFLAG_BLASTER_NOEFFECTS))
|
|
effect = EF_NONE;
|
|
else if (self->spawnflags.has(SPAWNFLAG_BLASTER_NOTRAIL))
|
|
effect = EF_HYPERBLASTER;
|
|
else
|
|
effect = EF_BLASTER;
|
|
|
|
fire_blaster(self, self->s.origin, self->movedir, self->dmg, (int) self->speed, effect, MOD_TARGET_BLASTER);
|
|
gi.sound(self, CHAN_VOICE, self->noise_index, 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
void SP_target_blaster(edict_t *self)
|
|
{
|
|
self->use = use_target_blaster;
|
|
G_SetMovedir(self->s.angles, self->movedir);
|
|
self->noise_index = gi.soundindex("weapons/laser2.wav");
|
|
|
|
if (!self->dmg)
|
|
self->dmg = 15;
|
|
if (!self->speed)
|
|
self->speed = 1000;
|
|
|
|
self->svflags = SVF_NOCLIENT;
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_crosslevel_trigger (.5 .5 .5) (-8 -8 -8) (8 8 8) trigger1 trigger2 trigger3 trigger4 trigger5 trigger6 trigger7 trigger8
|
|
Once this trigger is touched/used, any trigger_crosslevel_target with the same trigger number is automatically used when a level is started within the same unit. It is OK to check multiple triggers. Message, delay, target, and killtarget also work.
|
|
*/
|
|
USE(trigger_crosslevel_trigger_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
game.cross_level_flags |= self->spawnflags.value;
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
void SP_target_crosslevel_trigger(edict_t *self)
|
|
{
|
|
self->svflags = SVF_NOCLIENT;
|
|
self->use = trigger_crosslevel_trigger_use;
|
|
}
|
|
|
|
/*QUAKED target_crosslevel_target (.5 .5 .5) (-8 -8 -8) (8 8 8) trigger1 trigger2 trigger3 trigger4 trigger5 trigger6 trigger7 trigger8 - - - - - - - - trigger9 trigger10 trigger11 trigger12 trigger13 trigger14 trigger15 trigger16
|
|
Triggered by a trigger_crosslevel elsewhere within a unit. If multiple triggers are checked, all must be true. Delay, target and
|
|
killtarget also work.
|
|
|
|
"delay" delay before using targets if the trigger has been activated (default 1)
|
|
*/
|
|
THINK(target_crosslevel_target_think) (edict_t *self) -> void
|
|
{
|
|
if (self->spawnflags.value == (game.cross_level_flags & SFL_CROSS_TRIGGER_MASK & self->spawnflags.value))
|
|
{
|
|
G_UseTargets(self, self);
|
|
G_FreeEdict(self);
|
|
}
|
|
}
|
|
|
|
void SP_target_crosslevel_target(edict_t *self)
|
|
{
|
|
if (!self->delay)
|
|
self->delay = 1;
|
|
self->svflags = SVF_NOCLIENT;
|
|
|
|
self->think = target_crosslevel_target_think;
|
|
self->nextthink = level.time + gtime_t::from_sec(self->delay);
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_laser (0 .5 .8) (-8 -8 -8) (8 8 8) START_ON RED GREEN BLUE YELLOW ORANGE FAT WINDOWSTOP
|
|
When triggered, fires a laser. You can either set a target or a direction.
|
|
|
|
WINDOWSTOP - stops at CONTENTS_WINDOW
|
|
*/
|
|
|
|
//======
|
|
// PGM
|
|
constexpr spawnflags_t SPAWNFLAG_LASER_STOPWINDOW = 0x0080_spawnflag;
|
|
// PGM
|
|
//======
|
|
|
|
|
|
struct laser_pierce_t : pierce_args_t
|
|
{
|
|
edict_t *self;
|
|
int32_t count;
|
|
bool damaged_thing = false;
|
|
|
|
inline laser_pierce_t(edict_t *self, int32_t count) :
|
|
pierce_args_t(),
|
|
self(self),
|
|
count(count)
|
|
{
|
|
}
|
|
|
|
// we hit an entity; return false to stop the piercing.
|
|
// you can adjust the mask for the re-trace (for water, etc).
|
|
virtual bool hit(contents_t &mask, vec3_t &end) override
|
|
{
|
|
// hurt it if we can
|
|
if (self->dmg > 0 && (tr.ent->takedamage) && !(tr.ent->flags & FL_IMMUNE_LASER) && self->damage_debounce_time <= level.time)
|
|
{
|
|
damaged_thing = true;
|
|
T_Damage(tr.ent, self, self->activator, self->movedir, tr.endpos, vec3_origin, self->dmg, 1, DAMAGE_ENERGY, MOD_TARGET_LASER);
|
|
}
|
|
|
|
// if we hit something that's not a monster or player or is immune to lasers, we're done
|
|
// ROGUE
|
|
if (!(tr.ent->svflags & SVF_MONSTER) && (!tr.ent->client) && !(tr.ent->flags & FL_DAMAGEABLE))
|
|
// ROGUE
|
|
{
|
|
if (self->spawnflags.has(SPAWNFLAG_LASER_ZAP))
|
|
{
|
|
self->spawnflags &= ~SPAWNFLAG_LASER_ZAP;
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_LASER_SPARKS);
|
|
gi.WriteByte(count);
|
|
gi.WritePosition(tr.endpos);
|
|
gi.WriteDir(tr.plane.normal);
|
|
gi.WriteByte(self->s.skinnum);
|
|
gi.multicast(tr.endpos, MULTICAST_PVS, false);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!mark(tr.ent))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
THINK(target_laser_think) (edict_t *self) -> void
|
|
{
|
|
int32_t count;
|
|
|
|
if (self->spawnflags.has(SPAWNFLAG_LASER_ZAP))
|
|
count = 8;
|
|
else
|
|
count = 4;
|
|
|
|
if (self->enemy)
|
|
{
|
|
vec3_t last_movedir = self->movedir;
|
|
vec3_t point = (self->enemy->absmin + self->enemy->absmax) * 0.5f;
|
|
self->movedir = point - self->s.origin;
|
|
self->movedir.normalize();
|
|
if (self->movedir != last_movedir)
|
|
self->spawnflags |= SPAWNFLAG_LASER_ZAP;
|
|
}
|
|
|
|
vec3_t start = self->s.origin;
|
|
vec3_t end = start + (self->movedir * 2048);
|
|
|
|
laser_pierce_t args {
|
|
self,
|
|
count
|
|
};
|
|
|
|
contents_t mask = self->spawnflags.has(SPAWNFLAG_LASER_STOPWINDOW) ? MASK_SHOT : (CONTENTS_SOLID | CONTENTS_MONSTER | CONTENTS_PLAYER | CONTENTS_DEADMONSTER);
|
|
|
|
pierce_trace(start, end, self, args, mask);
|
|
|
|
self->s.old_origin = args.tr.endpos;
|
|
|
|
if (args.damaged_thing)
|
|
self->damage_debounce_time = level.time + 10_hz;
|
|
|
|
self->nextthink = level.time + FRAME_TIME_S;
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
void target_laser_on(edict_t *self)
|
|
{
|
|
if (!self->activator)
|
|
self->activator = self;
|
|
self->spawnflags |= SPAWNFLAG_LASER_ZAP | SPAWNFLAG_LASER_ON;
|
|
self->svflags &= ~SVF_NOCLIENT;
|
|
self->flags |= FL_TRAP;
|
|
target_laser_think(self);
|
|
}
|
|
|
|
void target_laser_off(edict_t *self)
|
|
{
|
|
self->spawnflags &= ~SPAWNFLAG_LASER_ON;
|
|
self->svflags |= SVF_NOCLIENT;
|
|
self->flags &= ~FL_TRAP;
|
|
self->nextthink = 0_ms;
|
|
}
|
|
|
|
USE(target_laser_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
self->activator = activator;
|
|
if (self->spawnflags.has(SPAWNFLAG_LASER_ON))
|
|
target_laser_off(self);
|
|
else
|
|
target_laser_on(self);
|
|
}
|
|
|
|
THINK(target_laser_start) (edict_t *self) -> void
|
|
{
|
|
edict_t *ent;
|
|
|
|
self->movetype = MOVETYPE_NONE;
|
|
self->solid = SOLID_NOT;
|
|
self->s.renderfx |= RF_BEAM;
|
|
self->s.modelindex = MODELINDEX_WORLD; // must be non-zero
|
|
|
|
// [Sam-KEX] On Q2N64, spawnflag of 128 turns it into a lightning bolt
|
|
if (level.is_n64)
|
|
{
|
|
// Paril: fix for N64
|
|
if (self->spawnflags.has(SPAWNFLAG_LASER_STOPWINDOW))
|
|
{
|
|
self->spawnflags &= ~SPAWNFLAG_LASER_STOPWINDOW;
|
|
self->spawnflags |= SPAWNFLAG_LASER_LIGHTNING;
|
|
}
|
|
}
|
|
|
|
if (self->spawnflags.has(SPAWNFLAG_LASER_LIGHTNING))
|
|
{
|
|
self->s.renderfx |= RF_BEAM_LIGHTNING; // tell renderer it is lightning
|
|
|
|
if (!self->s.skinnum)
|
|
self->s.skinnum = 0xf3f3f1f1; // default lightning color
|
|
}
|
|
|
|
// set the beam diameter
|
|
// [Paril-KEX] lab has this set prob before lightning was implemented
|
|
if (!level.is_n64 && self->spawnflags.has(SPAWNFLAG_LASER_FAT))
|
|
self->s.frame = 16;
|
|
else
|
|
self->s.frame = 4;
|
|
|
|
// set the color
|
|
if (!self->s.skinnum)
|
|
{
|
|
if (self->spawnflags.has(SPAWNFLAG_LASER_RED))
|
|
self->s.skinnum = 0xf2f2f0f0;
|
|
else if (self->spawnflags.has(SPAWNFLAG_LASER_GREEN))
|
|
self->s.skinnum = 0xd0d1d2d3;
|
|
else if (self->spawnflags.has(SPAWNFLAG_LASER_BLUE))
|
|
self->s.skinnum = 0xf3f3f1f1;
|
|
else if (self->spawnflags.has(SPAWNFLAG_LASER_YELLOW))
|
|
self->s.skinnum = 0xdcdddedf;
|
|
else if (self->spawnflags.has(SPAWNFLAG_LASER_ORANGE))
|
|
self->s.skinnum = 0xe0e1e2e3;
|
|
}
|
|
|
|
if (!self->enemy)
|
|
{
|
|
if (self->target)
|
|
{
|
|
ent = G_FindByString<&edict_t::targetname>(nullptr, self->target);
|
|
if (!ent)
|
|
gi.Com_PrintFmt("{}: {} is a bad target\n", *self, self->target);
|
|
else
|
|
{
|
|
self->enemy = ent;
|
|
|
|
// N64 fix
|
|
// FIXME: which map was this for again? oops
|
|
if (level.is_n64 && !strcmp(self->enemy->classname, "func_train") && !(self->enemy->spawnflags & SPAWNFLAG_TRAIN_START_ON))
|
|
self->enemy->use(self->enemy, self, self);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
G_SetMovedir(self->s.angles, self->movedir);
|
|
}
|
|
}
|
|
self->use = target_laser_use;
|
|
self->think = target_laser_think;
|
|
|
|
if (!self->dmg)
|
|
self->dmg = 1;
|
|
|
|
self->mins = { -8, -8, -8 };
|
|
self->maxs = { 8, 8, 8 };
|
|
gi.linkentity(self);
|
|
|
|
if (self->spawnflags.has(SPAWNFLAG_LASER_ON))
|
|
target_laser_on(self);
|
|
else
|
|
target_laser_off(self);
|
|
}
|
|
|
|
void SP_target_laser(edict_t *self)
|
|
{
|
|
// let everything else get spawned before we start firing
|
|
self->think = target_laser_start;
|
|
self->flags |= FL_TRAP_LASER_FIELD;
|
|
self->nextthink = level.time + 1_sec;
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_lightramp (0 .5 .8) (-8 -8 -8) (8 8 8) TOGGLE
|
|
speed How many seconds the ramping will take
|
|
message two letters; starting lightlevel and ending lightlevel
|
|
*/
|
|
|
|
constexpr spawnflags_t SPAWNFLAG_LIGHTRAMP_TOGGLE = 1_spawnflag;
|
|
|
|
THINK(target_lightramp_think) (edict_t *self) -> void
|
|
{
|
|
char style[2];
|
|
|
|
style[0] = (char) ('a' + self->movedir[0] + ((level.time - self->timestamp) / gi.frame_time_s).seconds() * self->movedir[2]);
|
|
style[1] = 0;
|
|
|
|
gi.configstring(CS_LIGHTS + self->enemy->style, style);
|
|
|
|
if ((level.time - self->timestamp).seconds() < self->speed)
|
|
{
|
|
self->nextthink = level.time + FRAME_TIME_S;
|
|
}
|
|
else if (self->spawnflags.has(SPAWNFLAG_LIGHTRAMP_TOGGLE))
|
|
{
|
|
char temp;
|
|
|
|
temp = (char) self->movedir[0];
|
|
self->movedir[0] = self->movedir[1];
|
|
self->movedir[1] = temp;
|
|
self->movedir[2] *= -1;
|
|
}
|
|
}
|
|
|
|
USE(target_lightramp_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
if (!self->enemy)
|
|
{
|
|
edict_t *e;
|
|
|
|
// check all the targets
|
|
e = nullptr;
|
|
while (1)
|
|
{
|
|
e = G_FindByString<&edict_t::targetname>(e, self->target);
|
|
if (!e)
|
|
break;
|
|
if (strcmp(e->classname, "light") != 0)
|
|
{
|
|
gi.Com_PrintFmt("{}: target {} ({}) is not a light\n", *self, self->target, *e);
|
|
}
|
|
else
|
|
{
|
|
self->enemy = e;
|
|
}
|
|
}
|
|
|
|
if (!self->enemy)
|
|
{
|
|
gi.Com_PrintFmt("{}: target {} not found\n", *self, self->target);
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
self->timestamp = level.time;
|
|
target_lightramp_think(self);
|
|
}
|
|
|
|
void SP_target_lightramp(edict_t *self)
|
|
{
|
|
if (!self->message || strlen(self->message) != 2 || self->message[0] < 'a' || self->message[0] > 'z' || self->message[1] < 'a' || self->message[1] > 'z' || self->message[0] == self->message[1])
|
|
{
|
|
gi.Com_PrintFmt("{}: bad ramp ({})\n", *self, self->message ? self->message : "null string");
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (deathmatch->integer)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (!self->target)
|
|
{
|
|
gi.Com_PrintFmt("{}: no target\n", *self);
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->svflags |= SVF_NOCLIENT;
|
|
self->use = target_lightramp_use;
|
|
self->think = target_lightramp_think;
|
|
|
|
self->movedir[0] = (float) (self->message[0] - 'a');
|
|
self->movedir[1] = (float) (self->message[1] - 'a');
|
|
self->movedir[2] = (self->movedir[1] - self->movedir[0]) / (self->speed / gi.frame_time_s);
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_earthquake (1 0 0) (-8 -8 -8) (8 8 8) SILENT TOGGLE UNKNOWN_ROGUE ONE_SHOT
|
|
When triggered, this initiates a level-wide earthquake.
|
|
All players are affected with a screen shake.
|
|
"speed" severity of the quake (default:200)
|
|
"count" duration of the quake (default:5)
|
|
*/
|
|
|
|
constexpr spawnflags_t SPAWNFLAGS_EARTHQUAKE_SILENT = 1_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAGS_EARTHQUAKE_TOGGLE = 2_spawnflag;
|
|
[[maybe_unused]] constexpr spawnflags_t SPAWNFLAGS_EARTHQUAKE_UNKNOWN_ROGUE = 4_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAGS_EARTHQUAKE_ONE_SHOT = 8_spawnflag;
|
|
|
|
THINK(target_earthquake_think) (edict_t *self) -> void
|
|
{
|
|
uint32_t i;
|
|
edict_t *e;
|
|
|
|
if (!(self->spawnflags & SPAWNFLAGS_EARTHQUAKE_SILENT)) // PGM
|
|
{ // PGM
|
|
if (self->last_move_time < level.time)
|
|
{
|
|
gi.positioned_sound(self->s.origin, self, CHAN_VOICE, self->noise_index, 1.0, ATTN_NONE, 0);
|
|
self->last_move_time = level.time + 6.5_sec;
|
|
}
|
|
} // PGM
|
|
|
|
for (i = 1, e = g_edicts + i; i < globals.num_edicts; i++, e++)
|
|
{
|
|
if (!e->inuse)
|
|
continue;
|
|
if (!e->client)
|
|
break;
|
|
|
|
e->client->quake_time = level.time + 1000_ms;
|
|
}
|
|
|
|
if (level.time < self->timestamp)
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
USE(target_earthquake_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
if (self->spawnflags.has(SPAWNFLAGS_EARTHQUAKE_ONE_SHOT))
|
|
{
|
|
uint32_t i;
|
|
edict_t *e;
|
|
|
|
for (i = 1, e = g_edicts + i; i < globals.num_edicts; i++, e++)
|
|
{
|
|
if (!e->inuse)
|
|
continue;
|
|
if (!e->client)
|
|
break;
|
|
|
|
e->client->v_dmg_pitch = -self->speed * 0.1f;
|
|
e->client->v_dmg_time = level.time + DAMAGE_TIME();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
self->timestamp = level.time + gtime_t::from_sec(self->count);
|
|
|
|
if (self->spawnflags.has(SPAWNFLAGS_EARTHQUAKE_TOGGLE))
|
|
{
|
|
if (self->style)
|
|
self->nextthink = 0_ms;
|
|
else
|
|
self->nextthink = level.time + FRAME_TIME_S;
|
|
|
|
self->style = !self->style;
|
|
}
|
|
else
|
|
{
|
|
self->nextthink = level.time + FRAME_TIME_S;
|
|
self->last_move_time = 0_ms;
|
|
}
|
|
|
|
self->activator = activator;
|
|
}
|
|
|
|
void SP_target_earthquake(edict_t *self)
|
|
{
|
|
if (!self->targetname)
|
|
gi.Com_PrintFmt("{}: untargeted\n", *self);
|
|
|
|
if (level.is_n64)
|
|
{
|
|
self->spawnflags |= SPAWNFLAGS_EARTHQUAKE_TOGGLE;
|
|
self->speed = 5;
|
|
}
|
|
|
|
if (!self->count)
|
|
self->count = 5;
|
|
|
|
if (!self->speed)
|
|
self->speed = 200;
|
|
|
|
self->svflags |= SVF_NOCLIENT;
|
|
self->think = target_earthquake_think;
|
|
self->use = target_earthquake_use;
|
|
|
|
if (!(self->spawnflags & SPAWNFLAGS_EARTHQUAKE_SILENT)) // PGM
|
|
self->noise_index = gi.soundindex("world/quake.wav");
|
|
}
|
|
|
|
/*QUAKED target_camera (1 0 0) (-8 -8 -8) (8 8 8)
|
|
[Sam-KEX] Creates a camera path as seen in the N64 version.
|
|
*/
|
|
|
|
constexpr size_t HACKFLAG_TELEPORT_OUT = 2;
|
|
constexpr size_t HACKFLAG_SKIPPABLE = 64;
|
|
constexpr size_t HACKFLAG_END_OF_UNIT = 128;
|
|
|
|
static void camera_lookat_pathtarget(edict_t* self, vec3_t origin, vec3_t* dest)
|
|
{
|
|
if(self->pathtarget)
|
|
{
|
|
edict_t* pt = nullptr;
|
|
pt = G_FindByString<&edict_t::targetname>(pt, self->pathtarget);
|
|
if(pt)
|
|
{
|
|
float yaw, pitch;
|
|
vec3_t delta = pt->s.origin - origin;
|
|
|
|
float d = delta[0] * delta[0] + delta[1] * delta[1];
|
|
if(d == 0.0f)
|
|
{
|
|
yaw = 0.0f;
|
|
pitch = (delta[2] > 0.0f) ? 90.0f : -90.0f;
|
|
}
|
|
else
|
|
{
|
|
yaw = atan2(delta[1], delta[0]) * (180.0f / PIf);
|
|
pitch = atan2(delta[2], sqrt(d)) * (180.0f / PIf);
|
|
}
|
|
|
|
(*dest)[YAW] = yaw;
|
|
(*dest)[PITCH] = -pitch;
|
|
(*dest)[ROLL] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
THINK(update_target_camera) (edict_t *self) -> void
|
|
{
|
|
bool do_skip = false;
|
|
|
|
// only allow skipping after 2 seconds
|
|
if ((self->hackflags & HACKFLAG_SKIPPABLE) && level.time > 2_sec)
|
|
{
|
|
for (uint32_t i = 0; i < game.maxclients; i++)
|
|
{
|
|
edict_t *client = g_edicts + 1 + i;
|
|
if (!client->inuse || !client->client->pers.connected)
|
|
continue;
|
|
|
|
if (client->client->buttons & BUTTON_ANY)
|
|
{
|
|
do_skip = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!do_skip && self->movetarget)
|
|
{
|
|
self->moveinfo.remaining_distance -= (self->moveinfo.move_speed * gi.frame_time_s) * 0.8f;
|
|
|
|
if(self->moveinfo.remaining_distance <= 0)
|
|
{
|
|
if (self->movetarget->hackflags & HACKFLAG_TELEPORT_OUT)
|
|
{
|
|
if (self->enemy)
|
|
{
|
|
self->enemy->s.event = EV_PLAYER_TELEPORT;
|
|
self->enemy->hackflags = HACKFLAG_TELEPORT_OUT;
|
|
self->enemy->pain_debounce_time = self->enemy->timestamp = gtime_t::from_sec(self->movetarget->wait);
|
|
}
|
|
}
|
|
|
|
self->s.origin = self->movetarget->s.origin;
|
|
self->nextthink = level.time + gtime_t::from_sec(self->movetarget->wait);
|
|
if (self->movetarget->target)
|
|
{
|
|
self->movetarget = G_PickTarget(self->movetarget->target);
|
|
|
|
if (self->movetarget)
|
|
{
|
|
self->moveinfo.move_speed = self->movetarget->speed ? self->movetarget->speed : 55;
|
|
self->moveinfo.remaining_distance = (self->movetarget->s.origin - self->s.origin).normalize();
|
|
self->moveinfo.distance = self->moveinfo.remaining_distance;
|
|
}
|
|
}
|
|
else
|
|
self->movetarget = nullptr;
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
float frac = 1.0f - (self->moveinfo.remaining_distance / self->moveinfo.distance);
|
|
|
|
if (self->enemy && (self->enemy->hackflags & HACKFLAG_TELEPORT_OUT))
|
|
self->enemy->s.alpha = max(1.f / 255.f, frac);
|
|
|
|
vec3_t delta = self->movetarget->s.origin - self->s.origin;
|
|
delta *= frac;
|
|
vec3_t newpos = self->s.origin + delta;
|
|
|
|
camera_lookat_pathtarget(self, newpos, &level.intermission_angle);
|
|
level.intermission_origin = newpos;
|
|
|
|
// move all clients to the intermission point
|
|
for (uint32_t i = 0; i < game.maxclients; i++)
|
|
{
|
|
edict_t* client = g_edicts + 1 + i;
|
|
if (!client->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
MoveClientToIntermission(client);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (self->killtarget)
|
|
{
|
|
// destroy dummy player
|
|
if (self->enemy)
|
|
G_FreeEdict(self->enemy);
|
|
|
|
edict_t* t = nullptr;
|
|
level.intermissiontime = 0_ms;
|
|
level.level_intermission_set = true;
|
|
|
|
while ((t = G_FindByString<&edict_t::targetname>(t, self->killtarget)))
|
|
{
|
|
t->use(t, self, self->activator);
|
|
}
|
|
|
|
level.intermissiontime = level.time;
|
|
level.intermission_server_frame = gi.ServerFrame();
|
|
|
|
// end of unit requires a wait
|
|
if (level.changemap && !strchr(level.changemap, '*'))
|
|
level.exitintermission = true;
|
|
}
|
|
|
|
self->think = nullptr;
|
|
return;
|
|
}
|
|
|
|
self->nextthink = level.time + FRAME_TIME_S;
|
|
}
|
|
|
|
void G_SetClientFrame(edict_t *ent);
|
|
|
|
extern float xyspeed;
|
|
|
|
THINK(target_camera_dummy_think) (edict_t *self) -> void
|
|
{
|
|
// bit of a hack, but this will let the dummy
|
|
// move like a player
|
|
self->client = self->owner->client;
|
|
xyspeed = sqrtf(self->velocity[0] * self->velocity[0] + self->velocity[1] * self->velocity[1]);
|
|
G_SetClientFrame(self);
|
|
self->client = nullptr;
|
|
|
|
// alpha fade out for voops
|
|
if (self->hackflags & HACKFLAG_TELEPORT_OUT)
|
|
{
|
|
self->timestamp = max(0_ms, self->timestamp - 10_hz);
|
|
self->s.alpha = max(1.f / 255.f, (self->timestamp.seconds() / self->pain_debounce_time.seconds()));
|
|
}
|
|
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
USE(use_target_camera) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
if (self->sounds)
|
|
gi.configstring (CS_CDTRACK, G_Fmt("{}", self->sounds).data() );
|
|
|
|
if (!self->target)
|
|
return;
|
|
|
|
self->movetarget = G_PickTarget(self->target);
|
|
|
|
if (!self->movetarget)
|
|
return;
|
|
|
|
level.intermissiontime = level.time;
|
|
level.intermission_server_frame = gi.ServerFrame();
|
|
level.exitintermission = 0;
|
|
|
|
// spawn fake player dummy where we were
|
|
if (activator->client)
|
|
{
|
|
edict_t *dummy = self->enemy = G_Spawn();
|
|
dummy->owner = activator;
|
|
dummy->clipmask = activator->clipmask;
|
|
dummy->s.origin = activator->s.origin;
|
|
dummy->s.angles = activator->s.angles;
|
|
dummy->groundentity = activator->groundentity;
|
|
dummy->groundentity_linkcount = dummy->groundentity ? dummy->groundentity->linkcount : 0;
|
|
dummy->think = target_camera_dummy_think;
|
|
dummy->nextthink = level.time + 10_hz;
|
|
dummy->solid = SOLID_BBOX;
|
|
dummy->movetype = MOVETYPE_STEP;
|
|
dummy->mins = activator->mins;
|
|
dummy->maxs = activator->maxs;
|
|
dummy->s.modelindex = dummy->s.modelindex2 = MODELINDEX_PLAYER;
|
|
dummy->s.skinnum = activator->s.skinnum;
|
|
dummy->velocity = activator->velocity;
|
|
dummy->s.renderfx = RF_MINLIGHT;
|
|
dummy->s.frame = activator->s.frame;
|
|
gi.linkentity(dummy);
|
|
}
|
|
|
|
camera_lookat_pathtarget(self, self->s.origin, &level.intermission_angle);
|
|
level.intermission_origin = self->s.origin;
|
|
|
|
// move all clients to the intermission point
|
|
for (uint32_t i = 0; i < game.maxclients; i++)
|
|
{
|
|
edict_t* client = g_edicts + 1 + i;
|
|
if (!client->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// respawn any dead clients
|
|
if (client->health <= 0)
|
|
{
|
|
// give us our max health back since it will reset
|
|
// to pers.health; in instanced items we'd lose the items
|
|
// we touched so we always want to respawn with our max.
|
|
if (P_UseCoopInstancedItems())
|
|
client->client->pers.health = client->client->pers.max_health = client->max_health;
|
|
|
|
respawn(client);
|
|
}
|
|
|
|
MoveClientToIntermission(client);
|
|
}
|
|
|
|
self->activator = activator;
|
|
self->think = update_target_camera;
|
|
self->nextthink = level.time + gtime_t::from_sec(self->wait);
|
|
self->moveinfo.move_speed = self->speed;
|
|
|
|
self->moveinfo.remaining_distance = (self->movetarget->s.origin - self->s.origin).normalize();
|
|
self->moveinfo.distance = self->moveinfo.remaining_distance;
|
|
|
|
if (self->hackflags & HACKFLAG_END_OF_UNIT)
|
|
G_EndOfUnitMessage();
|
|
}
|
|
|
|
void SP_target_camera(edict_t* self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{ // auto-remove for deathmatch
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->use = use_target_camera;
|
|
self->svflags = SVF_NOCLIENT;
|
|
}
|
|
|
|
/*QUAKED target_gravity (1 0 0) (-8 -8 -8) (8 8 8) NOTRAIL NOEFFECTS
|
|
[Sam-KEX] Changes gravity, as seen in the N64 version
|
|
*/
|
|
|
|
USE(use_target_gravity) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
gi.cvar_set("sv_gravity", G_Fmt("{}", self->gravity).data());
|
|
level.gravity = self->gravity;
|
|
}
|
|
|
|
void SP_target_gravity(edict_t* self)
|
|
{
|
|
self->use = use_target_gravity;
|
|
self->gravity = atof(st.gravity);
|
|
}
|
|
|
|
/*QUAKED target_soundfx (1 0 0) (-8 -8 -8) (8 8 8) NOTRAIL NOEFFECTS
|
|
[Sam-KEX] Plays a sound fx, as seen in the N64 version
|
|
*/
|
|
|
|
THINK(update_target_soundfx) (edict_t *self) -> void
|
|
{
|
|
gi.positioned_sound(self->s.origin, self, CHAN_VOICE, self->noise_index, self->volume, self->attenuation, 0);
|
|
}
|
|
|
|
USE(use_target_soundfx) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
self->think = update_target_soundfx;
|
|
self->nextthink = level.time + gtime_t::from_sec(self->delay);
|
|
}
|
|
|
|
void SP_target_soundfx(edict_t* self)
|
|
{
|
|
if (!self->volume)
|
|
self->volume = 1.0;
|
|
|
|
if (!self->attenuation)
|
|
self->attenuation = 1.0;
|
|
else if (self->attenuation == -1) // use -1 so 0 defaults to 1
|
|
self->attenuation = 0;
|
|
|
|
self->noise_index = atoi(st.noise);
|
|
|
|
switch(self->noise_index)
|
|
{
|
|
case 1:
|
|
self->noise_index = gi.soundindex("world/x_alarm.wav");
|
|
break;
|
|
case 2:
|
|
self->noise_index = gi.soundindex("world/flyby1.wav");
|
|
break;
|
|
case 4:
|
|
self->noise_index = gi.soundindex("world/amb12.wav");
|
|
break;
|
|
case 5:
|
|
self->noise_index = gi.soundindex("world/amb17.wav");
|
|
break;
|
|
case 7:
|
|
self->noise_index = gi.soundindex("world/bigpump2.wav");
|
|
break;
|
|
default:
|
|
gi.Com_PrintFmt("{}: unknown noise {}\n", *self, self->noise_index);
|
|
return;
|
|
}
|
|
|
|
self->use = use_target_soundfx;
|
|
}
|
|
|
|
/*QUAKED target_light (1 0 0) (-8 -8 -8) (8 8 8) START_ON NO_LERP FLICKER
|
|
[Paril-KEX] dynamic light entity that follows a lightstyle.
|
|
|
|
*/
|
|
|
|
constexpr spawnflags_t SPAWNFLAG_TARGET_LIGHT_START_ON = 1_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_TARGET_LIGHT_NO_LERP = 2_spawnflag; // not used in N64, but I'll use it for this
|
|
constexpr spawnflags_t SPAWNFLAG_TARGET_LIGHT_FLICKER = 4_spawnflag;
|
|
|
|
THINK(target_light_flicker_think) (edict_t *self) -> void
|
|
{
|
|
if (brandom())
|
|
self->svflags ^= SVF_NOCLIENT;
|
|
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
// think function handles interpolation from start to finish.
|
|
THINK(target_light_think) (edict_t *self) -> void
|
|
{
|
|
if (self->spawnflags.has(SPAWNFLAG_TARGET_LIGHT_FLICKER))
|
|
target_light_flicker_think(self);
|
|
|
|
const char *style = gi.get_configstring(CS_LIGHTS + self->style);
|
|
self->delay += self->speed;
|
|
|
|
int32_t index = ((int32_t) self->delay) % strlen(style);
|
|
char style_value = style[index];
|
|
float current_lerp = (float) (style_value - 'a') / (float) ('z' - 'a');
|
|
float lerp;
|
|
|
|
if (!(self->spawnflags & SPAWNFLAG_TARGET_LIGHT_NO_LERP))
|
|
{
|
|
int32_t next_index = (index + 1) % strlen(style);
|
|
char next_style_value = style[next_index];
|
|
|
|
float next_lerp = (float) (next_style_value - 'a') / (float) ('z' - 'a');
|
|
|
|
float mod_lerp = fmod(self->delay, 1.0f);
|
|
lerp = (next_lerp * mod_lerp) + (current_lerp * (1.f - mod_lerp));
|
|
}
|
|
else
|
|
lerp = current_lerp;
|
|
|
|
int my_rgb = self->count;
|
|
int target_rgb = self->chain->s.skinnum;
|
|
|
|
int my_b = ((my_rgb >> 8 ) & 0xff);
|
|
int my_g = ((my_rgb >> 16) & 0xff);
|
|
int my_r = ((my_rgb >> 24) & 0xff);
|
|
|
|
int target_b = ((target_rgb >> 8 ) & 0xff);
|
|
int target_g = ((target_rgb >> 16) & 0xff);
|
|
int target_r = ((target_rgb >> 24) & 0xff);
|
|
|
|
float backlerp = 1.0f - lerp;
|
|
|
|
int b = (target_b * lerp) + (my_b * backlerp);
|
|
int g = (target_g * lerp) + (my_g * backlerp);
|
|
int r = (target_r * lerp) + (my_r * backlerp);
|
|
|
|
self->s.skinnum = (b << 8) | (g << 16) | (r << 24);
|
|
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
USE(target_light_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
self->health = !self->health;
|
|
|
|
if (self->health)
|
|
self->svflags &= ~SVF_NOCLIENT;
|
|
else
|
|
self->svflags |= SVF_NOCLIENT;
|
|
|
|
if (!self->health)
|
|
{
|
|
self->think = nullptr;
|
|
self->nextthink = 0_ms;
|
|
return;
|
|
}
|
|
|
|
// has dynamic light "target"
|
|
if (self->chain)
|
|
{
|
|
self->think = target_light_think;
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
else if (self->spawnflags.has(SPAWNFLAG_TARGET_LIGHT_FLICKER))
|
|
{
|
|
self->think = target_light_flicker_think;
|
|
self->nextthink = level.time + 10_hz;
|
|
}
|
|
}
|
|
|
|
void SP_target_light(edict_t *self)
|
|
{
|
|
self->s.modelindex = 1;
|
|
self->s.renderfx = RF_CUSTOM_LIGHT;
|
|
self->s.frame = st.radius ? st.radius : 150;
|
|
self->count = self->s.skinnum;
|
|
self->svflags |= SVF_NOCLIENT;
|
|
self->health = 0;
|
|
|
|
if (self->target)
|
|
self->chain = G_PickTarget(self->target);
|
|
|
|
if (self->spawnflags.has(SPAWNFLAG_TARGET_LIGHT_START_ON))
|
|
target_light_use(self, self, self);
|
|
|
|
if (!self->speed)
|
|
self->speed = 1.0f;
|
|
else
|
|
self->speed = 0.1f / self->speed;
|
|
|
|
if (level.is_n64)
|
|
self->style += 10;
|
|
|
|
self->use = target_light_use;
|
|
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
/*QUAKED target_poi (1 0 0) (-4 -4 -4) (4 4 4) NEAREST DUMMY DYNAMIC
|
|
[Paril-KEX] point of interest for help in player navigation.
|
|
Without any additional setup, targeting this entity will switch
|
|
the current POI in the level to the one this is linked to.
|
|
|
|
"count": if set, this value is the 'stage' linked to this POI. A POI
|
|
with this set that is activated will only take effect if the current
|
|
level's stage value is <= this value, and if it is, will also set
|
|
the current level's stage value to this value.
|
|
|
|
"style": only used for teamed POIs; the POI with the lowest style will
|
|
be activated when checking for which POI to activate. This is mainly
|
|
useful during development, to easily insert or change the order of teamed
|
|
POIs without needing to manually move the entity definitions around.
|
|
|
|
"team": if set, this will create a team of POIs. Teamed POIs act like
|
|
a single unit; activating any of them will do the same thing. When activated,
|
|
it will filter through all of the POIs on the team selecting the one that
|
|
best fits the current situation. This includes checking "count" and "style"
|
|
values. You can also set the NEAREST spawnflag on any of the teamed POIs,
|
|
which will additionally cause activation to prefer the nearest one to the player.
|
|
Killing a POI via killtarget will remove it from the chain, allowing you to
|
|
adjust valid POIs at runtime.
|
|
|
|
The DUMMY spawnflag is to allow you to use a single POI as a team member
|
|
that can be activated, if you're using killtargets to remove POIs.
|
|
|
|
The DYNAMIC spawnflag is for very specific circumstances where you want
|
|
to direct the player to the nearest teamed POI, but want the path to pick
|
|
the nearest at any given time rather than only when activated.
|
|
|
|
The DISABLED flag is mainly intended to work with DYNAMIC & teams; the POI
|
|
will be disabled until it is targeted, and afterwards will be enabled until
|
|
it is killed.
|
|
*/
|
|
|
|
constexpr spawnflags_t SPAWNFLAG_POI_NEAREST = 1_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_POI_DUMMY = 2_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_POI_DYNAMIC = 4_spawnflag;
|
|
constexpr spawnflags_t SPAWNFLAG_POI_DISABLED = 8_spawnflag;
|
|
|
|
static float distance_to_poi(vec3_t start, vec3_t end)
|
|
{
|
|
PathRequest request;
|
|
request.start = start;
|
|
request.goal = end;
|
|
request.moveDist = 64.f;
|
|
request.pathFlags = PathFlags::All;
|
|
request.nodeSearch.ignoreNodeFlags = true;
|
|
request.nodeSearch.minHeight = 128.0f;
|
|
request.nodeSearch.maxHeight = 128.0f;
|
|
request.nodeSearch.radius = 1024.0f;
|
|
request.pathPoints.count = 0;
|
|
|
|
PathInfo info;
|
|
|
|
if (gi.GetPathToGoal(request, info))
|
|
return info.pathDistSqr;
|
|
|
|
if (info.returnCode == PathReturnCode::NoNavAvailable)
|
|
return (end - start).lengthSquared();
|
|
|
|
return std::numeric_limits<float>::infinity();
|
|
}
|
|
|
|
USE(target_poi_use) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
// we were disabled, so remove the disable check
|
|
if (ent->spawnflags.has(SPAWNFLAG_POI_DISABLED))
|
|
ent->spawnflags &= ~SPAWNFLAG_POI_DISABLED;
|
|
|
|
// early stage check
|
|
if (ent->count && level.current_poi_stage > ent->count)
|
|
return;
|
|
|
|
// teamed POIs work a bit differently
|
|
if (ent->team)
|
|
{
|
|
edict_t *poi_master = ent->teammaster;
|
|
|
|
// unset ent, since we need to find one that matches
|
|
ent = nullptr;
|
|
|
|
float best_distance = std::numeric_limits<float>::infinity();
|
|
int32_t best_style = std::numeric_limits<int32_t>::max();
|
|
|
|
edict_t *dummy_fallback = nullptr;
|
|
|
|
for (edict_t *poi = poi_master; poi; poi = poi->teamchain)
|
|
{
|
|
// currently disabled
|
|
if (poi->spawnflags.has(SPAWNFLAG_POI_DISABLED))
|
|
continue;
|
|
|
|
// ignore dummy POI
|
|
if (poi->spawnflags.has(SPAWNFLAG_POI_DUMMY))
|
|
{
|
|
dummy_fallback = poi;
|
|
continue;
|
|
}
|
|
// POI is not part of current stage
|
|
else if (poi->count && level.current_poi_stage > poi->count)
|
|
continue;
|
|
// POI isn't the right style
|
|
else if (poi->style > best_style)
|
|
continue;
|
|
|
|
float dist = distance_to_poi(activator->s.origin, poi->s.origin);
|
|
|
|
// we have one already and it's farther away, don't bother
|
|
if (poi_master->spawnflags.has(SPAWNFLAG_POI_NEAREST) &&
|
|
ent &&
|
|
dist > best_distance)
|
|
continue;
|
|
|
|
// found a better style; overwrite dist
|
|
if (poi->style < best_style)
|
|
{
|
|
// unless we weren't reachable...
|
|
if (poi_master->spawnflags.has(SPAWNFLAG_POI_NEAREST) && std::isinf(dist))
|
|
continue;
|
|
|
|
best_style = poi->style;
|
|
if (poi_master->spawnflags.has(SPAWNFLAG_POI_NEAREST))
|
|
best_distance = dist;
|
|
ent = poi;
|
|
continue;
|
|
}
|
|
|
|
// if we're picking by nearest, check distance
|
|
if (poi_master->spawnflags.has(SPAWNFLAG_POI_NEAREST))
|
|
{
|
|
if (dist < best_distance)
|
|
{
|
|
best_distance = dist;
|
|
ent = poi;
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// not picking by distance, so it's order of appearance
|
|
ent = poi;
|
|
}
|
|
}
|
|
|
|
// no valid POI found; this isn't always an error,
|
|
// some valid techniques may require this to happen.
|
|
if (!ent)
|
|
{
|
|
if (dummy_fallback && dummy_fallback->spawnflags.has(SPAWNFLAG_POI_DYNAMIC))
|
|
ent = dummy_fallback;
|
|
else
|
|
return;
|
|
}
|
|
|
|
// copy over POI stage value
|
|
if (ent->count)
|
|
{
|
|
if (level.current_poi_stage <= ent->count)
|
|
level.current_poi_stage = ent->count;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ent->count)
|
|
{
|
|
if (level.current_poi_stage <= ent->count)
|
|
level.current_poi_stage = ent->count;
|
|
else
|
|
return; // this POI is not part of our current stage
|
|
}
|
|
}
|
|
|
|
// dummy POI; not valid
|
|
if (!strcmp(ent->classname, "target_poi") && ent->spawnflags.has(SPAWNFLAG_POI_DUMMY) && !ent->spawnflags.has(SPAWNFLAG_POI_DYNAMIC))
|
|
return;
|
|
|
|
level.valid_poi = true;
|
|
level.current_poi = ent->s.origin;
|
|
level.current_poi_image = ent->noise_index;
|
|
|
|
if (!strcmp(ent->classname, "target_poi") && ent->spawnflags.has(SPAWNFLAG_POI_DYNAMIC))
|
|
{
|
|
level.current_dynamic_poi = nullptr;
|
|
|
|
// pick the dummy POI, since it isn't supposed to get freed
|
|
// FIXME maybe store the team string instead?
|
|
|
|
for (edict_t *m = ent->teammaster; m; m = m->teamchain)
|
|
if (m->spawnflags.has(SPAWNFLAG_POI_DUMMY))
|
|
{
|
|
level.current_dynamic_poi = m;
|
|
break;
|
|
}
|
|
|
|
if (!level.current_dynamic_poi)
|
|
gi.Com_PrintFmt("can't activate poi for {}; need DUMMY in chain\n", *ent);
|
|
}
|
|
else
|
|
level.current_dynamic_poi = nullptr;
|
|
}
|
|
|
|
THINK(target_poi_setup) (edict_t *self) -> void
|
|
{
|
|
if (self->team)
|
|
{
|
|
// copy dynamic/nearest over to all teammates
|
|
if (self->spawnflags.has((SPAWNFLAG_POI_NEAREST | SPAWNFLAG_POI_DYNAMIC)))
|
|
for (edict_t *m = self->teammaster; m; m = m->teamchain)
|
|
m->spawnflags |= self->spawnflags & (SPAWNFLAG_POI_NEAREST | SPAWNFLAG_POI_DYNAMIC);
|
|
|
|
for (edict_t *m = self->teammaster; m; m = m->teamchain)
|
|
{
|
|
if (strcmp(m->classname, "target_poi"))
|
|
gi.Com_PrintFmt("WARNING: {} is teamed with target_poi's; unintentional\n", *m);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SP_target_poi(edict_t *self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{ // auto-remove for deathmatch
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (st.image)
|
|
self->noise_index = gi.imageindex(st.image);
|
|
else
|
|
self->noise_index = gi.imageindex("friend");
|
|
|
|
self->use = target_poi_use;
|
|
self->svflags |= SVF_NOCLIENT;
|
|
self->think = target_poi_setup;
|
|
self->nextthink = level.time + 1_ms;
|
|
|
|
if (!self->team)
|
|
{
|
|
if (self->spawnflags.has(SPAWNFLAG_POI_NEAREST))
|
|
gi.Com_PrintFmt("{} has useless spawnflag 'NEAREST'\n", *self);
|
|
if (self->spawnflags.has(SPAWNFLAG_POI_DYNAMIC))
|
|
gi.Com_PrintFmt("{} has useless spawnflag 'DYNAMIC'\n", *self);
|
|
}
|
|
}
|
|
|
|
/*QUAKED target_music (1 0 0) (-8 -8 -8) (8 8 8)
|
|
Change music when used
|
|
*/
|
|
|
|
USE(use_target_music) (edict_t* ent, edict_t* other, edict_t* activator) -> void
|
|
{
|
|
gi.configstring(CS_CDTRACK, G_Fmt("{}", ent->sounds).data());
|
|
}
|
|
|
|
void SP_target_music(edict_t* self)
|
|
{
|
|
self->use = use_target_music;
|
|
}
|
|
|
|
/*QUAKED target_healthbar (0 1 0) (-8 -8 -8) (8 8 8) PVS_ONLY
|
|
*
|
|
* Hook up health bars to monsters.
|
|
* "delay" is how long to show the health bar for after death.
|
|
* "message" is their name
|
|
*/
|
|
|
|
USE(use_target_healthbar) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
edict_t *target = G_PickTarget(ent->target);
|
|
|
|
if (!target || ent->health != target->spawn_count)
|
|
{
|
|
if (target)
|
|
gi.Com_PrintFmt("{}: target {} changed from what it used to be\n", *ent, *target);
|
|
else
|
|
gi.Com_PrintFmt("{}: no target\n", *ent);
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < MAX_HEALTH_BARS; i++)
|
|
{
|
|
if (level.health_bar_entities[i])
|
|
continue;
|
|
|
|
ent->enemy = target;
|
|
level.health_bar_entities[i] = ent;
|
|
gi.configstring(CONFIG_HEALTH_BAR_NAME, ent->message);
|
|
return;
|
|
}
|
|
|
|
gi.Com_PrintFmt("{}: too many health bars\n", *ent);
|
|
G_FreeEdict(ent);
|
|
}
|
|
|
|
THINK(check_target_healthbar) (edict_t *ent) -> void
|
|
{
|
|
edict_t *target = G_PickTarget(ent->target);
|
|
if (!target || !(target->svflags & SVF_MONSTER))
|
|
{
|
|
if ( target != nullptr ) {
|
|
gi.Com_PrintFmt( "{}: target {} does not appear to be a monster\n", *ent, *target );
|
|
}
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
// just for sanity check
|
|
ent->health = target->spawn_count;
|
|
}
|
|
|
|
void SP_target_healthbar(edict_t *self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (!self->target || !*self->target)
|
|
{
|
|
gi.Com_PrintFmt("{}: missing target\n", *self);
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (!self->message)
|
|
{
|
|
gi.Com_PrintFmt("{}: missing message\n", *self);
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->use = use_target_healthbar;
|
|
self->think = check_target_healthbar;
|
|
self->nextthink = level.time + 25_ms;
|
|
}
|
|
|
|
/*QUAKED target_autosave (0 1 0) (-8 -8 -8) (8 8 8)
|
|
*
|
|
* Auto save on command.
|
|
*/
|
|
|
|
USE(use_target_autosave) (edict_t *ent, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
gtime_t save_time = gtime_t::from_sec(gi.cvar("g_athena_auto_save_min_time", "60", CVAR_NOSET)->value);
|
|
|
|
if (level.time - level.next_auto_save > save_time)
|
|
{
|
|
gi.AddCommandString("autosave\n");
|
|
level.next_auto_save = level.time;
|
|
}
|
|
}
|
|
|
|
void SP_target_autosave(edict_t *self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->use = use_target_autosave;
|
|
}
|
|
|
|
/*QUAKED target_sky (0 1 0) (-8 -8 -8) (8 8 8)
|
|
*
|
|
* Change sky parameters.
|
|
"sky" environment map name
|
|
"skyaxis" vector axis for rotating sky
|
|
"skyrotate" speed of rotation in degrees/second
|
|
*/
|
|
|
|
USE(use_target_sky) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
if (self->map)
|
|
gi.configstring(CS_SKY, self->map);
|
|
|
|
if (self->count & 3)
|
|
{
|
|
float rotate;
|
|
int32_t autorotate;
|
|
|
|
sscanf(gi.get_configstring(CS_SKYROTATE), "%f %i", &rotate, &autorotate);
|
|
|
|
if (self->count & 1)
|
|
rotate = self->accel;
|
|
|
|
if (self->count & 2)
|
|
autorotate = self->style;
|
|
|
|
gi.configstring(CS_SKYROTATE, G_Fmt("{} {}", rotate, autorotate).data());
|
|
}
|
|
|
|
if (self->count & 4)
|
|
gi.configstring(CS_SKYAXIS, G_Fmt("{}", self->movedir).data());
|
|
}
|
|
|
|
void SP_target_sky(edict_t *self)
|
|
{
|
|
self->use = use_target_sky;
|
|
if (st.was_key_specified("sky"))
|
|
self->map = st.sky;
|
|
if (st.was_key_specified("skyaxis"))
|
|
{
|
|
self->count |= 4;
|
|
self->movedir = st.skyaxis;
|
|
}
|
|
if (st.was_key_specified("skyrotate"))
|
|
{
|
|
self->count |= 1;
|
|
self->accel = st.skyrotate;
|
|
}
|
|
if (st.was_key_specified("skyautorotate"))
|
|
{
|
|
self->count |= 2;
|
|
self->style = st.skyautorotate;
|
|
}
|
|
}
|
|
|
|
//==========================================================
|
|
|
|
/*QUAKED target_crossunit_trigger (.5 .5 .5) (-8 -8 -8) (8 8 8) trigger1 trigger2 trigger3 trigger4 trigger5 trigger6 trigger7 trigger8
|
|
Once this trigger is touched/used, any trigger_crossunit_target with the same trigger number is automatically used when a level is started within the same unit. It is OK to check multiple triggers. Message, delay, target, and killtarget also work.
|
|
*/
|
|
USE(trigger_crossunit_trigger_use) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
game.cross_unit_flags |= self->spawnflags.value;
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
void SP_target_crossunit_trigger(edict_t *self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->svflags = SVF_NOCLIENT;
|
|
self->use = trigger_crossunit_trigger_use;
|
|
}
|
|
|
|
/*QUAKED target_crossunit_target (.5 .5 .5) (-8 -8 -8) (8 8 8) trigger1 trigger2 trigger3 trigger4 trigger5 trigger6 trigger7 trigger8 - - - - - - - - trigger9 trigger10 trigger11 trigger12 trigger13 trigger14 trigger15 trigger16
|
|
Triggered by a trigger_crossunit elsewhere within a unit. If multiple triggers are checked, all must be true. Delay, target and
|
|
killtarget also work.
|
|
|
|
"delay" delay before using targets if the trigger has been activated (default 1)
|
|
*/
|
|
THINK(target_crossunit_target_think) (edict_t *self) -> void
|
|
{
|
|
if (self->spawnflags.value == (game.cross_unit_flags & SFL_CROSS_TRIGGER_MASK & self->spawnflags.value))
|
|
{
|
|
G_UseTargets(self, self);
|
|
G_FreeEdict(self);
|
|
}
|
|
}
|
|
|
|
void SP_target_crossunit_target(edict_t *self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
if (!self->delay)
|
|
self->delay = 1;
|
|
self->svflags = SVF_NOCLIENT;
|
|
|
|
self->think = target_crossunit_target_think;
|
|
self->nextthink = level.time + gtime_t::from_sec(self->delay);
|
|
}
|
|
|
|
/*QUAKED target_achievement (.5 .5 .5) (-8 -8 -8) (8 8 8)
|
|
Give an achievement.
|
|
|
|
"achievement" cheevo to give
|
|
*/
|
|
USE(use_target_achievement) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
gi.WriteByte(svc_achievement);
|
|
gi.WriteString(self->map);
|
|
gi.multicast(vec3_origin, MULTICAST_ALL, true);
|
|
}
|
|
|
|
void SP_target_achievement(edict_t *self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->map = st.achievement;
|
|
self->use = use_target_achievement;
|
|
}
|
|
|
|
USE(use_target_story) (edict_t *self, edict_t *other, edict_t *activator) -> void
|
|
{
|
|
if (self->message && *self->message)
|
|
level.story_active = true;
|
|
else
|
|
level.story_active = false;
|
|
|
|
gi.configstring(CONFIG_STORY, self->message ? self->message : "");
|
|
}
|
|
|
|
void SP_target_story(edict_t *self)
|
|
{
|
|
if (deathmatch->integer)
|
|
{
|
|
G_FreeEdict(self);
|
|
return;
|
|
}
|
|
|
|
self->use = use_target_story;
|
|
} |