Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/cgame/default/cg_view.c
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,42 @@ static void Cg_UpdateAngles(const player_state_t *ps0, const player_state_t *ps1
Vec3_Vectors(cgi.view->angles, &cgi.view->forward, &cgi.view->right, &cgi.view->up);
}

/**
* @brief While falling through a trigger_void, lock the camera at the entry point
* and re-aim it at the falling player each frame. Forces third person so the player
* model remains visible. Releases automatically once the void state clears.
*/
static void Cg_UpdateVoidCamera(const player_state_t *ps) {
static bool locked;
static vec3_t locked_origin;

if (!ps->stats[STAT_VOID]) {
locked = false;
return;
}

// snapshot the camera position at the moment we enter the void
if (!locked) {
locked_origin = cgi.view->origin;
locked = true;
}

// force third person so the falling player model is rendered
cgi.client->third_person = true;

// hold the camera in place
cgi.view->origin = locked_origin;

// track the falling player
const cl_entity_t *self = Cg_Self();
const vec3_t dir = Vec3_Subtract(self->origin, locked_origin);

if (!Vec3_Equal(dir, Vec3_Zero())) {
cgi.view->angles = Vec3_Euler(Vec3_Normalize(dir));
Vec3_Vectors(cgi.view->angles, &cgi.view->forward, &cgi.view->right, &cgi.view->up);
}
}

