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