SpaceQuakeHulk/Quake/sh_game.c

375 lines
9.1 KiB
C
Raw Permalink Normal View History

2026-03-24 10:46:22 -04:00
/*
* sh_game.c -- Space Hulk game state machine, grid utilities, animation
*
* Copyright (C) 2026 fish fvch studios
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*/
#include "quakedef.h"
#include "sh_game.h"
#include <math.h>
/* ============================================================
* GLOBALS
* ============================================================ */
sh_gamestate_t sh_game;
/* ============================================================
* COORDINATE CONVERSION
* ============================================================ */
void SH_GridToWorld (int gx, int gy, vec3_t out)
{
out[0] = (gx * SH_GRID_SIZE) + (SH_GRID_SIZE / 2);
out[1] = (gy * SH_GRID_SIZE) + (SH_GRID_SIZE / 2);
out[2] = 0; /* floor level; adjusted by caller if needed */
}
void SH_WorldToGrid (vec3_t pos, int *gx, int *gy)
{
*gx = (int)floorf(pos[0] / SH_GRID_SIZE);
*gy = (int)floorf(pos[1] / SH_GRID_SIZE);
}
/* ============================================================
* FACING HELPERS
* ============================================================ */
float SH_FacingToYaw (sh_facing_t facing)
{
switch (facing) {
case SH_FACING_NORTH: return 90;
case SH_FACING_EAST: return 0;
case SH_FACING_SOUTH: return 270;
case SH_FACING_WEST: return 180;
}
return 0;
}
sh_facing_t SH_YawToFacing (float yaw)
{
/* normalize to 0-360 */
yaw = fmodf(yaw, 360.0f);
if (yaw < 0) yaw += 360.0f;
if (yaw >= 315 || yaw < 45) return SH_FACING_EAST;
if (yaw >= 45 && yaw < 135) return SH_FACING_NORTH;
if (yaw >= 135 && yaw < 225) return SH_FACING_WEST;
return SH_FACING_SOUTH;
}
void SH_FacingToDir (sh_facing_t facing, int *dx, int *dy)
{
*dx = 0;
*dy = 0;
switch (facing) {
case SH_FACING_NORTH: *dy = 1; break;
case SH_FACING_EAST: *dx = 1; break;
case SH_FACING_SOUTH: *dy = -1; break;
case SH_FACING_WEST: *dx = -1; break;
}
}
/* ============================================================
* GRID COLLISION
* ============================================================ */
qboolean SH_GridBlocked (int gx, int gy, edict_t *ignore)
{
vec3_t start, end, mins, maxs;
trace_t trace;
int i;
/* trace downward through the BSP to see if a floor exists */
SH_GridToWorld(gx, gy, start);
VectorCopy(start, end);
start[2] = 64;
end[2] = -64;
mins[0] = -14; mins[1] = -14; mins[2] = 0;
maxs[0] = 14; maxs[1] = 14; maxs[2] = 56;
trace = SV_Move(start, mins, maxs, end, MOVE_NOMONSTERS, ignore);
if (trace.allsolid || trace.startsolid)
return true;
/* check if any marine occupies this cell */
for (i = 0; i < sh_game.num_marines; i++)
{
if (!sh_game.marines[i].active || !sh_game.marines[i].alive)
continue;
if (sh_game.marines[i].ent == ignore)
continue;
if (sh_game.marines[i].grid_x == gx && sh_game.marines[i].grid_y == gy)
return true;
}
/* check genestealers too */
for (i = 0; i < sh_game.num_genestealers; i++)
{
if (!sh_game.genestealers[i].active || !sh_game.genestealers[i].alive)
continue;
if (sh_game.genestealers[i].ent == ignore)
continue;
if (sh_game.genestealers[i].grid_x == gx && sh_game.genestealers[i].grid_y == gy)
return true;
}
return false;
}
/* ============================================================
* ANIMATION SYSTEM
* ============================================================ */
void SH_AnimPush (edict_t *ent, vec3_t start, vec3_t end,
float start_yaw, float end_yaw, double duration)
{
sh_anim_t *a;
int i;
/* find a free slot */
for (i = 0; i < SH_MAX_ANIMS; i++)
{
if (!sh_game.anims[i].active)
{
a = &sh_game.anims[i];
a->active = true;
a->ent = ent;
VectorCopy(start, a->start_pos);
VectorCopy(end, a->end_pos);
a->start_yaw = start_yaw;
a->end_yaw = end_yaw;
a->start_time = realtime;
a->duration = duration;
return;
}
}
/* no free slot -- snap immediately */
VectorCopy(end, ent->v.origin);
ent->v.angles[1] = end_yaw;
SV_LinkEdict(ent, false);
}
static void SH_AnimUpdate (void)
{
int i;
double t;
float frac;
sh_anim_t *a;
for (i = 0; i < SH_MAX_ANIMS; i++)
{
a = &sh_game.anims[i];
if (!a->active)
continue;
t = realtime - a->start_time;
if (t >= a->duration)
{
/* finished: snap to final position */
VectorCopy(a->end_pos, a->ent->v.origin);
a->ent->v.angles[1] = a->end_yaw;
SV_LinkEdict(a->ent, false);
a->active = false;
continue;
}
/* lerp position */
frac = (float)(t / a->duration);
a->ent->v.origin[0] = a->start_pos[0] + (a->end_pos[0] - a->start_pos[0]) * frac;
a->ent->v.origin[1] = a->start_pos[1] + (a->end_pos[1] - a->start_pos[1]) * frac;
a->ent->v.origin[2] = a->start_pos[2] + (a->end_pos[2] - a->start_pos[2]) * frac;
/* lerp yaw (handle wraparound) */
{
float diff = a->end_yaw - a->start_yaw;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;
a->ent->v.angles[1] = a->start_yaw + diff * frac;
}
SV_LinkEdict(a->ent, false);
}
}
qboolean SH_AnimBusy (void)
{
int i;
for (i = 0; i < SH_MAX_ANIMS; i++)
{
if (sh_game.anims[i].active)
return true;
}
return false;
}
/* ============================================================
* MESSAGE LOG
* ============================================================ */
void SH_Log (sh_log_type_t type, const char *fmt, ...)
{
va_list ap;
sh_log_msg_t *msg;
char buf[SH_LOG_MSG_LEN];
va_start(ap, fmt);
q_vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
/* also print to console so we can see it during development */
Con_Printf("[SH] %s\n", buf);
/* check for stacking with previous message */
if (sh_game.log.count > 0)
{
int prev = (sh_game.log.write_index - 1 + SH_MAX_LOG_MESSAGES) % SH_MAX_LOG_MESSAGES;
msg = &sh_game.log.messages[prev];
if ((int)msg->type == (int)type &&
msg->round == sh_game.round_number &&
strcmp(msg->text, buf) == 0)
{
msg->stack_count++;
sh_game.log.updated = true;
return;
}
}
/* write new message */
msg = &sh_game.log.messages[sh_game.log.write_index];
q_strlcpy(msg->text, buf, SH_LOG_MSG_LEN);
msg->type = type;
msg->round = sh_game.round_number;
msg->stack_count = 1;
sh_game.log.write_index = (sh_game.log.write_index + 1) % SH_MAX_LOG_MESSAGES;
if (sh_game.log.count < SH_MAX_LOG_MESSAGES)
sh_game.log.count++;
sh_game.log.updated = true;
}
/* ============================================================
* CONSOLE COMMANDS
* ============================================================ */
static void SH_Cmd_NewGame (void)
{
SH_NewGame();
}
/* ============================================================
* INIT / SHUTDOWN
* ============================================================ */
void SH_Init (void)
{
memset(&sh_game, 0, sizeof(sh_game));
sh_game.phase = SH_PHASE_INACTIVE;
Cmd_AddCommand("sh_newgame", SH_Cmd_NewGame);
SH_Input_Init();
Con_Printf("Space Hulk system initialized.\n");
}
void SH_Shutdown (void)
{
memset(&sh_game, 0, sizeof(sh_game));
}
/* ============================================================
* NEW GAME
* ============================================================ */
void SH_NewGame (void)
{
int i;
if (!sv.active)
{
Con_Printf("SH_NewGame: no map loaded. Load a map first.\n");
return;
}
memset(&sh_game, 0, sizeof(sh_game));
/* spawn a single marine at grid 2,2 facing north for Phase 1 testing */
SH_Marine_Spawn(0, 2, 2, SH_FACING_NORTH, SH_WEAPON_STORM_BOLTER, "Sgt. Marcus");
sh_game.num_marines = 1;
sh_game.selected_marine = 0;
/* begin player phase */
sh_game.round_number = 1;
sh_game.phase = SH_PHASE_PLAYER;
SH_Marine_BeginPlayerPhase();
/* clear anims */
for (i = 0; i < SH_MAX_ANIMS; i++)
sh_game.anims[i].active = false;
SH_Log(SH_LOG_INFO, "Round %d -- PLAYER PHASE", sh_game.round_number);
Con_Printf("Space Hulk: game started. Use W/A/D to move, Space to end turn.\n");
}
/* ============================================================
* ACTIVE CHECK
* ============================================================ */
qboolean SH_Active (void)
{
return (sh_game.phase != SH_PHASE_INACTIVE);
}
/* ============================================================
* FRAME -- called every render frame from _Host_Frame
* ============================================================ */
void SH_Frame (void)
{
if (!SH_Active())
return;
/* always update animations */
SH_AnimUpdate();
/* don't advance game logic while animations are playing */
if (SH_AnimBusy())
return;
switch (sh_game.phase) {
case SH_PHASE_PLAYER:
/* input-driven: SH_Input_KeyEvent dispatches actions.
* check if all marines are done. */
if (SH_Marine_AllDone())
{
sh_game.phase = SH_PHASE_ROUND_END;
}
break;
case SH_PHASE_ROUND_END:
sh_game.round_number++;
SH_Marine_BeginPlayerPhase();
sh_game.phase = SH_PHASE_PLAYER;
SH_Log(SH_LOG_INFO, "Round %d -- PLAYER PHASE", sh_game.round_number);
break;
case SH_PHASE_BRIEFING:
case SH_PHASE_ALIEN:
case SH_PHASE_OVERWATCH_REACT:
case SH_PHASE_VICTORY:
case SH_PHASE_DEFEAT:
case SH_PHASE_INACTIVE:
/* not implemented in Phase 1 */
break;
}
}