375 lines
9.1 KiB
C
375 lines
9.1 KiB
C
|
|
/*
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
}
|