283 lines
7.3 KiB
C
283 lines
7.3 KiB
C
|
|
/*
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
}
|