class_name CombatSystem extends RefCounted var game: Node3D const BLAST_RADIUS := 8.0 const BLAST_DAMAGE := 110.0 func _init(owner_game: Node3D) -> void: game = owner_game func actions(p: Fighter) -> void: if not game.is_host or not game.active or game.match_over or p.hp <= 0 or p.rope_active: return var cmd := p.input_data var fire: bool = cmd.get("fire", false) var pressed := fire and not p.trigger_held p.trigger_held = fire if cmd.get("ultimate", false): game.streaks.place(p) cmd["ultimate"] = false if cmd.get("shield", false): EnergyShield.activate(p) cmd["shield"] = false if cmd.get("melee", false): melee(p) cmd["melee"] = false if p.melee_left > 0 or p.mantle_left > 0 or p.rope_active: return if cmd.get("switch", false): p.weapon = 4 if p.weapon == p.primary else p.primary p.aim_blend = 0 p.reload_left = 0 p.cooldown = maxf(p.cooldown, float(WeaponHandling.DATA[p.weapon].equip)) cmd["switch"] = false if p.infinite_left <= 0 and (cmd.get("reload", false) or p.magazines[p.weapon] == 0) and p.reload_left <= 0 and p.magazines[p.weapon] < p.weapon_stats().mag and p.reserves[p.weapon] > 0: p.reload_left = p.weapon_stats().reload game.fx.rpc("reload", p.eye(), p.peer_id, 0) cmd["reload"] = false if cmd.get("grenade", false) and p.grenades > 0: p.grenades -= 1 p.protection = 0 var dir := Arsenal.direction(p.yaw, p.pitch) game.spawn_grenade(p.peer_id, p.eye() + dir * 0.6, dir * 17 + Vector3.UP * 4) cmd["grenade"] = false if cmd.get("flash", false) and p.flashes > 0: p.flashes -= 1 p.protection = 0 var dir := Arsenal.direction(p.yaw, p.pitch) game.spawn_grenade(p.peer_id, p.eye() + dir * 0.6, dir * 16 + Vector3.UP * 4, "flash") cmd["flash"] = false if fire and (not p.weapon_stats().get("semi", false) or pressed or p.bot) and p.cooldown <= 0 and p.reload_left <= 0: if p.magazines[p.weapon] > 0: shoot(p) else: cmd["reload"] = true func shoot(p: Fighter) -> void: p.radar_left = 0.0 if WeaponAttachments.clean(p.skin_designs.get(str(p.weapon), {}).get("attachments", {})).get("muzzle", 0) == 1 else 1.5 var w: Dictionary = p.weapon_stats() p.shot_serial += 1 if p.infinite_left <= 0: p.magazines[p.weapon] -= 1 p.cooldown = w.rate p.protection = 0 var hit_any := false var head_any := false for pellet in int(w.pellets): var spread: float = w.spread if p.aiming: spread *= 0.025 if p.weapon == 3 else (0.85 if w.pellets > 1 else 0.2) if p.velocity.length() > 4: spread *= 1.6 var direction := Arsenal.direction(p.yaw + randf_range(-spread, spread), p.pitch + randf_range(-spread, spread)) var route := ShotOrigin.path(p, direction, w.range) var start: Vector3 = route.start var end: Vector3 = route.end var hit: Dictionary = route.hit if not route.blocked and not p.bot and p.peer_id != 1 and p.peer_id in game.multiplayer.get_peers() and not game.history.frames.is_empty(): var peer: ENetPacketPeer = game.multiplayer.multiplayer_peer.get_peer(p.peer_id) if peer != null: var latency := minf(0.2, peer.get_statistic(ENetPacketPeer.PEER_ROUND_TRIP_TIME) / 2000.0) hit = game.history.rewind_hit(p, start, end, latency) hit = EnergyShield.clip_ray(game, p, start, end, hit) if not hit.is_empty(): end = hit.position if pellet == 0: game.impact.rpc(end) if hit.collider is Fighter: var target: Fighter = hit.collider var relative_height: float = hit.position.y - hit.get("origin", target.global_position).y var head := relative_height > (1.0 if target.crouched else 1.48) var multiplier := 1.65 if head else (0.78 if relative_height < 0.7 else 1.0) var distance := start.distance_to(end) var falloff := lerpf(1, 0.55, clampf((distance - float(w.range) * 0.3) / (float(w.range) * 0.7), 0, 1)) if damage(target, p, float(w.damage) * multiplier * falloff, w.short, head, start): hit_any = true head_any = head_any or head if pellet == 0: game.muzzle_trace.rpc(p.peer_id, start, end) game.shot_event.rpc(p.eye(), p.peer_id, p.weapon, hit_any and not p.bot, head_any) if p.bot: p.pitch += float(w.recoil) * 0.3 func damage(target: Fighter, source: Fighter, amount: float, weapon_name: String, head: bool, origin: Vector3 = Vector3.INF) -> bool: if target.hp <= 0 or target.protection > 0: return false if target != source and not game.enemies(target, source): return false var incoming := origin if origin.is_finite() else source.eye() if EnergyShield.blocks(target, incoming): return false target.hp = maxf(0, target.hp - amount) target.last_hurt = 0 if not target.bot and (target.peer_id == game.multiplayer.get_unique_id() or target.peer_id in game.multiplayer.get_peers()): game.combat_notice.rpc_id(target.peer_id, "hurt", target.life, origin if origin.is_finite() else source.eye()) if target.hp <= 0: target.deaths += 1 target.streak = 0 target.infinite_left = 0 target.recon_left = 0 target.shield_left = 0 game.objectives.death(target) game.ammo_drops.drop(target) target.respawn_left = 3 target.killer = source.nickname target.killer_weapon = weapon_name target.killer_id = source.peer_id if source != target else 0 target.shape.set_deferred("disabled", true) if source != target: source.kills += 1 game.streaks.earned(source) if not source.bot and (source.peer_id == game.multiplayer.get_unique_id() or source.peer_id in game.multiplayer.get_peers()): game.combat_notice.rpc_id(source.peer_id, "elimination", source.life, target.eye(), target.nickname) if game.config.mode in ["TDM", "FFA"]: game.scores[source.team] += 1 game.kill_notice.rpc(source.nickname, target.nickname, weapon_name, head) game.fx.rpc("death", target.eye(), target.peer_id, 0) game.check_win() return true func explode(pos: Vector3, owner_id: int) -> void: if not game.is_host or not game.active or game.match_over: return game.detonate.rpc(pos) if not game.players.has(owner_id): return game.burn_zones.ignite(pos, owner_id) var source: Fighter = game.players[owner_id] for target: Fighter in game.players.values(): var distance := pos.distance_to(target.eye()) if distance < BLAST_RADIUS and game.visible_between(pos + Vector3.UP * 0.15, target.eye(), [target.get_rid()]): damage(target, source, lerpf(BLAST_DAMAGE, 0, distance / BLAST_RADIUS), "FRAG", false, pos) func flash(pos: Vector3) -> void: if not game.is_host or not game.active or game.match_over: return game.fx.rpc("flash", pos, 0, 0) for target: Fighter in game.players.values(): var offset := pos - target.eye() var distance := offset.length() if target.hp <= 0 or EnergyShield.blocks(target, pos) or distance > 20 or not game.visible_between(pos, target.eye(), [target.get_rid()]): continue var facing := Arsenal.direction(target.yaw, target.pitch).dot(offset.normalized()) var strength := (1 - distance / 20) * (1.0 if facing > 0.2 else 0.3) target.flash_left = maxf(target.flash_left, strength * 4) func melee(p: Fighter) -> void: if not game.is_host or not game.active or game.match_over or p.hp <= 0 or p.melee_left > 0 or p.mantle_left > 0 or p.rope_active: return p.melee_left = 0.65 p.aiming = false p.cooldown = maxf(p.cooldown, p.melee_left) p.reload_left = 0 p.protection = 0 game.fx.rpc("melee", p.eye(), p.peer_id, 0) var forward := Arsenal.direction(p.yaw, p.pitch) var nearest: Fighter = null var best := 2.1 for target: Fighter in game.players.values(): if target == p or target.hp <= 0 or not game.enemies(p, target): continue var point := target.global_position + Vector3.UP * (0.75 if target.crouched else 1.2) var offset := point - p.eye() if offset.length() >= best or forward.dot(offset.normalized()) < 0.65: continue var ray := PhysicsRayQueryParameters3D.create(p.eye(), point, 3, [p.get_rid()]) var hit := game.get_world_3d().direct_space_state.intersect_ray(ray) if not hit.is_empty() and hit.collider == target: nearest = target best = offset.length() if nearest != null and damage(nearest, p, 65, "MELEE", false) and not p.bot: game.feedback.rpc_id(p.peer_id, false)