first
Some checks failed
Linux / Build Linux (push) Has been cancelled
Linux / Build Linux-1 (push) Has been cancelled
macOS / Build macOS (push) Has been cancelled
macOS / Build macOS-1 (push) Has been cancelled
Windows (MinGW) / Build MinGW (push) Has been cancelled
Windows (MinGW) / Build MinGW-1 (push) Has been cancelled
Windows (MSVC) / Build Windows (push) Has been cancelled
Windows (MSVC) / Build Windows-1 (push) Has been cancelled

This commit is contained in:
saarsena@gmail.com 2026-03-24 10:46:22 -04:00
commit 8269b17aa7
652 changed files with 273930 additions and 0 deletions

282
Quake/sh_marine.c Normal file
View file

@ -0,0 +1,282 @@
/*
* sh_marine.c -- Marine spawning, AP tracking, action execution
*
* 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"
/* ============================================================
* SPAWN
* ============================================================ */
void SH_Marine_Spawn (int index, int gx, int gy,
sh_facing_t facing, sh_weapon_t weapon, const char *name)
{
sh_marine_t *m;
vec3_t pos;
if (index < 0 || index >= SH_MAX_MARINES)
return;
m = &sh_game.marines[index];
memset(m, 0, sizeof(*m));
m->active = true;
m->alive = true;
m->squad_index = index;
m->grid_x = gx;
m->grid_y = gy;
m->facing = facing;
m->weapon = weapon;
m->ap_max = SH_DEFAULT_AP;
m->ap = m->ap_max;
m->health = 1;
q_strlcpy(m->name, name, sizeof(m->name));
/* allocate a quake edict */
m->ent = ED_Alloc();
m->ent->v.classname = PR_SetEngineString("sh_marine");
m->ent->v.movetype = MOVETYPE_NONE; /* we control all movement */
m->ent->v.solid = SOLID_BBOX;
/* use the player model as placeholder */
m->ent->v.modelindex = SV_ModelIndex("progs/player.mdl");
/* position at grid center */
SH_GridToWorld(gx, gy, pos);
VectorCopy(pos, m->ent->v.origin);
m->ent->v.angles[1] = SH_FacingToYaw(facing);
/* bbox same as player */
m->ent->v.mins[0] = -16; m->ent->v.mins[1] = -16; m->ent->v.mins[2] = -24;
m->ent->v.maxs[0] = 16; m->ent->v.maxs[1] = 16; m->ent->v.maxs[2] = 32;
SV_LinkEdict(m->ent, false);
Con_Printf("SH: Spawned %s at grid (%d, %d) facing %s\n",
m->name, gx, gy,
facing == SH_FACING_NORTH ? "North" :
facing == SH_FACING_EAST ? "East" :
facing == SH_FACING_SOUTH ? "South" : "West");
}
/* ============================================================
* PHASE MANAGEMENT
* ============================================================ */
void SH_Marine_BeginPlayerPhase (void)
{
int i;
for (i = 0; i < sh_game.num_marines; i++)
{
if (!sh_game.marines[i].active || !sh_game.marines[i].alive)
continue;
sh_game.marines[i].ap = sh_game.marines[i].ap_max;
sh_game.marines[i].has_acted = false;
sh_game.marines[i].on_overwatch = false;
sh_game.marines[i].overwatch_ap = 0;
}
/* roll command points (1d6) */
sh_game.command_points = (rand() % SH_MAX_COMMAND_POINTS) + 1;
sh_game.command_points_used = 0;
}
qboolean SH_Marine_AllDone (void)
{
int i;
for (i = 0; i < sh_game.num_marines; i++)
{
sh_marine_t *m = &sh_game.marines[i];
if (!m->active || !m->alive)
continue;
/* a marine is "done" if has_acted with 0 AP, or is on overwatch */
if (m->ap > 0 && !m->on_overwatch && !m->has_acted)
return false;
/* also check if they have AP but haven't ended turn */
if (m->ap > 0 && !m->on_overwatch)
return false;
}
return true;
}
void SH_Marine_SelectNext (void)
{
int start = sh_game.selected_marine;
int i;
for (i = 1; i <= sh_game.num_marines; i++)
{
int idx = (start + i) % sh_game.num_marines;
sh_marine_t *m = &sh_game.marines[idx];
if (m->active && m->alive && m->ap > 0 && !m->on_overwatch)
{
sh_game.selected_marine = idx;
sh_game.camera_marine = idx;
SH_Log(SH_LOG_INFO, "Selected: %s (%d AP)", m->name, m->ap);
return;
}
}
/* no marine with AP found, stay on current */
}
void SH_Marine_SelectPrev (void)
{
int start = sh_game.selected_marine;
int i;
for (i = 1; i <= sh_game.num_marines; i++)
{
int idx = (start - i + sh_game.num_marines) % sh_game.num_marines;
sh_marine_t *m = &sh_game.marines[idx];
if (m->active && m->alive && m->ap > 0 && !m->on_overwatch)
{
sh_game.selected_marine = idx;
sh_game.camera_marine = idx;
SH_Log(SH_LOG_INFO, "Selected: %s (%d AP)", m->name, m->ap);
return;
}
}
}
/* ============================================================
* ACTION EXECUTION
* ============================================================ */
qboolean SH_Marine_TryAction (int marine_index, sh_action_t action)
{
sh_marine_t *m;
vec3_t old_pos, new_pos;
float old_yaw, new_yaw;
int dx, dy, new_gx, new_gy;
if (marine_index < 0 || marine_index >= sh_game.num_marines)
return false;
m = &sh_game.marines[marine_index];
if (!m->active || !m->alive)
return false;
switch (action)
{
case SH_ACTION_MOVE_FORWARD:
if (m->ap < SH_AP_MOVE_FORWARD)
{
SH_Log(SH_LOG_INFO, "Not enough AP to move.");
return false;
}
SH_FacingToDir(m->facing, &dx, &dy);
new_gx = m->grid_x + dx;
new_gy = m->grid_y + dy;
if (SH_GridBlocked(new_gx, new_gy, m->ent))
{
SH_Log(SH_LOG_INFO, "Blocked!");
return false;
}
/* capture old position for animation */
SH_GridToWorld(m->grid_x, m->grid_y, old_pos);
SH_GridToWorld(new_gx, new_gy, new_pos);
/* update grid state */
m->grid_x = new_gx;
m->grid_y = new_gy;
m->ap -= SH_AP_MOVE_FORWARD;
/* push animation */
old_yaw = SH_FacingToYaw(m->facing);
SH_AnimPush(m->ent, old_pos, new_pos, old_yaw, old_yaw, SH_ANIM_MOVE_DURATION);
SH_Log(SH_LOG_INFO, "%s moves forward. (%d AP)", m->name, m->ap);
return true;
case SH_ACTION_TURN_LEFT:
/* counter-clockwise: N -> W -> S -> E -> N */
old_yaw = SH_FacingToYaw(m->facing);
m->facing = (sh_facing_t)((m->facing + 3) % 4);
new_yaw = SH_FacingToYaw(m->facing);
/* turns are free (SH_AP_TURN == 0) but still cost if configured */
if (m->ap < SH_AP_TURN)
return false;
m->ap -= SH_AP_TURN;
SH_GridToWorld(m->grid_x, m->grid_y, old_pos);
SH_AnimPush(m->ent, old_pos, old_pos, old_yaw, new_yaw, SH_ANIM_TURN_DURATION);
SH_Log(SH_LOG_INFO, "%s turns left. (%d AP)", m->name, m->ap);
return true;
case SH_ACTION_TURN_RIGHT:
/* clockwise: N -> E -> S -> W -> N */
old_yaw = SH_FacingToYaw(m->facing);
m->facing = (sh_facing_t)((m->facing + 1) % 4);
new_yaw = SH_FacingToYaw(m->facing);
if (m->ap < SH_AP_TURN)
return false;
m->ap -= SH_AP_TURN;
SH_GridToWorld(m->grid_x, m->grid_y, old_pos);
SH_AnimPush(m->ent, old_pos, old_pos, old_yaw, new_yaw, SH_ANIM_TURN_DURATION);
SH_Log(SH_LOG_INFO, "%s turns right. (%d AP)", m->name, m->ap);
return true;
case SH_ACTION_TURN_180:
old_yaw = SH_FacingToYaw(m->facing);
m->facing = (sh_facing_t)((m->facing + 2) % 4);
new_yaw = SH_FacingToYaw(m->facing);
if (m->ap < SH_AP_TURN)
return false;
m->ap -= SH_AP_TURN;
SH_GridToWorld(m->grid_x, m->grid_y, old_pos);
SH_AnimPush(m->ent, old_pos, old_pos, old_yaw, new_yaw, SH_ANIM_TURN_DURATION);
SH_Log(SH_LOG_INFO, "%s turns around. (%d AP)", m->name, m->ap);
return true;
case SH_ACTION_END_TURN:
m->ap = 0;
m->has_acted = true;
SH_Log(SH_LOG_INFO, "%s ends turn.", m->name);
/* auto-select next marine with AP */
SH_Marine_SelectNext();
return true;
case SH_ACTION_OVERWATCH:
if (m->ap < SH_AP_OVERWATCH)
{
SH_Log(SH_LOG_INFO, "Need at least %d AP for Overwatch.", SH_AP_OVERWATCH);
return false;
}
m->overwatch_ap = m->ap;
m->ap = 0;
m->on_overwatch = true;
SH_Log(SH_LOG_ALERT, "%s goes on OVERWATCH!", m->name);
SH_Marine_SelectNext();
return true;
case SH_ACTION_SHOOT:
case SH_ACTION_MELEE:
case SH_ACTION_DOOR:
/* not implemented in Phase 1 */
SH_Log(SH_LOG_INFO, "Not yet implemented.");
return false;
case SH_ACTION_NONE:
default:
return false;
}
}