63 bool>> pendingChargedSpawns;
65 auto updatePlayerScore = [&](uint32_t roomId,
ecs::Entity owner,
int delta) {
73 const auto playersInRoom = room->getPlayers();
74 for (
const auto &player : playersInRoom) {
78 if (player->getEntityId() !=
static_cast<uint32_t
>(owner.index())) {
81 player->addScore(delta);
90 constexpr float kChargeMax = 2.0f;
91 constexpr float kChargeMin = 0.2f;
113 if (!transformRes || !inputRes || !typeRes || !roomIdRes || !weaponRes || !netIdRes || !ammoRes)
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();
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)) {
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];
143 bool hasDoubleFire =
false;
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) {
152 hasDoubleFire =
true;
157 weapon.lastShotTime += dt;
159 const bool hasInfiniteAmmo = (weapon.maxAmmo < 0);
160 const bool canShoot = !ammo.isReloading &&
161 (ammo.current > 0 || hasInfiniteAmmo);
164 if (weapon.beamCooldownRemaining > 0.0f) {
165 weapon.beamCooldownRemaining = std::max(0.0f, weapon.beamCooldownRemaining - dt);
170 weapon.beamActiveTime -= dt;
174 constexpr float kBeamTick = 0.2f;
175 if (acc >= kBeamTick) {
178 if (healthRes && typeRes && transformRes && roomIdRes) {
179 auto &healths = healthRes->get();
180 auto *boxes = boxRes ? &boxRes->get() :
nullptr;
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()) {
189 if (!transforms.has(t) || !types.has(t) || !roomIds.has(t) || !healths.has(t))
191 if (roomIds[t].
id != roomId.id)
196 const auto &ttf = transforms[t];
197 if (ttf.position.x <= tf.position.x)
200 std::vector<float> centers;
201 centers.push_back(tf.position.y);
202 if (weapon.beamWasDouble) {
204 centers.push_back(tf.position.y - 4.0f);
205 centers.push_back(tf.position.y + 4.0f);
209 if (boxes && boxes->has(t)) {
210 halfH = (*boxes)[t].height * 0.5f;
214 for (
float centerY : centers) {
215 if (std::fabs(ttf.position.y - centerY) > halfH + 4.0f)
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);
226 const int dropChance = std::rand() % 100;
227 if (dropChance < 30) {
236 auto &nets = netRes->get();
237 if (nets.has(t)) dp.
netId = nets[t].id;
239 dp.type =
static_cast<uint8_t
>(types[t].type);
240 dp.position = transforms[t].position;
252 if (weapon.beamActiveTime <= 0.0f) {
253 weapon.beamActive =
false;
254 weapon.beamCooldownRemaining = weapon.beamCooldown;
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());
265 if (weapon.beamWasDouble) {
270 bp1.timeRemaining = 0.0f;
280 bp2.timeRemaining = 0.0f;
290 bp.timeRemaining = 0.0f;
298 weapon.beamWasDouble =
false;
305 const bool debugPressed = (input.mask & Bits::DebugPowerup);
306 const bool debugWasPressed = (input.lastMask & Bits::DebugPowerup);
308 if (debugPressed && !debugWasPressed) {
309 Vec2f spawnPos = tf.position;
319 log::info(
"[DEBUG] Spawned all 3 powerup types at player position");
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;
338 if ((input.mask & Bits::DebugPowerup) && !(input.lastMask & Bits::DebugPowerup)) {
340 const int debugRoll = std::rand() % 30;
341 Vec2f spawnPos = tf.position;
344 log::info(
"[DEBUG] Spawned powerup at player position (roll: {})", debugRoll);
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;
355 const bool shootPressed = (input.mask & Bits::Shoot);
356 const bool shootWasPressed = (input.lastMask & Bits::Shoot);
360 if (weapon.beamCooldownRemaining <= 0.0f && !weapon.beamActive && (ammo.current > 0 || hasInfiniteAmmo)) {
361 weapon.beamActive =
true;
362 weapon.beamActiveTime = weapon.beamDuration;
364 weapon.beamWasDouble = hasDoubleFire;
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));
370 }
else if (ammo.max == 0) {
372 weapon.beamActive =
false;
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());
384 const float visualLength = 800.0f;
385 if (weapon.beamWasDouble) {
390 bp1.timeRemaining = weapon.beamActiveTime;
391 bp1.length = visualLength;
400 bp2.timeRemaining = weapon.beamActiveTime;
401 bp2.length = visualLength;
410 bp.timeRemaining = weapon.beamActiveTime;
411 bp.length = visualLength;
424 if (!shootPressed && shootWasPressed) input.chargeTime = 0.0f;
425 input.lastMask = input.mask;
433 if (ammo.isReloading || (ammo.current == 0 && !hasInfiniteAmmo)) {
434 input.chargeTime = 0.0f;
437 if (shootPressed && canShoot) {
438 input.chargeTime = std::min(input.chargeTime + dt, kChargeMax);
441 const float fireInterval = (weapon.fireRate > 0.0f)
442 ? (1.0f / weapon.fireRate)
445 if (!shootPressed && shootWasPressed) {
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) {
454 weapon.lastShotTime = 0.0f;
456 pendingSpawns.emplace_back(entity, tf, roomId, hasDoubleFire);
457 if (!hasInfiniteAmmo && ammo.current > 0) {
461 weapon.lastShotTime = 0.0f;
464 input.chargeTime = 0.0f;
467 if (!shootPressed && !shootWasPressed) {
468 input.chargeTime = 0.0f;
471 input.lastMask = input.mask;
479 for (
const auto& [owner, tf, roomId, doubleFire] : pendingSpawns) {
483 for (
const auto& [owner, tf, roomId, ratio, doubleFire] : pendingChargedSpawns) {
501 log::error(
"Failed to spawn bullet entity: {}", entityRes.error().message());
526 int damageAmount = 25;
530 auto &weapons = weaponRes->get();
531 if (weapons.has(owner)) {
532 const auto &w = weapons[owner];
533 damageAmount = w.damage;
535 const float diffScale = 1.0f + (w.difficulty - 2) * 0.1f;
536 boxW = 8.0f * diffScale;
537 boxH = 4.0f * diffScale;
568 auto &weapons = weaponRes->get();
569 if (weapons.has(owner) && weapons[owner].isBoomerang) {
581 auto &weapons = weaponRes->get();
582 if (weapons.has(owner) && weapons[owner].isBoomerang) {
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;
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;
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());
628 uint8_t weaponKind = 0;
630 auto &weapons = weaponRes->get();
631 if (weapons.has(owner)) {
632 weaponKind =
static_cast<uint8_t
>(weapons[owner].kind);
637 static_cast<uint32_t
>(bullet.
index()),
652 log::error(
"Failed to spawn second bullet entity: {}", entityRes2.error().message());
699 auto &weapons = weaponRes2->get();
700 if (weapons.has(owner) && weapons[owner].isBoomerang) {
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;
722 uint8_t weaponKind2 = 0;
724 auto &weapons = weaponRes2->get();
725 if (weapons.has(owner)) {
726 weaponKind2 =
static_cast<uint8_t
>(weapons[owner].kind);
731 static_cast<uint32_t
>(bullet2.
index()),