Air-Trap 1.0.0
A multiplayer R-Type clone game engine built with C++23 and ECS architecture
Loading...
Searching...
No Matches
PlayerShootSystem.cpp
Go to the documentation of this file.
1
11#include "RType/Logger.hpp"
18#include <cmath>
19
20#include <algorithm>
21#include <tuple>
22#include <vector>
23#include <cstdlib>
24
25namespace rtp::server
26{
27 namespace {
28 int getKillScore(net::EntityType type)
29 {
30 switch (type) {
31 case net::EntityType::Scout: return 10;
32 case net::EntityType::Tank: return 25;
33 case net::EntityType::Boss: return 100;
34 case net::EntityType::Boss2: return 200; // Kraken gives more points!
35 case net::EntityType::Obstacle: return 5;
36 case net::EntityType::ObstacleSolid: return 15;
37 default: return 0;
38 }
39 }
40 } // namespace
41
43 // Public API
45
47 RoomSystem& roomSystem,
48 NetworkSyncSystem& networkSync)
49 : _registry(registry), _roomSystem(roomSystem), _networkSync(networkSync)
50 {
51 }
52
54 {
55 std::vector<std::tuple<ecs::Entity,
58 bool>> pendingSpawns; // owner, transform, roomId, doubleFire
59 std::vector<std::tuple<ecs::Entity,
62 float,
63 bool>> pendingChargedSpawns; // owner, transform, roomId, ratio, doubleFire
64
65 auto updatePlayerScore = [&](uint32_t roomId, ecs::Entity owner, int delta) {
66 if (delta == 0 || owner == ecs::NullEntity) {
67 return;
68 }
69 auto room = _roomSystem.getRoom(roomId);
70 if (!room) {
71 return;
72 }
73 const auto playersInRoom = room->getPlayers();
74 for (const auto &player : playersInRoom) {
75 if (!player) {
76 continue;
77 }
78 if (player->getEntityId() != static_cast<uint32_t>(owner.index())) {
79 continue;
80 }
81 player->addScore(delta);
83 net::ScoreUpdatePayload payload{player->getScore()};
84 packet << payload;
86 break;
87 }
88 };
89
90 constexpr float kChargeMax = 2.0f;
91 constexpr float kChargeMin = 0.2f;
92
93 auto view =
101
102 auto doubleFireRes = _registry.get<ecs::components::DoubleFire>();
103
104 // Get all component arrays
105 auto transformRes = _registry.get<ecs::components::Transform>();
107 auto typeRes = _registry.get<ecs::components::EntityType>();
108 auto roomIdRes = _registry.get<ecs::components::RoomId>();
109 auto weaponRes = _registry.get<ecs::components::SimpleWeapon>();
110 auto netIdRes = _registry.get<ecs::components::NetworkId>();
111 auto ammoRes = _registry.get<ecs::components::Ammo>();
112
113 if (!transformRes || !inputRes || !typeRes || !roomIdRes || !weaponRes || !netIdRes || !ammoRes)
114 return;
115
116 auto& transforms = transformRes->get();
117 auto& inputs = inputRes->get();
118 auto& types = typeRes->get();
119 auto& roomIds = roomIdRes->get();
120 auto& weapons = weaponRes->get();
121 auto& netIds = netIdRes->get();
122 auto& ammos = ammoRes->get();
123 auto healthRes = _registry.get<ecs::components::Health>();
125
126 for (const auto &entity : transforms.entities()) {
127 if (!inputs.has(entity) || !types.has(entity) || !roomIds.has(entity) ||
128 !weapons.has(entity) || !netIds.has(entity) || !ammos.has(entity)) {
129 continue;
130 }
131
132 auto& tf = transforms[entity];
133 auto& input = inputs[entity];
134 auto& type = types[entity];
135 auto& roomId = roomIds[entity];
136 auto& weapon = weapons[entity];
137 auto& net = netIds[entity];
138 auto& ammo = ammos[entity];
139 if (type.type != net::EntityType::Player)
140 continue;
141
142 // Update double fire timer
143 bool hasDoubleFire = false;
144 if (doubleFireRes) {
145 auto& doubleFires = doubleFireRes->get();
146 if (doubleFires.has(entity)) {
147 auto& doubleFire = doubleFires[entity];
148 doubleFire.remainingTime -= dt;
149 if (doubleFire.remainingTime <= 0.0f) {
151 } else {
152 hasDoubleFire = true;
153 }
154 }
155 }
156
157 weapon.lastShotTime += dt;
158
159 const bool hasInfiniteAmmo = (weapon.maxAmmo < 0);
160 const bool canShoot = !ammo.isReloading &&
161 (ammo.current > 0 || hasInfiniteAmmo);
162
163 // Update beam cooldown timer
164 if (weapon.beamCooldownRemaining > 0.0f) {
165 weapon.beamCooldownRemaining = std::max(0.0f, weapon.beamCooldownRemaining - dt);
166 }
167
168 // Beam active handling: apply periodic damage along a horizontal line in front of the player
169 if (weapon.kind == ecs::components::WeaponKind::Beam && weapon.beamActive) {
170 weapon.beamActiveTime -= dt;
171 // accumulate tick
172 auto &acc = _beamTickTimers[static_cast<uint32_t>(entity.index())];
173 acc += dt;
174 constexpr float kBeamTick = 0.2f; // apply damage every 0.2s
175 if (acc >= kBeamTick) {
176 acc -= kBeamTick;
177 // apply damage to entities in same room whose x is in front of player
178 if (healthRes && typeRes && transformRes && roomIdRes) {
179 auto &healths = healthRes->get();
180 auto *boxes = boxRes ? &boxRes->get() : nullptr;
181 auto room = _roomSystem.getRoom(roomId.id);
182 if (room) {
183 const auto players = room->getPlayers();
184 std::vector<uint32_t> sessions;
185 sessions.reserve(players.size());
186 for (const auto& p : players) sessions.push_back(p->getId());
187 for (auto target : healths.entities()) {
188 ecs::Entity t = target;
189 if (!transforms.has(t) || !types.has(t) || !roomIds.has(t) || !healths.has(t))
190 continue;
191 if (roomIds[t].id != roomId.id)
192 continue;
193 if (types[t].type == net::EntityType::Player)
194 continue;
195 // Only hit entities located in front of player (x greater)
196 const auto &ttf = transforms[t];
197 if (ttf.position.x <= tf.position.x)
198 continue;
199 // Y proximity check: support single or double beam offsets
200 std::vector<float> centers;
201 centers.push_back(tf.position.y);
202 if (weapon.beamWasDouble) {
203 centers.clear();
204 centers.push_back(tf.position.y - 4.0f);
205 centers.push_back(tf.position.y + 4.0f);
206 }
207
208 float halfH = 8.0f;
209 if (boxes && boxes->has(t)) {
210 halfH = (*boxes)[t].height * 0.5f;
211 }
212
213 // For each beam center, if within vertical range apply damage
214 for (float centerY : centers) {
215 if (std::fabs(ttf.position.y - centerY) > halfH + 4.0f)
216 continue;
217
218 // Apply damage for this beam
219 auto &ht = healths[t];
220 ht.currentHealth -= weapon.damage;
221 log::info("Beam: applied {} damage to entity {} (hp left={})", weapon.damage, t.index(), ht.currentHealth);
222 if (ht.currentHealth <= 0) {
223 const int award = getKillScore(types[t].type);
224 updatePlayerScore(roomId.id, entity, award);
225 // 30% chance to drop a power-up (beam kills should drop too)
226 const int dropChance = std::rand() % 100;
227 if (dropChance < 30) {
228 // spawn a powerup using this system's debug spawner
229 spawnDebugPowerup(transforms[t].position, roomId.id, dropChance);
230 }
231
232 // send death packet to players in room
235 if (auto netRes = _registry.get<ecs::components::NetworkId>()) {
236 auto &nets = netRes->get();
237 if (nets.has(t)) dp.netId = nets[t].id;
238 }
239 dp.type = static_cast<uint8_t>(types[t].type);
240 dp.position = transforms[t].position;
241 dpacket << dp;
243 _registry.kill(t);
244 break; // entity dead, stop processing centers
245 }
246 }
247 }
248 }
249 }
250 }
251 // If beam duration ended, stop beam and start cooldown
252 if (weapon.beamActiveTime <= 0.0f) {
253 weapon.beamActive = false;
254 weapon.beamCooldownRemaining = weapon.beamCooldown;
255 _beamTickTimers.erase(static_cast<uint32_t>(entity.index()));
256 // Notify clients in the room about beam end (visual removal)
257 auto roomForEnd = _roomSystem.getRoom(roomId.id);
258 if (roomForEnd) {
259 const auto playersForEnd = roomForEnd->getPlayers();
260 std::vector<uint32_t> sessionsForEnd;
261 sessionsForEnd.reserve(playersForEnd.size());
262 for (const auto &p : playersForEnd) sessionsForEnd.push_back(p->getId());
263
264 // send beam end notifications. If double fire, send two end packets
265 if (weapon.beamWasDouble) {
268 bp1.ownerNetId = net.id;
269 bp1.active = 0;
270 bp1.timeRemaining = 0.0f;
271 bp1.length = 0.0f;
272 bp1.offsetY = -4.0f;
273 b1 << bp1;
275
278 bp2.ownerNetId = net.id;
279 bp2.active = 0;
280 bp2.timeRemaining = 0.0f;
281 bp2.length = 0.0f;
282 bp2.offsetY = 4.0f;
283 b2 << bp2;
285 } else {
288 bp.ownerNetId = net.id;
289 bp.active = 0;
290 bp.timeRemaining = 0.0f;
291 bp.length = 0.0f;
292 bp.offsetY = 0.0f;
293 bpacket << bp;
295 }
296 }
297 // reset captured double-fire flag after beam ended
298 weapon.beamWasDouble = false;
299 }
300 }
301
303
304 // Debug: Spawn powerup on P key press
305 const bool debugPressed = (input.mask & Bits::DebugPowerup);
306 const bool debugWasPressed = (input.lastMask & Bits::DebugPowerup);
307
308 if (debugPressed && !debugWasPressed) {
309 Vec2f spawnPos = tf.position;
310 spawnPos.x += 50.0f;
311
312 // Spawn all 3 powerup types for testing
313 spawnDebugPowerup(spawnPos, roomId.id, 0); // Heal (red)
314 spawnPos.y += 30.0f;
315 spawnDebugPowerup(spawnPos, roomId.id, 15); // DoubleFire (yellow)
316 spawnPos.y += 30.0f;
317 spawnDebugPowerup(spawnPos, roomId.id, 25); // Shield (green)
318
319 log::info("[DEBUG] Spawned all 3 powerup types at player position");
320 }
321
322 // Progress reload only when beam is not active (pause reload during beam)
323 // Also do not progress reload if the equipped weapon is a boomerang
324 if (ammo.isReloading && !weapon.beamActive && !weapon.isBoomerang) {
325 ammo.reloadTimer += dt;
326 if (ammo.reloadTimer >= ammo.reloadCooldown) {
327 ammo.current = ammo.max;
328 ammo.isReloading = false;
329 ammo.reloadTimer = 0.0f;
330 ammo.dirty = true;
331 }
332 }
333
334 using Bits =
336
337 // Debug: Spawn powerup on P key
338 if ((input.mask & Bits::DebugPowerup) && !(input.lastMask & Bits::DebugPowerup)) {
339 // Spawn a random powerup at player position
340 const int debugRoll = std::rand() % 30; // 0-29 for all powerup types
341 Vec2f spawnPos = tf.position;
342 spawnPos.x += 50.0f; // Spawn slightly ahead of player
343 spawnDebugPowerup(spawnPos, roomId.id, debugRoll);
344 log::info("[DEBUG] Spawned powerup at player position (roll: {})", debugRoll);
345 }
346
347 // Do not allow starting a reload while beam is active
348 // Also disallow starting a reload if the current weapon is a boomerang
349 if ((input.mask & Bits::Reload) && !ammo.isReloading && ammo.current < ammo.max && !weapon.beamActive && !weapon.isBoomerang) {
350 ammo.isReloading = true;
351 ammo.reloadTimer = 0.0f;
352 ammo.dirty = true;
353 }
354
355 const bool shootPressed = (input.mask & Bits::Shoot);
356 const bool shootWasPressed = (input.lastMask & Bits::Shoot);
357
358 // Beam activation on press: if player has Beam weapon, not in cooldown and has ammo
359 if (weapon.kind == ecs::components::WeaponKind::Beam && shootPressed && !shootWasPressed) {
360 if (weapon.beamCooldownRemaining <= 0.0f && !weapon.beamActive && (ammo.current > 0 || hasInfiniteAmmo)) {
361 weapon.beamActive = true;
362 weapon.beamActiveTime = weapon.beamDuration;
363 // capture whether this beam instance was started with double-fire
364 weapon.beamWasDouble = hasDoubleFire;
365 // consume one ammo
366 if (!hasInfiniteAmmo && ammo.current > 0 && ammo.max > 0) {
367 ammo.current = static_cast<uint16_t>(std::max<int>(0, static_cast<int>(ammo.current) - 1));
368 ammo.dirty = true;
369 sendAmmoUpdate(net.id, ammo);
370 } else if (ammo.max == 0) {
371 // no ammo - cancel activation
372 weapon.beamActive = false;
373 }
374 // Notify clients in the room about beam start (visual)
375 {
376 auto room = _roomSystem.getRoom(roomId.id);
377 if (room) {
378 const auto players = room->getPlayers();
379 std::vector<uint32_t> sessions;
380 sessions.reserve(players.size());
381 for (const auto &p : players) sessions.push_back(p->getId());
382
383 // send beam start(s). If double fire is active, send two beams with vertical offsets
384 const float visualLength = 800.0f;
385 if (weapon.beamWasDouble) {
388 bp1.ownerNetId = net.id;
389 bp1.active = 1;
390 bp1.timeRemaining = weapon.beamActiveTime;
391 bp1.length = visualLength;
392 bp1.offsetY = -4.0f;
393 bpacket1 << bp1;
395
398 bp2.ownerNetId = net.id;
399 bp2.active = 1;
400 bp2.timeRemaining = weapon.beamActiveTime;
401 bp2.length = visualLength;
402 bp2.offsetY = 4.0f;
403 bpacket2 << bp2;
405 } else {
408 bp.ownerNetId = net.id;
409 bp.active = 1;
410 bp.timeRemaining = weapon.beamActiveTime;
411 bp.length = visualLength; // visual length in pixels (approx)
412 bp.offsetY = 0.0f;
413 bpacket << bp;
415 }
416 }
417 }
418 }
419 }
420
421 // Prevent normal shooting while beam active or in beam cooldown
422 if (weapon.kind == ecs::components::WeaponKind::Beam && (weapon.beamActive || weapon.beamCooldownRemaining > 0.0f)) {
423 // still update input state and ammo reloads, skip normal shot logic
424 if (!shootPressed && shootWasPressed) input.chargeTime = 0.0f;
425 input.lastMask = input.mask;
426 if (ammo.dirty) {
427 sendAmmoUpdate(net.id, ammo);
428 ammo.dirty = false;
429 }
430 continue;
431 }
432
433 if (ammo.isReloading || (ammo.current == 0 && !hasInfiniteAmmo)) {
434 input.chargeTime = 0.0f;
435 }
436
437 if (shootPressed && canShoot) {
438 input.chargeTime = std::min(input.chargeTime + dt, kChargeMax);
439 }
440
441 const float fireInterval = (weapon.fireRate > 0.0f)
442 ? (1.0f / weapon.fireRate)
443 : 0.0f;
444
445 if (!shootPressed && shootWasPressed) {
446 if (canShoot) {
447 if (input.chargeTime >= kChargeMin && weapon.kind != ecs::components::WeaponKind::Beam) {
448 const float ratio = std::clamp(input.chargeTime / kChargeMax, 0.0f, 1.0f);
449 pendingChargedSpawns.emplace_back(entity, tf, roomId, ratio, hasDoubleFire);
450 if (!hasInfiniteAmmo && ammo.current > 0) {
451 ammo.current -= 1;
452 }
453 ammo.dirty = true;
454 weapon.lastShotTime = 0.0f;
455 } else if (weapon.kind != ecs::components::WeaponKind::Beam && weapon.lastShotTime >= fireInterval) {
456 pendingSpawns.emplace_back(entity, tf, roomId, hasDoubleFire);
457 if (!hasInfiniteAmmo && ammo.current > 0) {
458 ammo.current -= 1;
459 }
460 ammo.dirty = true;
461 weapon.lastShotTime = 0.0f;
462 }
463 }
464 input.chargeTime = 0.0f;
465 }
466
467 if (!shootPressed && !shootWasPressed) {
468 input.chargeTime = 0.0f;
469 }
470
471 input.lastMask = input.mask;
472
473 if (ammo.dirty) {
474 sendAmmoUpdate(net.id, ammo);
475 ammo.dirty = false;
476 }
477 }
478
479 for (const auto& [owner, tf, roomId, doubleFire] : pendingSpawns) {
480 spawnBullet(owner, tf, roomId, doubleFire);
481 }
482
483 for (const auto& [owner, tf, roomId, ratio, doubleFire] : pendingChargedSpawns) {
484 spawnChargedBullet(owner, tf, roomId, ratio, doubleFire);
485 }
486 }
487
489 // Private API
491
493 ecs::Entity owner,
495 const ecs::components::RoomId& roomId,
496 bool doubleFire)
497 {
498 // Spawn first bullet
499 auto entityRes = _registry.spawn();
500 if (!entityRes) {
501 log::error("Failed to spawn bullet entity: {}", entityRes.error().message());
502 return;
503 }
504
505 ecs::Entity bullet = entityRes.value();
506
507 const float x = tf.position.x + _spawnOffsetX;
508 float y = tf.position.y;
509
510 // If double fire, offset vertically
511 if (doubleFire) {
512 y -= 4.0f; // Offset first bullet up by 4 pixels
513 }
514
516 bullet,
517 ecs::components::Transform{ {x, y}, 0.f, {1.f, 1.f} }
518 );
519
521 bullet,
523 );
524
525 // Use owner's weapon damage and size if available
526 int damageAmount = 25;
527 float boxW = 8.0f;
528 float boxH = 4.0f;
529 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
530 auto &weapons = weaponRes->get();
531 if (weapons.has(owner)) {
532 const auto &w = weapons[owner];
533 damageAmount = w.damage;
534 // Optionally adjust box size based on difficulty
535 const float diffScale = 1.0f + (w.difficulty - 2) * 0.1f;
536 boxW = 8.0f * diffScale;
537 boxH = 4.0f * diffScale;
538 }
539 }
540
542 bullet,
543 ecs::components::BoundingBox{ boxW, boxH }
544 );
545
547 bullet,
548 ecs::components::Damage{ damageAmount, owner }
549 );
550
552 bullet,
553 ecs::components::NetworkId{ static_cast<uint32_t>(bullet.index()) }
554 );
555
557 bullet,
559 );
560
562 bullet,
564 );
565
566 // Attach boomerang component for charged bullet if applicable
567 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
568 auto &weapons = weaponRes->get();
569 if (weapons.has(owner) && weapons[owner].isBoomerang) {
571 b.ownerIndex = static_cast<uint32_t>(owner.index());
572 b.startPos = {x, y};
573 b.maxDistance = 400.0f;
574 b.returning = false;
576 }
577 }
578
579 // Attach boomerang component if owner's weapon is a boomerang
580 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
581 auto &weapons = weaponRes->get();
582 if (weapons.has(owner) && weapons[owner].isBoomerang) {
584 b.ownerIndex = static_cast<uint32_t>(owner.index());
585 b.startPos = {x, y};
586 b.maxDistance = 400.0f; // default max distance, could be configurable
587 b.returning = false;
589 }
590 }
591
592 // Attach homing component for charged bullet if owner's weapon supports homing
593 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
594 auto &weapons = weaponRes->get();
595 if (weapons.has(owner) && weapons[owner].homing) {
597 h.steering = weapons[owner].homingSteering;
598 h.range = weapons[owner].homingRange;
600 }
601 }
602
603 // Attach homing component if owner's weapon supports homing
604 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
605 auto &weapons = weaponRes->get();
606 if (weapons.has(owner) && weapons[owner].homing) {
608 h.steering = weapons[owner].homingSteering;
609 h.range = weapons[owner].homingRange;
611 }
612 }
613
614 auto room = _roomSystem.getRoom(roomId.id);
615 if (!room)
616 return;
617 if (room->getState() != Room::State::InGame)
618 return;
619
620 const auto players = room->getPlayers();
621 std::vector<uint32_t> sessions;
622 sessions.reserve(players.size());
623 for (const auto& player : players) {
624 sessions.push_back(player->getId());
625 }
626
628 uint8_t weaponKind = 0;
629 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
630 auto &weapons = weaponRes->get();
631 if (weapons.has(owner)) {
632 weaponKind = static_cast<uint8_t>(weapons[owner].kind);
633 }
634 }
635
636 net::EntitySpawnPayload payload = {
637 static_cast<uint32_t>(bullet.index()),
638 static_cast<uint8_t>(net::EntityType::Bullet),
639 x,
640 y,
641 0.0f,
642 0.0f,
643 weaponKind
644 };
645 packet << payload;
647
648 // Spawn second bullet if double fire is active
649 if (doubleFire) {
650 auto entityRes2 = _registry.spawn();
651 if (!entityRes2) {
652 log::error("Failed to spawn second bullet entity: {}", entityRes2.error().message());
653 return;
654 }
655
656 ecs::Entity bullet2 = entityRes2.value();
657 const float y2 = tf.position.y + 4.0f; // Offset second bullet down by 4 pixels
658
660 bullet2,
661 ecs::components::Transform{ {x, y2}, 0.f, {1.f, 1.f} }
662 );
663
665 bullet2,
667 );
668
669 // second bullet uses same damage/size as first
671 bullet2,
672 ecs::components::BoundingBox{ boxW, boxH }
673 );
674
676 bullet2,
677 ecs::components::Damage{ damageAmount, owner }
678 );
679
681 bullet2,
682 ecs::components::NetworkId{ static_cast<uint32_t>(bullet2.index()) }
683 );
684
686 bullet2,
688 );
689
691 bullet2,
693 );
694
695
696
697 // Attach boomerang for second bullet if owner's weapon is boomerang
698 if (auto weaponRes2 = _registry.get<ecs::components::SimpleWeapon>()) {
699 auto &weapons = weaponRes2->get();
700 if (weapons.has(owner) && weapons[owner].isBoomerang) {
702 b2.ownerIndex = static_cast<uint32_t>(owner.index());
703 b2.startPos = {x, y2};
704 b2.maxDistance = 400.0f;
705 b2.returning = false;
707 }
708 }
709
710 // Attach homing for second bullet if applicable
711 if (auto weaponRes2 = _registry.get<ecs::components::SimpleWeapon>()) {
712 auto &weapons = weaponRes2->get();
713 if (weapons.has(owner) && weapons[owner].homing) {
715 h2.steering = weapons[owner].homingSteering;
716 h2.range = weapons[owner].homingRange;
718 }
719 }
720
722 uint8_t weaponKind2 = 0;
723 if (auto weaponRes2 = _registry.get<ecs::components::SimpleWeapon>()) {
724 auto &weapons = weaponRes2->get();
725 if (weapons.has(owner)) {
726 weaponKind2 = static_cast<uint8_t>(weapons[owner].kind);
727 }
728 }
729
730 net::EntitySpawnPayload payload2 = {
731 static_cast<uint32_t>(bullet2.index()),
732 static_cast<uint8_t>(net::EntityType::Bullet),
733 x,
734 y2,
735 0.0f,
736 0.0f,
737 weaponKind2
738 };
739 packet2 << payload2;
741 }
742 }
743
745 ecs::Entity owner,
747 const ecs::components::RoomId& roomId,
748 float chargeRatio,
749 bool doubleFire)
750 {
751 auto entityRes = _registry.spawn();
752 if (!entityRes) {
753 log::error("Failed to spawn charged bullet entity: {}", entityRes.error().message());
754 return;
755 }
756
757 ecs::Entity bullet = entityRes.value();
758
759 const float x = tf.position.x + _spawnOffsetX;
760 float y = tf.position.y;
761
762 // If double fire, offset vertically
763 if (doubleFire) {
764 y -= 4.0f; // Offset first bullet up by 4 pixels
765 }
766 const float ratio = std::clamp(chargeRatio, 0.0f, 1.0f);
767
768 // Determine discrete charge tiers:
769 // Tier 1: normal shot (no change)
770 // Tier 2: slightly larger hitbox, damage x2
771 // Tier 3: larger hitbox, damage x4
772 const float baseW = 8.0f;
773 const float baseH = 4.0f;
774
775 float tierScale = 1.0f;
776 int damageMultiplier = 1;
777
778 if (ratio < 0.34f) {
779 // Tier 1
780 tierScale = 1.0f;
781 damageMultiplier = 1;
782 } else if (ratio < 0.67f) {
783 // Tier 2
784 tierScale = 1.5f; // a bit bigger
785 damageMultiplier = 2;
786 } else {
787 // Tier 3
788 tierScale = 2.0f; // biggest
789 damageMultiplier = 4;
790 }
791
792 const float sizeX = baseW * tierScale;
793 const float sizeY = baseH * tierScale;
794
795 // Base damage comes from owner's weapon if available, otherwise fallback
796 int baseDamage = 25;
797 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
798 auto &weapons = weaponRes->get();
799 if (weapons.has(owner)) {
800 baseDamage = weapons[owner].damage;
801 }
802 }
803
804 int damage = baseDamage * damageMultiplier;
805
807 bullet,
808 ecs::components::Transform{ {x, y}, 0.f, {1.f, 1.f} }
809 );
810
812 bullet,
814 );
815
817 bullet,
818 ecs::components::BoundingBox{ sizeX, sizeY }
819 );
820
822 bullet,
823 ecs::components::Damage{ damage, owner }
824 );
825
827 bullet,
828 ecs::components::NetworkId{ static_cast<uint32_t>(bullet.index()) }
829 );
830
832 bullet,
834 );
835
837 bullet,
839 );
840
841 // Attach boomerang component for charged bullet if applicable
842 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
843 auto &weapons = weaponRes->get();
844 if (weapons.has(owner) && weapons[owner].isBoomerang) {
846 b.ownerIndex = static_cast<uint32_t>(owner.index());
847 b.startPos = {x, y};
848 b.maxDistance = 400.0f * tierScale; // farther for bigger tiers
849 b.returning = false;
851 }
852 }
853
854 // Attach homing component for charged bullet if owner's weapon supports homing
855 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
856 auto &weapons = weaponRes->get();
857 if (weapons.has(owner) && weapons[owner].homing) {
859 h.steering = weapons[owner].homingSteering;
860 h.range = weapons[owner].homingRange;
862 }
863 }
864
865 auto room = _roomSystem.getRoom(roomId.id);
866 if (!room)
867 return;
868 if (room->getState() != Room::State::InGame)
869 return;
870
871 const auto players = room->getPlayers();
872 std::vector<uint32_t> sessions;
873 sessions.reserve(players.size());
874 for (const auto& player : players) {
875 sessions.push_back(player->getId());
876 }
877
879 net::EntitySpawnPayload payload = {
880 static_cast<uint32_t>(bullet.index()),
881 static_cast<uint8_t>(net::EntityType::ChargedBullet),
882 x,
883 y,
884 sizeX,
885 sizeY
886 };
887 // Include owner's weapon kind for client-side visuals
888 if (auto weaponRes = _registry.get<ecs::components::SimpleWeapon>()) {
889 auto &weapons = weaponRes->get();
890 if (weapons.has(owner)) {
891 payload.weaponKind = static_cast<uint8_t>(weapons[owner].kind);
892 }
893 }
894 packet << payload;
896
897 // Spawn second charged bullet if double fire is active
898 if (doubleFire) {
899 auto entityRes2 = _registry.spawn();
900 if (!entityRes2) {
901 log::error("Failed to spawn second charged bullet entity: {}", entityRes2.error().message());
902 return;
903 }
904
905 ecs::Entity bullet2 = entityRes2.value();
906 const float y2 = tf.position.y + 4.0f; // Offset second bullet down by 4 pixels
907
909 bullet2,
910 ecs::components::Transform{ {x, y2}, 0.f, {1.f, 1.f} }
911 );
912
914 bullet2,
916 );
917
919 bullet2,
920 ecs::components::BoundingBox{ sizeX, sizeY }
921 );
922
924 bullet2,
925 ecs::components::Damage{ damage, owner }
926 );
927
929 bullet2,
930 ecs::components::NetworkId{ static_cast<uint32_t>(bullet2.index()) }
931 );
932
934 bullet2,
936 );
937
939 bullet2,
941 );
942
944 net::EntitySpawnPayload payload2 = {
945 static_cast<uint32_t>(bullet2.index()),
946 static_cast<uint8_t>(net::EntityType::ChargedBullet),
947 x,
948 y2,
949 sizeX,
950 sizeY,
951 0
952 };
953 if (auto weaponRes2 = _registry.get<ecs::components::SimpleWeapon>()) {
954 auto &weapons = weaponRes2->get();
955 if (weapons.has(owner)) {
956 payload2.weaponKind = static_cast<uint8_t>(weapons[owner].kind);
957 }
958 }
959 packet2 << payload2;
961 }
962 }
963
965 {
967 net::AmmoUpdatePayload payload{};
968 payload.current = ammo.current;
969 payload.max = ammo.max;
970 payload.isReloading = static_cast<uint8_t>(ammo.isReloading ? 1 : 0);
971 payload.cooldownRemaining = ammo.isReloading
972 ? (ammo.reloadCooldown - ammo.reloadTimer)
973 : 0.0f;
974 packet << payload;
976 }
977
978 void PlayerShootSystem::spawnDebugPowerup(const Vec2f& position, uint32_t roomId, int dropRoll)
979 {
980 auto entityRes = _registry.spawn();
981 if (!entityRes) {
982 log::error("Failed to spawn debug powerup: {}", entityRes.error().message());
983 return;
984 }
985
986 ecs::Entity e = entityRes.value();
987
988 _registry.add<ecs::components::Transform>(e, ecs::components::Transform{position, 0.0f, {1.0f, 1.0f}});
992
993 // Determine powerup type from dropRoll
994 net::EntityType entityType;
996
997 if (dropRoll < 10) {
998 entityType = net::EntityType::PowerupHeal;
1000 } else if (dropRoll < 20) {
1003 } else {
1004 entityType = net::EntityType::PowerupShield;
1006 }
1007
1009 _registry.add<ecs::components::Powerup>(e, ecs::components::Powerup{powerupType, 1.0f, 0.0f});
1011
1012 auto room = _roomSystem.getRoom(roomId);
1013 if (!room)
1014 return;
1015
1016 const auto players = room->getPlayers();
1017 std::vector<uint32_t> sessions;
1018 sessions.reserve(players.size());
1019 for (const auto& player : players) {
1020 sessions.push_back(player->getId());
1021 }
1022
1024 net::EntitySpawnPayload payload = {
1025 static_cast<uint32_t>(e.index()),
1026 static_cast<uint8_t>(entityType),
1027 position.x,
1028 position.y
1029 };
1030 packet << payload;
1032 }
1033} // namespace rtp::server
Logger declaration with support for multiple log levels.
Network packet implementation for R-Type protocol.
Represents an entity in the ECS (Entity-Component-System) architecture.
Definition Entity.hpp:63
constexpr std::uint32_t index(void) const
auto spawn(void) -> std::expected< Entity, rtp::Error >
Definition Registry.cpp:51
void kill(Entity entity)
Definition Registry.cpp:73
auto add(Entity entity, Args &&...args) -> std::expected< std::reference_wrapper< T >, rtp::Error >
auto get(this const Self &self) -> std::expected< std::reference_wrapper< ConstLike< Self, SparseArray< T > > >, rtp::Error >
auto zipView(this Self &self)
void remove(Entity entity) noexcept
Network packet with header and serializable body.
Definition Packet.hpp:471
System to handle network-related operations on the server side.
void sendPacketToSessions(const std::vector< uint32_t > &sessions, const net::Packet &packet, net::NetworkMode mode)
Send a packet to multiple sessions.
void sendPacketToSession(uint32_t sessionId, const net::Packet &packet, net::NetworkMode mode)
Send a packet to a specific session.
void sendPacketToEntity(uint32_t entityId, const net::Packet &packet, net::NetworkMode mode)
Send a packet to the entity associated with the given ID.
ecs::Registry & _registry
Reference to the entity registry.
float _bulletSpeed
Speed of the spawned bullets.
void sendAmmoUpdate(uint32_t netId, const ecs::components::Ammo &ammo)
Send an ammo update to the client for a specific network ID.
float _chargedBulletSpeed
Speed of the charged bullets.
void spawnDebugPowerup(const Vec2f &position, uint32_t roomId, int dropRoll)
Spawn a debug powerup for testing (triggered by P key)
RoomSystem & _roomSystem
Reference to the RoomSystem.
NetworkSyncSystem & _networkSync
Reference to the NetworkSyncSystem.
void spawnChargedBullet(ecs::Entity owner, const ecs::components::Transform &tf, const ecs::components::RoomId &roomId, float chargeRatio, bool doubleFire=false)
Spawn a charged bullet based on the player's transform and room ID.
std::unordered_map< uint32_t, float > _beamTickTimers
void spawnBullet(ecs::Entity owner, const ecs::components::Transform &tf, const ecs::components::RoomId &roomId, bool doubleFire=false)
Spawn a bullet entity based on the player's transform and room ID.
void update(float dt) override
Update player shoot system logic for one frame.
float _spawnOffsetX
X offset for bullet spawn position.
PlayerShootSystem(ecs::Registry &registry, RoomSystem &roomSystem, NetworkSyncSystem &networkSync)
Constructor for PlayerShootSystem.
System to handle room-related operations on the server side.
std::shared_ptr< Room > getRoom(uint32_t roomId)
Get a room by its ID.
@ InGame
Game in progress.
Definition Room.hpp:43
File : Network.hpp License: MIT Author : Elias Josué HAJJAR LLAUQUEN elias-josue.hajjar-llauquen@epit...
@ Beam
Continuous beam 5s active, 5s cooldown.
PowerupType
Supported powerup types.
Definition Powerup.hpp:16
constexpr Entity NullEntity
Definition Entity.hpp:109
void error(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an error message.
void info(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an informational message.
EntityType
Types of entities in the game.
Definition Packet.hpp:144
@ BeamState
Beam start/stop notification.
@ AmmoUpdate
Ammo update notification.
@ EntitySpawn
Entity spawn notification.
@ ScoreUpdate
Player score update.
@ EntityDeath
Entity death notification.
File : GameManager.hpp License: MIT Author : Elias Josué HAJJAR LLAUQUEN elias-josue....
Ammo tracking for weapons.
Definition Ammo.hpp:18
Marks a projectile as a boomerang and stores state for return logic.
Definition Boomerang.hpp:16
bool returning
Whether the boomerang is on its way back.
Definition Boomerang.hpp:20
uint32_t ownerIndex
ECS entity index of the owner who fired this boomerang.
Definition Boomerang.hpp:17
rtp::Vec2f startPos
Spawn position to compute travel distance.
Definition Boomerang.hpp:18
float maxDistance
Distance before returning.
Definition Boomerang.hpp:19
Component representing damage value.
Definition Damage.hpp:16
Component for double fire powerup.
Component representing an entity's health.
Definition Health.hpp:15
Marks an entity as homing and provides tuning parameters.
Definition Homing.hpp:17
float range
Detection range in pixels.
Definition Homing.hpp:19
float steering
Steering factor (per second) used to lerp direction toward target.
Definition Homing.hpp:18
Component representing a network identifier for an entity.
Definition NetworkId.hpp:22
Component representing a powerup pickup.
Definition Powerup.hpp:27
Component representing a network identifier for an entity.
Definition RoomId.hpp:22
uint32_t id
Unique network identifier for the entity.
Definition RoomId.hpp:23
Component representing a weapon configuration.
Component representing position, rotation, and scale of an entity.
Definition Transform.hpp:23
Vec2f position
X and Y coordinates.
Definition Transform.hpp:24
Component representing a 2D velocity.
Definition Velocity.hpp:17
Component to handle network input for server entities.
Ammo update notification data Server OpCode.
Definition Packet.hpp:374
uint16_t current
Current ammo.
Definition Packet.hpp:375
Notify clients that an entity's beam started or stopped.
Definition Packet.hpp:385
uint32_t ownerNetId
Network id of the player owning the beam.
Definition Packet.hpp:386
Entity death notification data.
Definition Packet.hpp:362
uint32_t netId
Network entity identifier.
Definition Packet.hpp:363
Entity spawn notification data.
Definition Packet.hpp:348
uint8_t weaponKind
Optional weapon kind for player entities.
Definition Packet.hpp:355
Player score update notification data.
Definition Packet.hpp:397