73 auto &transforms = transformsRes->get();
74 auto &boxes = boxesRes->get();
75 auto &types = typesRes->get();
76 auto &rooms = roomsRes->get();
77 auto &healths = healthRes->get();
78 auto &speeds = speedRes->get();
79 auto &powerups = powerupRes->get();
80 auto &damages = damageRes->get();
84 auto *velocities = velocitiesRes ? &velocitiesRes->get() :
nullptr;
89 std::unordered_set<uint32_t> removed;
90 std::vector<std::pair<ecs::Entity, uint32_t>> pending;
91 std::vector<ecs::Entity> players;
92 std::vector<ecs::Entity> enemies;
93 std::vector<ecs::Entity> obstacles;
94 std::vector<ecs::Entity> bullets;
95 std::vector<ecs::Entity> enemyBullets;
96 std::vector<ecs::Entity> powerupEntities;
98 auto markForDespawn = [&](
ecs::Entity entity, uint32_t roomId) {
99 if (removed.find(entity.
index()) != removed.end()) {
102 removed.insert(entity.
index());
103 pending.emplace_back(entity, roomId);
106 auto updatePlayerScore = [&](uint32_t roomId,
ecs::Entity playerEntity,
int delta) {
114 const auto playersInRoom = room->getPlayers();
115 for (
const auto &player : playersInRoom) {
119 if (player->getEntityId() !=
static_cast<uint32_t
>(playerEntity.index())) {
122 player->addScore(delta);
136 const auto playersInRoom = room->getPlayers();
137 for (
const auto &player : playersInRoom) {
141 if (player->getEntityId() !=
static_cast<uint32_t
>(playerEntity.index())) {
152 auto sendGameOverIfNeeded = [&](
const std::shared_ptr<Room> &room) {
159 if (room->hasActivePlayers()) {
163 room->forceFinishGame();
165 const auto playersInRoom = room->getPlayers();
166 if (playersInRoom.empty()) {
170 std::string bestName;
172 bool bestSet =
false;
174 for (
const auto &player : playersInRoom) {
178 const int score = player->getScore();
179 if (!bestSet || score > bestScore) {
182 bestName = player->getUsername();
186 for (
const auto &player : playersInRoom) {
192 std::strncpy(payload.bestPlayer, bestName.c_str(),
sizeof(payload.bestPlayer) - 1);
193 payload.bestPlayer[
sizeof(payload.bestPlayer) - 1] =
'\0';
194 payload.bestScore = bestScore;
195 payload.playerScore = player->getScore();
201 for (
auto entity : types.entities()) {
202 if (!types.has(entity)) {
205 const auto type = types[entity].type;
207 if (transforms.has(entity) &&
210 healths.has(entity) &&
211 speeds.has(entity)) {
212 players.push_back(entity);
224 if (transforms.has(entity) &&
227 healths.has(entity)) {
228 enemies.push_back(entity);
233 if (transforms.has(entity) &&
236 healths.has(entity)) {
237 obstacles.push_back(entity);
242 if (transforms.has(entity) &&
245 damages.has(entity)) {
246 bullets.push_back(entity);
249 if (transforms.has(entity) &&
252 damages.has(entity)) {
253 enemyBullets.push_back(entity);
260 if (transforms.has(entity) &&
263 powerups.has(entity)) {
264 powerupEntities.push_back(entity);
269 for (
auto player : players) {
270 auto &ptf = transforms[player];
271 auto &pbox = boxes[player];
272 auto &proom = rooms[player];
273 auto &health = healths[player];
274 auto &speed = speeds[player];
275 auto *pvel = (velocities && velocities->has(player)) ? &(*velocities)[player] :
nullptr;
277 for (
auto powerEntity : powerupEntities) {
278 if (removed.find(powerEntity.index()) != removed.end()) {
281 if (rooms[powerEntity].
id != proom.id) {
285 const auto &tf = transforms[powerEntity];
286 const auto &box = boxes[powerEntity];
287 if (!
overlaps(ptf, pbox, tf, box)) {
291 const auto &powerup = powerups[powerEntity];
292 log::info(
"=== Power-up collision! Type: {} ===",
static_cast<int>(powerup.type));
296 int oldHealth = health.currentHealth;
297 if (health.currentHealth < health.maxHealth) {
298 health.currentHealth = std::min(
300 health.currentHealth + 1);
301 sendPlayerHealth(proom.id, player, health);
302 log::info(
"✚ HEAL: {} HP → {} HP (max: {})", oldHealth, health.currentHealth, health.maxHealth);
304 log::info(
"✚ HEAL: Already at max health ({}/{}), not applied", health.currentHealth, health.maxHealth);
308 std::max(speed.multiplier, powerup.value);
309 speed.boostRemaining =
310 std::max(speed.boostRemaining, powerup.duration);
311 log::info(
"⚡ SPEED: Multiplier={}, Duration={}s", speed.multiplier, speed.boostRemaining);
314 if (doubleFiresRes) {
315 auto& doubleFires = doubleFiresRes->get();
316 if (!doubleFires.has(player)) {
318 log::info(
"🔫 DOUBLE FIRE: Added component (20s duration)");
320 doubleFires[player].remainingTime = 20.0f;
321 log::info(
"🔫 DOUBLE FIRE: Reset timer to 20s");
324 log::warning(
"DoubleFire component storage not available!");
329 auto& shields = shieldsRes->get();
330 if (!shields.has(player)) {
332 log::info(
"🛡️ SHIELD: Added component (1 charge)");
334 shields[player].charges = 1;
335 log::info(
"🛡️ SHIELD: Reset to 1 charge");
338 log::warning(
"Shield component storage not available!");
342 log::info(
"Despawning power-up entity {}", powerEntity.index());
343 markForDespawn(powerEntity, proom.id);
347 for (
auto player : players) {
348 auto &ptf = transforms[player];
349 auto &pbox = boxes[player];
350 auto &proom = rooms[player];
351 auto *pvel = (velocities && velocities->has(player)) ? &(*velocities)[player] :
nullptr;
353 for (
auto obstacle : obstacles) {
354 if (rooms[obstacle].
id != proom.id) {
357 const auto &otf = transforms[obstacle];
358 const auto &obox = boxes[obstacle];
359 if (!
overlaps(ptf, pbox, otf, obox)) {
363 const float px = ptf.position.x;
364 const float py = ptf.position.y;
365 const float pw = pbox.width;
366 const float ph = pbox.height;
368 const float ox = otf.position.x;
369 const float oy = otf.position.y;
370 const float ow = obox.width;
371 const float oh = obox.height;
373 const float overlapX1 = (px + pw) - ox;
374 const float overlapX2 = (ox + ow) - px;
375 const float overlapY1 = (py + ph) - oy;
376 const float overlapY2 = (oy + oh) - py;
378 const float minOverlapX =
379 (overlapX1 < overlapX2) ? overlapX1 : -overlapX2;
380 const float minOverlapY =
381 (overlapY1 < overlapY2) ? overlapY1 : -overlapY2;
383 if (std::abs(minOverlapX) < std::abs(minOverlapY)) {
384 ptf.position.x -= minOverlapX;
386 pvel->direction.x = 0.0f;
389 ptf.position.y -= minOverlapY;
391 pvel->direction.y = 0.0f;
397 for (
auto bullet : bullets) {
398 if (removed.find(bullet.index()) != removed.end()) {
401 const auto &btf = transforms[bullet];
402 const auto &bbox = boxes[bullet];
403 const auto &broom = rooms[bullet];
404 const auto &damage = damages[bullet];
407 for (
auto enemy : enemies) {
408 if (rooms[enemy].
id != broom.id) {
411 const auto &etf = transforms[enemy];
412 const auto &ebox = boxes[enemy];
413 auto &health = healths[enemy];
414 if (!
overlaps(btf, bbox, etf, ebox)) {
422 for (
auto potentialShield : enemies) {
423 if (rooms[potentialShield].
id == broom.id &&
425 healths[potentialShield].currentHealth > 0) {
431 if (shieldCount > 0) {
432 bool isBoomer = (boomerResLocal && boomerResLocal->get().has(bullet));
435 markForDespawn(bullet, broom.id);
444 health.currentHealth -= damage.amount;
445 bool isBoomer = (boomerResLocal && boomerResLocal->get().has(bullet));
447 markForDespawn(bullet, broom.id);
449 if (health.currentHealth <= 0) {
450 const int award = getKillScore(types[enemy].type);
451 updatePlayerScore(broom.id, damage.sourceEntity, award);
453 const int dropChance = std::rand() % 100;
454 if (dropChance < 30) {
457 markForDespawn(enemy, broom.id);
462 if (removed.find(bullet.index()) != removed.end()) {
466 for (
auto obstacle : obstacles) {
467 if (rooms[obstacle].
id != broom.id) {
470 const auto &otf = transforms[obstacle];
471 const auto &obox = boxes[obstacle];
472 auto &health = healths[obstacle];
473 if (!
overlaps(btf, bbox, otf, obox)) {
478 health.currentHealth -= damage.amount;
480 bool isBoomerObs = (boomerResLocal && boomerResLocal->get().has(bullet));
482 markForDespawn(bullet, broom.id);
484 if (types[obstacle].type ==
486 health.currentHealth <= 0) {
487 markForDespawn(obstacle, broom.id);
495 auto &boomers = boomResCheck->get();
496 for (
auto bullet : bullets) {
497 if (removed.find(bullet.index()) != removed.end())
continue;
498 if (!boomers.has(bullet))
continue;
499 auto &b = boomers[bullet];
500 if (!b.returning)
continue;
502 const auto &btf = transforms[bullet];
503 const auto &bbox = boxes[bullet];
506 for (
auto player : players) {
507 if (rooms[player].
id != rooms[bullet].
id)
continue;
508 if (
static_cast<uint32_t
>(player.index()) != b.ownerIndex)
continue;
509 const auto &ptf = transforms[player];
510 const auto &pbox = boxes[player];
511 if (!
overlaps(ptf, pbox, btf, bbox))
continue;
515 auto &ammos = ammoRes->get();
516 if (ammos.has(player)) {
517 if (ammos[player].max > 0) {
518 ammos[player].current =
static_cast<uint16_t
>(std::min<int>(ammos[player].max, ammos[player].current + 1));
520 ammos[player].dirty =
true;
525 payload.
current = ammos[player].current;
526 payload.max = ammos[player].max;
527 payload.isReloading =
static_cast<uint8_t
>(ammos[player].isReloading ? 1 : 0);
528 payload.cooldownRemaining = ammos[player].isReloading ? (ammos[player].reloadCooldown - ammos[player].reloadTimer) : 0.0f;
531 auto &nets = netRes->get();
532 if (nets.has(player)) {
539 markForDespawn(bullet, rooms[bullet].
id);
545 for (
auto bullet : enemyBullets) {
546 if (removed.find(bullet.index()) != removed.end()) {
549 const auto &btf = transforms[bullet];
550 const auto &bbox = boxes[bullet];
551 const auto &broom = rooms[bullet];
552 const auto &damage = damages[bullet];
554 for (
auto player : players) {
555 if (rooms[player].
id != broom.id) {
558 const auto &ptf = transforms[player];
559 const auto &pbox = boxes[player];
560 auto &health = healths[player];
561 if (!
overlaps(btf, bbox, ptf, pbox)) {
566 bool shieldBlocked =
false;
568 auto& shields = shieldsRes->get();
569 if (shields.has(player) && shields[player].charges > 0) {
571 shields[player].charges--;
572 if (shields[player].charges <= 0) {
576 log::info(
"Player shield blocked attack ({} charges left)", shields[player].charges);
578 shieldBlocked =
true;
584 health.currentHealth =
585 std::max(0, health.currentHealth - damage.amount);
586 sendPlayerHealth(broom.id, player, health);
588 markForDespawn(bullet, broom.id);
593 const auto playersInRoom = room->getPlayers();
594 for (
const auto &roomPlayer : playersInRoom) {
598 if (roomPlayer->getEntityId() ==
static_cast<uint32_t
>(player.index())) {
600 roomPlayer->setEntityId(0);
605 sendGameOverIfNeeded(room);
607 markForDespawn(player, broom.id);
613 for (
const auto &[entity, roomId] : pending) {