extends Node3D const PORT := 27840 var port := PORT var config := {"server_name": "Relay / Local", "mode": "TDM", "map": "quarry", "max_players": 8, "bots": 7, "difficulty": 0, "score_limit": 50, "time_limit": 600} var active := false var is_host := false var dedicated := false var local_id := 1 var players := {} var scores := [0, 0] var time_left := 600.0 var match_over := false var winner := "" var arena: RelayArena var hit_stop: HitStop var combat: CombatSystem var bots: BotDirector var audio: ArenaAudio var discovery: LanDiscovery var ui: GameUI var hud: ArenaHUD var menu_camera: Camera3D var spectator_camera: Camera3D var death_camera_active := false var death_camera_target_id := 0 var death_camera_fallback := Transform3D.IDENTITY var death_camera_look := Vector3.ZERO var grenades: Node3D var effects: Node3D var projectiles: Node3D var objectives: RoundObjectives var history: CombatHistory var ammo_drops: AmmoDrops var streaks: Killstreaks var burn_zones: BurnZones var grenade_serial := 0 var snapshot_clock := 0.0 var snapshot_serial := 0 var snapshot_fragments := SnapshotFragments.new() var input_clock := 0.0 var input_sequence := 0 var pending := {} var sensitivity := 0.002 var base_fov := 88.0 var nickname := "Operator" var operator_id := "steve" var loadout := 0 var paused := false var graphics_quality := 1 var headless := false var connect_clock := 0.0 var connecting := false var last_address := "127.0.0.1:27840" var action_sequence := 0 var ready_for_play := false func _ready() -> void: hit_stop = HitStop.new() add_child(hit_stop) Input.use_accumulated_input = false headless = DisplayServer.get_name() == "headless" dedicated = "--dedicated" in OS.get_cmdline_user_args() if not headless: PlayerSettings.restore(self) ClassLoadouts.restore(loadout) history = CombatHistory.new() history.game = self add_child(history) setup_inputs() arena = RelayArena.new() add_child(arena) GraphicsSettings.apply(self, graphics_quality) grenades = Node3D.new() grenades.name = "Grenades" add_child(grenades) effects = Node3D.new() effects.name = "CombatEffects" add_child(effects) projectiles = Node3D.new() projectiles.name = "Projectiles" add_child(projectiles) objectives = RoundObjectives.new() objectives.game = self add_child(objectives) objectives.reset() combat = CombatSystem.new(self) for hazard in arena.find_children("*", "Node3D", true, false): if hazard is WreckFire: hazard.damage_tick.connect(_on_wreck_tick) burn_zones = BurnZones.new() burn_zones.game = self add_child(burn_zones) ammo_drops = AmmoDrops.new() ammo_drops.game = self add_child(ammo_drops) streaks = Killstreaks.new() streaks.game = self add_child(streaks) bots = BotDirector.new(self) audio = ArenaAudio.new() add_child(audio) var music := MusicDirector.new() music.game = self add_child(music) discovery = LanDiscovery.new() discovery.game = self add_child(discovery) menu_camera = Camera3D.new() add_child(menu_camera) menu_camera.position = Vector3(26, 13, 32) menu_camera.look_at(Vector3(-2, 1.2, -4)) menu_camera.current = true spectator_camera = Camera3D.new() spectator_camera.name = "DeathCamera" spectator_camera.near = 0.04 spectator_camera.fov = 78 add_child(spectator_camera) ui = GameUI.new() ui.game = self ui.host_requested.connect(host) ui.join_requested.connect(join) ui.leave_requested.connect(leave) ui.preferences_changed.connect(save_preferences) add_child(ui) hud = ArenaHUD.new() hud.game = self add_child(hud) multiplayer.connected_to_server.connect(_connected) multiplayer.connection_failed.connect(func(): leave("Verbindung fehlgeschlagen. IP und Firewall prüfen.")) multiplayer.server_disconnected.connect(func(): leave("Host hat das Spiel beendet.")) multiplayer.peer_disconnected.connect(_disconnected) await ShaderPrewarm.run(self) ready_for_play = true var args := OS.get_cmdline_user_args() if dedicated: var server := DedicatedServer.new() server.game = self add_child(server) server.start(args) return for arg in args: if arg.begins_with("--port="): port = int(arg.get_slice("=", 1)) for arg in args: if arg.begins_with("--map="): config["map"] = arg.trim_prefix("--map=") if "--host" in args: host(config) for arg in args: if arg.begins_with("--join="): join(arg.get_slice("=", 1)) if "--smoke" in args: var timer := get_tree().create_timer(12) timer.timeout.connect(func(): print("SMOKE active=", active, " players=", players.size(), " scores=", scores) get_tree().quit(0 if active and players.size() > 1 else 1)) func setup_inputs() -> void: if headless: ControlBindings.defaults() else: ControlBindings.setup() func select_map(id: String) -> void: if arena.map_id == id: return remove_child(arena) arena.free() arena = MapCatalog.create(id) add_child(arena) bots.routes = BotRoutes.new(arena) for hazard in arena.find_children("*", "Node3D", true, false): if hazard is WreckFire: hazard.damage_tick.connect(_on_wreck_tick) GraphicsSettings.apply(self, graphics_quality) objectives.reset() if id == "dust2": menu_camera.position = Vector3(35, 27, 35) menu_camera.look_at(Vector3(0, 3, -10)) else: menu_camera.position = Vector3(26, 13, 32) menu_camera.look_at(Vector3(-2, 1.2, -4)) func host(settings: Dictionary, as_dedicated: bool = false) -> bool: var selected_map := str(settings.get("map", "quarry")) if selected_map not in MapCatalog.IDS: ui.message("Unbekannte Map: " + selected_map) return false dedicated = as_dedicated var peer := ENetMultiplayerPeer.new() if dedicated: peer.set_bind_ip(settings.get("bind", "0.0.0.0")) var err := peer.create_server(port, 8 if dedicated else 7) if err != OK: ui.message("Port %s ist belegt oder nicht verfügbar (Fehler %s)." % [port, err]) if dedicated: printerr("SERVER ERROR: UDP %s konnte nicht gebunden werden (Fehler %s)." % [port, err]) return false config = settings.duplicate() config["map"] = selected_map select_map(selected_map) config["difficulty"] = clampi(int(config.get("difficulty", 0)), 0, 3) config.max_players = clampi(int(config.max_players), 2, 8) config.bots = clampi(int(config.bots), 0, config.max_players if dedicated else config.max_players - 1) config.score_limit = clampi(int(config.score_limit), 1, 200) config.time_limit = clampi(int(config.time_limit), 60, 1800) multiplayer.multiplayer_peer = peer local_id = 0 if dedicated else 1 is_host = true active = true match_over = false winner = "" scores = [0, 0] time_left = config.time_limit if not dedicated: add_player(1, nickname, false, loadout) fill_bots() objectives.reset() enter_match() return true func join(address: String) -> void: last_address = address.strip_edges() save_preferences() var host_address := address.strip_edges() if host_address.contains(":"): port = int(host_address.get_slice(":", 1)) host_address = host_address.get_slice(":", 0) if not host_address.is_valid_ip_address() or port < 1024 or port > 65535: ui.message("Bitte eine gültige lokale IPv4-Adresse eingeben.") return var peer := ENetMultiplayerPeer.new() var err := peer.create_client(host_address, port) if err != OK: ui.message("Verbindung konnte nicht gestartet werden: %s" % err) return multiplayer.multiplayer_peer = peer is_host = false connecting = true connect_clock = 0 ui.message("Verbinde mit %s …" % host_address) func _connected() -> void: local_id = multiplayer.get_unique_id() register_player.rpc_id(1, nickname, loadout, WeaponSkins.loadout_designs(loadout), WeaponStickers.bundle(WeaponSkins.loadout_designs(loadout)), operator_id) @rpc("any_peer", "call_remote", "reliable") func register_player(display_name: String, selected: int, cosmetics: Dictionary = {}, sticker_files: Dictionary = {}, appearance: String = "steve") -> void: if not is_host or not active: return var id := multiplayer.get_remote_sender_id() if players.has(id): return var humans := 0 for p: Fighter in players.values(): if not p.bot: humans += 1 if humans >= config.max_players: rejected.rpc_id(id, "Server ist voll.") return for p: Fighter in players.values(): if p.bot and (dedicated or players.size() >= config.max_players): remove_player(p.peer_id) break var joined := add_player(id, display_name.strip_edges().left(20), false, Arsenal.primary_id(selected), appearance) joined.skin_designs = WeaponSkins.clean_loadout(cosmetics, joined.primary) for index in [joined.primary, 4]: joined.magazines[index] = WeaponAttachments.stats(index, joined.skin_designs.get(str(index), {})).mag var accepted_stickers := WeaponStickers.accept(sticker_files) if not accepted_stickers.is_empty(): receive_stickers.rpc(accepted_stickers) for member: Fighter in players.values(): var files := WeaponStickers.bundle(member.skin_designs) if not files.is_empty(): receive_stickers.rpc_id(id, files) welcome.rpc_id(id, config) print("JOIN accepted ", id, " roster=", players.size()) @rpc("authority", "call_remote", "reliable") func rejected(reason: String) -> void: leave(reason) @rpc("authority", "call_remote", "reliable") func welcome(settings: Dictionary) -> void: var selected_map := str(settings.get("map", "quarry")) if selected_map not in MapCatalog.IDS: leave("Die Map des Hosts ist in dieser Spielversion nicht vorhanden.") return config = settings select_map(selected_map) active = true connecting = false match_over = false enter_match() func enter_match() -> void: ui.hide_menu() paused = false if not headless: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func add_player(id: int, display_name: String, ai: bool, selected: int, appearance: String = "steve") -> Fighter: var counts := [0, 0] for p: Fighter in players.values(): counts[p.team] += 1 var p := Fighter.new() var model := ("gas_mask" if posmod(id, 2) == 1 else "steve") if ai else (operator_id if id == local_id else appearance) p.setup(self, id, display_name if not display_name.is_empty() else "Operator", 0 if counts[0] <= counts[1] else 1, ai, selected, model) p.respawn_requested.connect(respawn) p.combat_requested.connect(combat.actions) p.bot_update_requested.connect(bots.update) p.fall_damage_requested.connect(func(actor: Fighter): combat.damage(actor, actor, 200, "FALL", false)) p.footstep_requested.connect(func(pos: Vector3, actor_id: int): fx.rpc("step", pos, actor_id, 0)) players[id] = p add_child(p) respawn(p) return p func fill_bots() -> void: var target: int = mini(config.max_players, config.bots + (0 if dedicated else 1)) var id := -1 while players.size() < target: while players.has(id): id -= 1 add_player(id, ["Rook", "Mica", "Cinder", "Atlas", "Echo", "Vale", "Finch"][(-id - 1) % 7] + " [BOT]", true, Arsenal.PRIMARY_IDS[(-id - 1) % Arsenal.PRIMARY_IDS.size()]) func restart_round() -> void: if not is_host or not dedicated or not active: return history.stop() history.frames.clear() scores = [0, 0] time_left = config.time_limit winner = "" match_over = false for grenade in grenades.get_children(): grenades.remove_child(grenade) grenade.queue_free() burn_zones.clear() streaks.clear() ammo_drops.clear() for effect in effects.get_children(): effects.remove_child(effect) effect.queue_free() objectives.reset() for projectile in projectiles.get_children(): projectiles.remove_child(projectile) projectile.queue_free() for fighter: Fighter in players.values(): fighter.kills = 0 fighter.streak = 0 fighter.ult_charge = 0 fighter.infinite_left = 0 fighter.recon_left = 0 fighter.deaths = 0 respawn(fighter) snapshot_clock = 0.05 func remove_player(id: int) -> void: bots.steering.erase(id) if players.has(id): players[id].queue_free() players.erase(id) func _disconnected(id: int) -> void: if is_host: remove_player(id) fill_bots() func leave(reason: String = "") -> void: snapshot_fragments = SnapshotFragments.new() hit_stop.restore() history.stop() history.frames.clear() death_camera_active = false death_camera_target_id = 0 active = false connecting = false match_over = false if multiplayer.multiplayer_peer: multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() is_host = false for id in players.keys(): remove_player(id) for grenade in grenades.get_children(): grenade.queue_free() burn_zones.clear() streaks.clear() ammo_drops.clear() for effect in effects.get_children(): effects.remove_child(effect) effect.queue_free() objectives.reset() for projectile in projectiles.get_children(): projectiles.remove_child(projectile) projectile.queue_free() menu_camera.current = true Input.mouse_mode = Input.MOUSE_MODE_VISIBLE ui.main_menu() ui.message(reason) func enemies(a: Fighter, b: Fighter) -> bool: return a != b and (config.mode == "FFA" or a.team != b.team) func death_camera_killer(victim: Fighter) -> Fighter: if victim.hp > 0 or victim.killer_id == 0 or victim.killer_id == victim.peer_id: return null var target: Fighter = players.get(victim.killer_id) if not is_instance_valid(target) or target.is_queued_for_deletion() or target.hp <= 0: return null return target func is_killcam_visible(victim: Fighter) -> bool: return not headless and not match_over and death_camera_active and spectator_camera.current \ and death_camera_target_id == victim.killer_id and death_camera_killer(victim) != null # Returns true while normal first-person camera animation must be suppressed. # Only the local Fighter calls this; remote/bot cameras never become current. func update_death_camera(victim: Fighter, delta: float) -> bool: if headless or paused: return true if not active or not victim.is_local(): return true if victim.hp > 0 or match_over: history.stop() death_camera_active = false death_camera_target_id = 0 victim.camera.make_current() return false if not death_camera_active: history.begin(victim.killer_id) death_camera_active = true death_camera_target_id = 0 # Capture once: even later authoritative corpse corrections cannot move it. death_camera_fallback = victim.camera.global_transform death_camera_fallback.origin = victim.eye() + Vector3.UP * 0.5 var target := death_camera_killer(victim) if target == null: history.stop() death_camera_target_id = 0 spectator_camera.global_transform = death_camera_fallback else: if history.advance(delta, spectator_camera): death_camera_target_id = target.peer_id return true spectator_camera.fov = 78 var pivot := target.eye() var forward := Arsenal.direction(target.yaw, target.pitch) var shoulder := Basis(Vector3.UP, target.yaw) * Vector3.RIGHT * 0.45 var destination := pivot - forward * 2.4 + Vector3.UP * 0.5 + shoulder if death_camera_target_id != target.peer_id: # Cut on entry; never fly across the map from the victim to the killer. spectator_camera.global_position = destination death_camera_look = pivot else: var weight := 1.0 - exp(-delta * 8.0) spectator_camera.global_position = spectator_camera.global_position.lerp(destination, weight) death_camera_look = death_camera_look.lerp(pivot, weight) spectator_camera.look_at(death_camera_look) death_camera_target_id = target.peer_id spectator_camera.make_current() return true func visible_between(a: Vector3, b: Vector3, exclude: Array = []) -> bool: var rids: Array[RID] = [] rids.assign(exclude) var ray := PhysicsRayQueryParameters3D.create(a, b, 1, rids) return get_world_3d().direct_space_state.intersect_ray(ray).is_empty() func respawn(p: Fighter) -> void: if not p.pending_class.is_empty(): p.primary = p.pending_class.primary p.skin_designs = p.pending_class.skins p.pending_class = {} p.world_weapon = -1 var best := arena.spawn_points[0] var best_score := -INF for candidate in arena.spawn_points: var score := randf_range(0, 2) var home := candidate.z > 0 if p.team == 0 else candidate.z < 0 if home and config.mode != "FFA": score += 12 for other: Fighter in players.values(): if other == p or other.hp <= 0: continue var distance := candidate.distance_to(other.global_position) if distance < 2: score -= 1000 if enemies(p, other): score -= maxf(0, 30 - distance) * 3 if visible_between(candidate + Vector3.UP * 1.65, other.eye()): score -= 65 if other.cooldown > 0 and distance < 25: score -= 25 if score > best_score: best_score = score best = candidate p.reset_at(best) func _unhandled_input(event: InputEvent) -> void: if ShaderPrewarm.running: return if event.is_action_pressed("fullscreen") and not event.is_echo(): var window := get_window() window.mode = Window.MODE_WINDOWED if window.mode == Window.MODE_FULLSCREEN else Window.MODE_FULLSCREEN save_preferences() if event.is_action_pressed("pause") and not event.is_echo() and active: paused = not paused Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if paused else Input.MOUSE_MODE_CAPTURED if paused: ui.pause_menu() else: ui.hide_menu() if not active or paused or match_over or not players.has(local_id): return var p: Fighter = players[local_id] if p.hp <= 0: return if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: FpsCamera.mouse(p, event, sensitivity) for action in ["jump", "reload", "grenade", "flash", "switch", "melee", "interact", "shield", "ultimate"]: if event.is_action_pressed(action): pending[action] = true func _physics_process(dt: float) -> void: if connecting: connect_clock += dt if connect_clock > 10: leave("Zeitüberschreitung. Host-IP, Port und Netzwerk prüfen.") if not active: return if players.has(local_id) and not players[local_id].bot: var p: Fighter = players[local_id] var command := {"move": Vector2.ZERO, "yaw": p.yaw, "pitch": p.pitch} if p.hp > 0 and not paused and not match_over and not headless: command.merge({"move": Input.get_vector("left", "right", "forward", "back"), "sprint": Input.is_action_pressed("sprint"), "crouch": Input.is_action_pressed("crouch"), "ads": Input.is_action_pressed("aim"), "fire": Input.is_action_pressed("fire")}, true) if is_host: command.merge(pending) elif not pending.is_empty(): action_sequence += 1 submit_actions.rpc_id(1, action_sequence, pending) pending.clear() p.input_data = command.duplicate() p.input_age = 0 if not is_host: input_sequence += 1 submit_input.rpc_id(1, input_sequence, command) if is_host: ammo_drops.advance(dt) streaks.advance(dt) burn_zones.advance(dt) objectives.advance(dt) if not match_over: time_left = maxf(0, time_left - dt) if time_left <= 0 and streaks.nuke_left <= 0: finish_match() snapshot_clock += dt if snapshot_clock >= 0.05: snapshot_clock = 0 var roster := [] for p: Fighter in players.values(): roster.append(p.snapshot()) history.record(roster) var frags := [] for g: FragGrenade in grenades.get_children(): frags.append({"id": int(g.name), "p": g.global_position, "owner": g.owner_id, "kind": g.kind}) var packet := var_to_bytes([roster, scores, time_left, match_over, winner, frags, burn_zones.snapshot(), objectives.snapshot(), streaks.snapshot(), ammo_drops.rows]).compress(FileAccess.COMPRESSION_DEFLATE) snapshot_serial += 1 var count := ceili(float(packet.size()) / SnapshotFragments.PART_BYTES) for index in count: receive_state_part.rpc(snapshot_serial, index, count, packet.slice(index * SnapshotFragments.PART_BYTES, (index + 1) * SnapshotFragments.PART_BYTES)) @rpc("any_peer", "call_remote", "unreliable_ordered", 1) func submit_input(seq: int, command: Dictionary) -> void: if not is_host or not active: return var id := multiplayer.get_remote_sender_id() if not players.has(id): return var p: Fighter = players[id] if seq <= p.last_sequence: return var axis: Variant = command.get("move", Vector2.ZERO) var y: Variant = command.get("yaw", 0.0) var tilt: Variant = command.get("pitch", 0.0) if not axis is Vector2 or not axis.is_finite() or not (y is float or y is int) or not (tilt is float or tilt is int): return if not is_finite(float(y)) or not is_finite(float(tilt)): return p.last_sequence = seq p.yaw = wrapf(float(y), -PI, PI) p.pitch = clampf(float(tilt), -1.5, 1.5) var clean := {"move": axis.limit_length()} for flag in ["sprint", "crouch", "ads", "fire"]: clean[flag] = command.get(flag, false) == true p.input_data = clean p.input_age = 0 @rpc("any_peer", "call_remote", "reliable", 3) func submit_actions(seq: int, actions: Dictionary) -> void: if not is_host or not active or match_over: return var p: Fighter = players.get(multiplayer.get_remote_sender_id()) if p == null or p.hp <= 0 or seq <= p.last_action_sequence: return p.last_action_sequence = seq for action in ["jump", "reload", "grenade", "flash", "switch", "melee", "interact", "shield", "ultimate"]: if actions.get(action, false) == true: p.queued_actions[action] = true func save_preferences() -> void: if not headless and not dedicated: if PlayerSettings.save(self) != OK and is_instance_valid(ui): ui.message("Einstellungen konnten nicht gespeichert werden.") @rpc("authority", "call_remote", "unreliable_ordered", 2) func receive_state_part(serial: int, index: int, count: int, bytes: PackedByteArray) -> void: if not active: return var packet := snapshot_fragments.accept(serial, index, count, bytes) if not packet.is_empty(): receive_state(packet) @rpc("authority", "call_remote", "unreliable_ordered", 2) func receive_state(packet: PackedByteArray) -> void: if not active: return var decoded: Array = bytes_to_var(packet.decompress_dynamic(65536, FileAccess.COMPRESSION_DEFLATE)) if decoded.size() < 7 or decoded.size() > 10: return var roster: Array = decoded[0] history.record(roster) var team_scores: Array = decoded[1] var remaining: float = decoded[2] var ended: bool = decoded[3] var result: String = decoded[4] var new_round := match_over and not ended if new_round: history.stop() history.frames.clear() var frags: Array = decoded[5] burn_zones.sync(decoded[6]) if decoded.size() > 7: objectives.sync(decoded[7]) if decoded.size() > 8: streaks.sync(decoded[8]) if decoded.size() > 9: ammo_drops.sync(decoded[9]) scores = team_scores time_left = remaining match_over = ended winner = result var seen := [] for state: Dictionary in roster: seen.append(state.id) if not players.has(state.id): var p := Fighter.new() p.setup(self, state.id, state.name, state.team, state.bot, state.primary, str(state.get("operator", "steve"))) players[state.id] = p add_child(p) p.position = state.p players[state.id].apply_snapshot(state) if new_round and state.id == local_id: players[state.id].global_position = state.p players[state.id].velocity = Vector3.ZERO players[state.id].yaw = state.yaw players[state.id].pitch = state.pitch for id in players.keys(): if not id in seen: remove_player(id) var frag_ids := [] for data: Dictionary in frags: frag_ids.append(str(data.id)) if not grenades.has_node(str(data.id)): create_grenade(data.id, data.owner, data.p, Vector3.ZERO, data.get("kind", "frag")) grenades.get_node(str(data.id)).position = data.p for g in grenades.get_children(): if not str(g.name) in frag_ids: g.queue_free() func check_win() -> void: if streaks.nuke_left > 0 or match_over: return if config.mode != "FFA": if scores.max() >= config.score_limit: finish_match() else: for p: Fighter in players.values(): if p.kills >= config.score_limit: finish_match() func finish_match() -> void: match_over = true if config.mode != "FFA": winner = "DRAW" if scores[0] == scores[1] else ("TEAM RELAY WINS" if scores[0] > scores[1] else "TEAM EMBER WINS") else: var best := -1 var leaders := [] for p: Fighter in players.values(): if p.kills > best: best = p.kills leaders = [p.nickname] elif p.kills == best: leaders.append(p.nickname) winner = "%s WINS" % leaders[0] if leaders.size() == 1 else "DRAW" func spawn_grenade(owner_id: int, pos: Vector3, speed: Vector3, kind: String = "frag") -> void: grenade_serial += 1 create_grenade(grenade_serial, owner_id, pos, speed, kind) func _on_grenade_detonation(pos: Vector3, owner_id: int, kind: String, grenade_id: int) -> void: if kind == "flash": combat.flash(pos) else: combat.explode(pos, owner_id) remove_grenade.rpc(grenade_id) func create_grenade(id: int, owner_id: int, pos: Vector3, speed: Vector3, kind: String = "frag") -> void: var g := FragGrenade.new() g.game = self g.detonation_requested.connect(_on_grenade_detonation) g.owner_id = owner_id g.kind = kind g.fuse = 1.6 if kind == "flash" else 2.6 g.name = str(id) grenades.add_child(g) g.position = pos g.linear_velocity = speed g.angular_velocity = Vector3(4, 2, 3) @rpc("authority", "call_local", "reliable") func remove_grenade(id: int) -> void: var g := grenades.get_node_or_null(str(id)) if g: g.queue_free() @rpc("authority", "call_local", "unreliable") func fx(kind: String, pos: Vector3, id: int, variant: int) -> void: audio.play_at(kind, pos, id == local_id, variant) if kind == "shot": ShotFeedback.present(self, id, variant, false, false) if kind == "flash" and not headless: CombatVisuals.flash(effects, pos) if kind == "explosion" and not headless: CombatVisuals.explosion(effects, pos) # One reliable event keeps flash, recoil, shot audio and confirmed hits together. @rpc("authority", "call_local", "reliable") func shot_event(pos: Vector3, id: int, weapon: int, hit: bool, head: bool) -> void: audio.play_at("shot", pos, id == local_id, weapon, id) ShotFeedback.present(self, id, weapon, hit, head) @rpc("authority", "call_local", "reliable") func detonate(pos: Vector3) -> void: audio.play_at("explosion", pos, false) if not headless: CombatVisuals.explosion(effects, pos) if players.has(local_id): var p: Fighter = players[local_id] p.blast_shake = maxf(p.blast_shake, clampf(1 - p.eye().distance_to(pos) / 28.0, 0, 1)) @rpc("authority", "call_local", "unreliable") func impact(pos: Vector3) -> void: if not headless: CombatVisuals.impact(effects, pos) @rpc("authority", "call_local", "unreliable") func tracer(a: Vector3, b: Vector3) -> void: if headless or a.distance_to(b) < 0.01: return var line := MeshInstance3D.new() var mesh := BoxMesh.new() mesh.size = Vector3(0.012, 0.012, a.distance_to(b)) line.mesh = mesh var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.albedo_color = Color("ffe3a5") line.material_override = mat effects.add_child(line) line.position = (a + b) / 2 line.look_at(b, Vector3.UP if absf((b - a).normalized().y) < 0.99 else Vector3.RIGHT) get_tree().create_timer(0.045).timeout.connect(line.queue_free) @rpc("authority", "call_local", "reliable") func feedback(head: bool) -> void: if active: hit_stop.trigger() hud.hit_time = 0.18 hud.head_hit = head audio.play_at("hit", Vector3.ZERO, true) @rpc("authority", "call_local", "reliable") func kill_notice(attacker: String, victim: String, weapon_name: String, head: bool) -> void: hud.feed.push_front({"text": "%s › %s › %s%s" % [attacker, weapon_name, victim, " [HS]" if head else ""], "until": Time.get_ticks_msec() + 5500}) if hud.feed.size() > 5: hud.feed.pop_back() @rpc("authority", "call_local", "reliable") func combat_notice(kind: String, actor_life: int, origin: Vector3, victim: String = "") -> void: var p: Fighter = players.get(local_id) if not active or p == null or p.life != actor_life: return hud.notice_life = actor_life if kind == "hurt": hud.hurt_origin = origin hud.hurt_time = 0.9 elif kind == "elimination": hud.elimination_name = victim hud.elimination_time = 2.0 audio.play_at("confirm", p.eye(), true) @rpc("authority", "call_local", "unreliable") func muzzle_trace(id: int, start: Vector3, end: Vector3) -> void: if headless: return var p: Fighter = players.get(id) if p != null: start = ShotOrigin.visual(p) var wall := get_world_3d().direct_space_state.intersect_ray(PhysicsRayQueryParameters3D.create(p.eye(), start, 1)) if not wall.is_empty(): start = wall.position tracer(start, end) @rpc("authority", "call_local", "reliable") func nuke_effect() -> void: streaks.flash_left = 3 if not headless: var point := Vector3(0, 12, 0) CombatVisuals.explosion(effects, point) for i in 12: var direction := Vector3(cos(i * TAU / 12), 0, sin(i * TAU / 12)) CombatVisuals.puff(effects, point + Vector3.UP * 14 + direction * 12, 16, "smoke", 7, direction * 18 + Vector3.UP * 12) CombatVisuals.puff(effects, point + direction * 5, 12, "fireball", 2, direction * 12 + Vector3.UP * 8) audio.play_at("explosion", menu_camera.global_position, true) @rpc("authority", "call_remote", "reliable") func receive_stickers(files: Dictionary) -> void: WeaponStickers.accept(files) WeaponSkins.revision += 1 for member: Fighter in players.values(): member.world_weapon = -1 func queue_local_class() -> void: if not active: return var skins := WeaponSkins.loadout_designs(loadout) if is_host: if players.has(local_id): players[local_id].pending_class = {"primary": loadout, "skins": skins} var files := WeaponStickers.bundle(skins) if not files.is_empty(): receive_stickers.rpc(files) else: request_class.rpc_id(1, loadout, skins, WeaponStickers.bundle(skins)) @rpc("any_peer", "call_remote", "reliable") func request_class(selected: int, cosmetics: Dictionary, files: Dictionary) -> void: var id := multiplayer.get_remote_sender_id() if not is_host or not players.has(id): return var p: Fighter = players[id] var now := Time.get_ticks_msec() p.class_update_time = now var primary := Arsenal.primary_id(selected) p.pending_class = {"primary": primary, "skins": WeaponSkins.clean_loadout(cosmetics, primary)} var accepted := WeaponStickers.accept(files) if not accepted.is_empty(): receive_stickers.rpc(accepted) func _on_wreck_tick(origin: Vector3) -> void: if not active or not is_host or match_over: return for actor: Fighter in players.values(): if actor.hp > 0 and actor.eye().distance_to(origin) < 2 and visible_between(origin, actor.eye(), [actor.get_rid()]): combat.damage(actor, actor, 5, "WRECK FIRE", false, origin)