/**
* @brief Updates the view ambient light level from the worldspawn entity definition.
*/
Expand Down Expand Up @@ -347,6 +383,8 @@ void Cg_PrepareView(const cl_frame_t *frame) {

Cg_UpdateBob(ps1);

Cg_UpdateVoidCamera(ps1);

Cg_UpdateAmbient();

cgi.view->contents = cgi.PointContents(cgi.view->origin);
Expand Down
18 changes: 15 additions & 3 deletions src/game/default/g_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ static void G_ClientObituary(g_client_t *cl, g_entity_t *attacker, uint32_t mod)
case MOD_TRIGGER_HURT:
msg = "%s was in the wrong place :actofgod:";
break;
case MOD_TRIGGER_VOID:
msg = "%s has succumbed to the void king :actofgod:";
break;
case MOD_ACT_OF_GOD:
msg = "%s was killed by an act of god :actofgod:";
break;
Expand Down Expand Up @@ -534,13 +537,15 @@ static void G_ClientDie(g_entity_t *ent, g_entity_t *attacker, uint32_t mod) {

G_ClientObituary(cl, attacker, mod);

cl->in_void = false;

G_HookDetach(cl);

G_TossQuadDamage(cl);
G_TossInvisibility(cl);
G_TossInvulnerability(cl);

if (g_level.gameplay == GAME_DEATHMATCH && mod != MOD_TRIGGER_HURT) {
if (g_level.gameplay == GAME_DEATHMATCH && mod != MOD_TRIGGER_HURT && mod != MOD_TRIGGER_VOID) {
G_TossWeapon(cl);
}

Expand Down Expand Up @@ -986,7 +991,7 @@ static g_entity_t *G_SelectTeamSpawnPoint(g_client_t *cl) {
/**
* @brief Selects the most appropriate spawn point for the given client.
*/
static g_entity_t *G_SelectSpawnPoint(g_client_t *cl) {
g_entity_t *G_SelectSpawnPoint(g_client_t *cl) {
g_entity_t *spawn = NULL;

if (g_level.teams || g_level.ctf) { // try team spawns first if applicable
Expand Down Expand Up @@ -1110,6 +1115,7 @@ static void G_ClientRespawn_(g_client_t *cl) {
}

ent->dead = false;
cl->in_void = false;
ent->Die = G_ClientDie;
memset(&ent->ground, 0, sizeof(ent->ground));
ent->max_health = 100;
Expand Down Expand Up @@ -1989,7 +1995,13 @@ void G_ClientBeginFrame(g_client_t *cl) {

g_entity_t *ent = cl->entity;

if ((G_IsMeat(ent) && ent->dead) || ((cl->buttons | cl->latched_buttons) & BUTTON_SCORE)) {
// clear a stale void state if the player has left the brush without reaching
// the bottom (e.g. air-controlled back out over the lip)
if (cl->in_void && g_level.time > cl->void_touch_time + 100) {
cl->in_void = false;
}

if ((G_IsMeat(ent) && ent->dead) || cl->in_void || ((cl->buttons | cl->latched_buttons) & BUTTON_SCORE)) {
cl->show_scores = true;
} else {
cl->show_scores = false;
Expand Down
1 change: 1 addition & 0 deletions src/game/default/g_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#if defined(__GAME_LOCAL_H__)
void G_ClientBegin(g_client_t *cl);
g_entity_t *G_SelectSpawnPoint(g_client_t *cl);
void G_ClientBeginFrame(g_client_t *cl);
bool G_ClientConnect(g_client_t *cl, char *user_info);
void G_ClientDisconnect(g_client_t *cl);
Expand Down
3 changes: 3 additions & 0 deletions src/game/default/g_client_stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ void G_ClientStats(g_client_t *cl) {
cl->ps.stats[STAT_SCORES] |= 1;
}

// void fall: signals the client to lock the camera and track the falling player
cl->ps.stats[STAT_VOID] = cl->in_void ? 1 : 0;

if (cl->persistent.team) { // send team ID, -1 is no team
cl->ps.stats[STAT_TEAM] = cl->persistent.team->id;
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/game/default/g_entity.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ static const g_entity_class_t g_entity_classes[] = {
{ "trigger_once", G_trigger_once },
{ "trigger_push", G_trigger_push },
{ "trigger_relay", G_trigger_relay },
{ "trigger_void", G_trigger_void },
{ "trigger_teleporter", G_misc_teleporter },

{ "worldspawn", G_worldspawn },
Expand Down Expand Up @@ -244,6 +245,7 @@ static void G_InitMedia(void) {
// precache player sounds; clients will load these when a new player model gets loaded.
gi.SoundIndex("*death_1");
gi.SoundIndex("*death_2");
gi.SoundIndex("*death_void");
gi.SoundIndex("*drown_1");
gi.SoundIndex("*fall_1");
gi.SoundIndex("*fall_2");
Expand Down
164 changes: 164 additions & 0 deletions src/game/default/g_entity_trigger.c
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,170 @@ static void G_trigger_exec_Touch(g_entity_t *ent, g_entity_t *other, const cm_tr
}
}

/**
* @brief Handles touch events on a `trigger_void`, pulling players into the void.
*/
static void G_trigger_void_Touch(g_entity_t *ent, g_entity_t *other, const cm_trace_t *trace) {

if (!other->take_damage) { // handle dropped items landing in the void
if (other->item) {
if (other->item->def.type == ITEM_FLAG) {
G_ResetDroppedFlag(other);
} else if (other->item->def.type == ITEM_TECH) {
G_ResetDroppedTech(other);
} else {
G_FreeEntity(other);
}
}
return;
}

if (!other->client || other->dead) {
return;
}

g_client_t *cl = other->client;

if (!cl->in_void) {

// Only enter void from the top half of the brush (side entry is ignored)
const float mid_z = (ent->abs_bounds.mins.z + ent->abs_bounds.maxs.z) * 0.5f;
if (other->s.origin.z < mid_z) {
return;
}

if (ent->spawn_flags & 1) { // VOID_TELEPORT: warp to a spawn instead of killing
const g_entity_t *spawn = NULL;

if (ent->target) { // a specific destination pad was requested
spawn = G_Find(NULL, EOFS(target_name), ent->target);
if (!spawn) {
gi.Warn("trigger_void: target \"%s\" not found\n", ent->target);
}
}

if (!spawn) { // no target, or it was missing; pick a random spawn point
spawn = G_SelectSpawnPoint(cl);
}

if (spawn) {
gi.UnlinkEntity(other);

other->s.origin = spawn->s.origin;
other->s.origin.z += 8.0;

cl->ps.pm_state.flags &= ~PMF_TIME_MASK;
cl->ps.pm_state.flags |= PMF_TIME_TELEPORT;
cl->ps.pm_state.time = 20;
cl->ps.pm_state.delta_angles = Vec3_Subtract(spawn->s.angles, cl->cmd_angles);

other->velocity = Vec3_Zero();
cl->cmd_angles = Vec3_Zero();
cl->angles = Vec3_Zero();

other->s.event = EV_CLIENT_TELEPORT;

G_MulticastSound(&(const g_play_sound_t) {
.index = g_media.sounds.teleport,
.origin = &other->s.origin,
}, MULTICAST_PHS);

gi.LinkEntity(other);
}
return;
}

// Mark as falling into the void, suppress HUD, play death sound
cl->in_void = true;
cl->void_touch_time = g_level.time;

G_MulticastSound(&(const g_play_sound_t) {
.index = gi.SoundIndex("*death_void"),
.entity = other,
}, MULTICAST_PHS);

return;
}

// Still overlapping the brush; refresh the touch time so the state isn't
// cleared as stale (see G_ClientBeginFrame).
cl->void_touch_time = g_level.time;

// Already falling — kill when the player's feet reach the brush's bottom face
if (other->abs_bounds.mins.z > ent->abs_bounds.mins.z) {
return;
}

// Strip powerups without dropping them
if (cl->inventory[POWERUP_QUAD]) {
cl->inventory[POWERUP_QUAD] = 0;
cl->quad_damage_time = 0;
other->s.effects &= ~EF_QUAD;
}

if (cl->inventory[POWERUP_INVISIBILITY]) {
cl->inventory[POWERUP_INVISIBILITY] = 0;
cl->invisibility_time = 0;
other->s.effects &= ~EF_INVISIBILITY;
}

if (cl->inventory[POWERUP_INVULNERABILITY]) {
cl->inventory[POWERUP_INVULNERABILITY] = 0;
cl->invulnerability_time = 0;
cl->invulnerability_countdown_time = 0;
other->s.effects &= ~EF_INVULNERABILITY;
}

// Return the carried CTF flag immediately rather than dropping it
if (g_level.ctf) {
G_ReturnFlag(cl);
}

// Respawn any carried tech back into its pool rather than dropping it
if (g_level.techs) {
g_entity_t *tech = G_TossTech(cl);
if (tech) {
G_ResetDroppedTech(tech);
}
}

// Instant kill bypassing armor and god mode; G_ClientDie handles gibs
cl->in_void = false;
G_Damage(&(g_damage_t) {
.target = other,
.inflictor = ent,
.attacker = NULL,
.dir = Vec3_Zero(),
.point = other->s.origin,
.normal = Vec3_Zero(),
.damage = 999,
.knockback = 0,
.flags = DMG_NO_GOD,
.mod = MOD_TRIGGER_VOID
});
}

/*QUAKED trigger_void (.5 .5 .5) ? teleport
Players stepping onto the top face are pulled into the void. A special death sound plays and
the HUD is suppressed during the fall. Upon reaching the bottom face, the player is instantly
killed: powerups are removed, carried techs are respawned, and any carried CTF flag is returned.

-------- Keys --------
target : With the teleport flag, the target_name of a specific destination (e.g. a
misc_teleporter_dest) to warp to. If unset, a random spawn point is chosen.

-------- Spawn flags --------
teleport : Instead of killing the player, teleport them to a spawn point (see target).
*/
void G_trigger_void(g_entity_t *ent) {

G_Trigger_Init(ent);

ent->Touch = G_trigger_void_Touch;

gi.LinkEntity(ent);
}

/*QUAKED trigger_exec (0 1 0) ?
Executes a console command or script file when activated.

Expand Down
1 change: 1 addition & 0 deletions src/game/default/g_entity_trigger.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ void G_trigger_multiple(g_entity_t *ent);
void G_trigger_once(g_entity_t *ent);
void G_trigger_push(g_entity_t *ent);
void G_trigger_relay(g_entity_t *ent);
void G_trigger_void(g_entity_t *ent);
#endif /* __GAME_LOCAL_H__ */
43 changes: 43 additions & 0 deletions src/game/default/g_item.c
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,49 @@ g_entity_t *G_TossFlag(g_client_t *cl) {
return G_DropItem(cl, flag);
}

/**
* @brief Removes the CTF flag carried by the client and returns it directly to
* its base, announcing both actions in a single message. Used when a carrier is
* removed without dropping the flag (e.g. falling into a trigger_void).
*/
void G_ReturnFlag(g_client_t *cl) {

const g_item_t *flag = G_GetFlag(cl);

if (!flag) {
return;
}

const g_team_t *team = &g_team_list[flag->def.tag - FLAG_FIRST];
const g_item_tag_t index = flag->def.tag;

if (!cl->inventory[index]) {
return;
}

// remove the flag from the carrier
cl->inventory[index] = 0;
cl->entity->s.model3 = 0;
cl->entity->s.effects &= ~EF_CTF_MASK;

// send the team's flag entity straight back to its base
g_entity_t *f = G_FlagForTeam(team);
if (f) {
f->sv_flags &= ~SVF_NO_CLIENT;
f->s.event = EV_ITEM_RESPAWN;
f->s.event_data = f->item->def.tag;
f->solid = SOLID_TRIGGER;
gi.LinkEntity(f);
}

G_MulticastSound(&(const g_play_sound_t) {
.index = g_media.sounds.ctf_return
}, MULTICAST_PHS_R);

gi.BroadcastPrint(PRINT_HIGH, "%s dropped the %s flag and it was returned :flag%d_return:\n",
cl->persistent.net_name, team->name, team->id + 1);
}

/**
* @brief Drop command callback that tosses the client's carried CTF flag.
*/
Expand Down
1 change: 1 addition & 0 deletions src/game/default/g_item.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
void G_SpawnItem(g_entity_t *ent, const g_item_t *item);
bool G_SetAmmo(g_client_t *cl, const g_item_t *item, int16_t count);
g_entity_t *G_TossFlag(g_client_t *cl);
void G_ReturnFlag(g_client_t *cl);
g_entity_t *G_TossTech(g_client_t *cl);
g_entity_t *G_TossQuadDamage(g_client_t *cl);
g_entity_t *G_TossInvisibility(g_client_t *cl);
Expand Down
Loading
Loading