/* * 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 /* ============================================================ * 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; } }