@@ -107,8 +107,15 @@ void GameServer::OnPlayerConnected(uint8_t playerId)
107107 data.state .yaw = 0 .0f ;
108108 data.state .pitch = 0 .0f ;
109109 data.state .stateFlags = NetStateFlags::IS_GROUNDED;
110+ data.state .health = MAX_HEALTH;
111+ data.state .hitByPlayerId = 0xFF ;
112+ data.state .fireCounter = 0 ;
110113 data.lastInput = {};
111114 data.reloadTimer = 0.0 ;
115+ data.health = MAX_HEALTH;
116+ data.respawnTimer = 0.0 ;
117+ data.fireTimer = 0.0 ;
118+ data.fireCounter = 0 ;
112119
113120 m_Players[playerId] = data;
114121 printf (" [GameServer] Player %u (Team %s) spawned at (%.1f, %.1f, %.1f)\n " ,
@@ -162,13 +169,45 @@ void GameServer::Tick()
162169 // 3. Simulate physics for all players
163170 SimulatePhysics ();
164171
165- // 4. Update tick ID in all player states
172+ // 4. Process firing and combat for all players
173+ for (auto & [id, player] : m_Players)
174+ {
175+ // Clear hit marker each tick
176+ player.state .hitByPlayerId = 0xFF ;
177+
178+ // Respawn timer
179+ if (player.state .stateFlags & NetStateFlags::IS_DEAD)
180+ {
181+ player.respawnTimer -= TICK_DURATION;
182+ if (player.respawnTimer <= 0.0 )
183+ {
184+ // Respawn
185+ player.health = MAX_HEALTH;
186+ player.state .stateFlags &= ~NetStateFlags::IS_DEAD;
187+ player.state .stateFlags |= NetStateFlags::IS_GROUNDED;
188+ player.state .position = GetSpawnPosition (id, player.teamId );
189+ player.state .velocity = { 0 .0f , 0 .0f , 0 .0f };
190+ player.respawnTimer = 0.0 ;
191+ printf (" [GameServer] Player %u respawned\n " , id);
192+ }
193+ }
194+ else
195+ {
196+ ProcessFiring (player, id);
197+ }
198+
199+ // Sync combat data to state
200+ player.state .health = player.health ;
201+ player.state .fireCounter = player.fireCounter ;
202+ }
203+
204+ // 5. Update tick ID in all player states
166205 for (auto & [id, player] : m_Players)
167206 {
168207 player.state .tickId = m_CurrentTick;
169208 }
170209
171- // 5 . Broadcast per-player snapshots
210+ // 6 . Broadcast per-player snapshots
172211 BroadcastSnapshots ();
173212}
174213
@@ -198,6 +237,14 @@ void GameServer::ProcessInputCmd(const InputCmd& cmd, uint8_t playerId)
198237 PlayerData& player = it->second ;
199238 player.lastInput = cmd;
200239
240+ // Dead players: only update camera, skip all actions
241+ if (player.state .stateFlags & NetStateFlags::IS_DEAD)
242+ {
243+ player.state .yaw = cmd.yaw ;
244+ player.state .pitch = cmd.pitch ;
245+ return ;
246+ }
247+
201248 player.state .yaw = cmd.yaw ;
202249 player.state .pitch = cmd.pitch ;
203250
@@ -244,6 +291,9 @@ void GameServer::SimulatePhysics()
244291{
245292 for (auto & [id, player] : m_Players)
246293 {
294+ // Skip dead players
295+ if (player.state .stateFlags & NetStateFlags::IS_DEAD)
296+ continue ;
247297 SimulatePlayerPhysics (player);
248298 }
249299}
@@ -410,3 +460,123 @@ void GameServer::BroadcastSnapshots()
410460 m_pNetwork->SendSnapshotToPlayer (myId, snapshot);
411461 }
412462}
463+
464+ // -----------------------------------------------------------------------------
465+ // ProcessFiring - Handle fire rate and hitscan for a player
466+ // -----------------------------------------------------------------------------
467+ void GameServer::ProcessFiring (PlayerData& shooter, uint8_t shooterId)
468+ {
469+ bool isFiring = (shooter.lastInput .buttons & InputButtons::FIRE) != 0 ;
470+
471+ if (!isFiring)
472+ {
473+ shooter.fireTimer = 0.0 ;
474+ return ;
475+ }
476+
477+ // Determine RPM and damage based on team
478+ double rpm = (shooter.teamId == PlayerTeam::RED) ? RED_RPM : BLUE_RPM;
479+ uint8_t damage = (shooter.teamId == PlayerTeam::RED) ? RED_DAMAGE : BLUE_DAMAGE;
480+ double fireInterval = 60.0 / rpm;
481+
482+ // First shot fires immediately; subsequent shots at RPM interval
483+ bool shouldFire = false ;
484+ if (shooter.fireTimer <= 0.0 )
485+ {
486+ // First press — fire immediately
487+ shouldFire = true ;
488+ shooter.fireTimer = fireInterval;
489+ }
490+ else
491+ {
492+ shooter.fireTimer -= TICK_DURATION;
493+ if (shooter.fireTimer <= 0.0 )
494+ {
495+ shouldFire = true ;
496+ shooter.fireTimer += fireInterval;
497+ }
498+ }
499+
500+ if (!shouldFire) return ;
501+
502+ // Increment fire counter
503+ shooter.fireCounter ++;
504+
505+ // Cast ray from eye position
506+ Float3 eyePos = {
507+ shooter.state .position .x ,
508+ shooter.state .position .y + PLAYER_HEIGHT - 0 .1f ,
509+ shooter.state .position .z
510+ };
511+ Float3 rayDir = ServerRaycast::DirectionFromYawPitch (
512+ shooter.state .yaw , shooter.state .pitch );
513+
514+ // Test against all other alive players (no friendly fire)
515+ uint8_t hitId = 0xFF ;
516+ float hitDist = 0 .0f ;
517+ if (RaycastPlayers (eyePos, rayDir, shooterId, shooter.teamId , hitId, hitDist))
518+ {
519+ // Apply damage
520+ auto hitIt = m_Players.find (hitId);
521+ if (hitIt != m_Players.end ())
522+ {
523+ PlayerData& target = hitIt->second ;
524+ if (target.health > damage)
525+ {
526+ target.health -= damage;
527+ }
528+ else
529+ {
530+ target.health = 0 ;
531+ target.state .stateFlags |= NetStateFlags::IS_DEAD;
532+ target.respawnTimer = RESPAWN_TIME;
533+ target.state .velocity = { 0 .0f , 0 .0f , 0 .0f };
534+ printf (" [GameServer] Player %u killed Player %u\n " , shooterId, hitId);
535+ }
536+
537+ // Record hit for attacker's hit marker
538+ shooter.state .hitByPlayerId = hitId;
539+ }
540+ }
541+ }
542+
543+ // -----------------------------------------------------------------------------
544+ // RaycastPlayers - Test ray against all alive enemy players
545+ //
546+ // Returns true if any enemy player is hit. outHitId/outDist are set to the
547+ // closest hit. Players on excludeTeam are skipped (no friendly fire).
548+ // -----------------------------------------------------------------------------
549+ bool GameServer::RaycastPlayers (const Float3& origin, const Float3& dir,
550+ uint8_t excludeId, uint8_t excludeTeam,
551+ uint8_t & outHitId, float & outDist)
552+ {
553+ bool anyHit = false ;
554+ float closestT = 9999 .0f ;
555+
556+ for (const auto & [id, player] : m_Players)
557+ {
558+ // Skip self
559+ if (id == excludeId) continue ;
560+ // Skip same team (no friendly fire)
561+ if (player.teamId == excludeTeam) continue ;
562+ // Skip dead
563+ if (player.state .stateFlags & NetStateFlags::IS_DEAD) continue ;
564+
565+ float t = 0 .0f ;
566+ if (ServerRaycast::RayCapsule (origin, dir,
567+ player.state .position , PLAYER_HEIGHT, CAPSULE_RADIUS, t))
568+ {
569+ if (t < closestT)
570+ {
571+ closestT = t;
572+ outHitId = id;
573+ anyHit = true ;
574+ }
575+ }
576+ }
577+
578+ if (anyHit)
579+ outDist = closestT;
580+
581+ return anyHit;
582+ }
0 commit comments