diff --git a/releases/Brogether-Brogether-v0.1.6.zip b/releases/Brogether-Brogether-v0.1.6.zip new file mode 100644 index 0000000..19690c0 Binary files /dev/null and b/releases/Brogether-Brogether-v0.1.6.zip differ diff --git a/src/mods-unpacked/Brogether-Brogether/brotatogether_options.gd b/src/mods-unpacked/Brogether-Brogether/brotatogether_options.gd new file mode 100644 index 0000000..2752d6d --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/brotatogether_options.gd @@ -0,0 +1,19 @@ +extends Node + +# TODO rename options to globals + +var joining_multiplayer_lobby : bool = false +var is_solo_test : bool = false + +var in_multiplayer_game : bool = false + +var current_network_id : int = 0 + +var batched_enemy_deaths = {} +var batched_unit_flashes = {} +var batched_floating_text = [] +var batched_hit_particles = [] +var batched_explosions = [] +var batched_hit_effects = [] +var batched_sounds = [] +var batched_2d_sounds = [] diff --git a/src/mods-unpacked/Brogether-Brogether/client/client_attack_behavior.gd b/src/mods-unpacked/Brogether-Brogether/client/client_attack_behavior.gd new file mode 100644 index 0000000..92b761c --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/client/client_attack_behavior.gd @@ -0,0 +1,17 @@ +extends AttackBehavior + +signal shot +signal finished_shooting +signal entered_long_cooldown +signal started_shooting +signal move_unlocked +signal wanted_to_spawn_an_enemy(enemy_scene, position) + +# check out all the nothing this enemy will do! +var max_range +var charge_duration +var charge_speed +var long_cooldown = 1.0 +var cooldown = 1.0 +var _current_cd = 1.0 +var number_projectiles diff --git a/src/mods-unpacked/Brogether-Brogether/client/client_main.gd b/src/mods-unpacked/Brogether-Brogether/client/client_main.gd new file mode 100644 index 0000000..772c690 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/client/client_main.gd @@ -0,0 +1,504 @@ +extends Node + +var ClientMovementBehavior = load("res://mods-unpacked/Brogether-Brogether/client/client_movement_behavior.gd") + +signal consumable_to_process_added(consumable_data) +signal upgrade_to_process_added(consumable_data) + +export (PackedScene) var gold_bag_scene:PackedScene +export (PackedScene) var gold_scene:PackedScene +export (PackedScene) var consumable_scene:PackedScene +export (Array) var gold_sprites:Array +export (Array, Resource) var gold_pickup_sounds:Array +export (Array, Resource) var gold_alt_pickup_sounds:Array +export (Resource) var level_up_sound:Resource +export (Array, Resource) var run_won_sounds:Array +export (Array, Resource) var run_lost_sounds:Array +export (Array, Resource) var end_wave_sounds:Array + +const EDGE_SIZE = 96 +const MAX_GOLDS = 50 +const MIN_MAP_SIZE = 12 + +const MOUSE_DIST_FROM_PLAYER_MANUAL_AIM = 200 + +var _cleaning_up: = false +var _golds: = [] +var _consumables: = [] +var _upgrades_to_process: = [] +var _consumables_to_process: = [] +var _end_wave_timer_timedout: = false + +var _player:Player +var _is_run_lost:bool +var _is_run_won:bool +var _gold_bag:Node + +var _landmine_timer:Timer = null +var _is_chal_ui_displayed = false + +var _last_rjoy_input:Vector2 = Vector2.ZERO + +var _proj_on_death_stat_cache:Dictionary = {} +var _items_spawned_this_wave: = 0 + +var _nb_bosses_killed_this_wave: = 0 +var _update_stats_on_gold_changed: = false +var _update_stats_on_enemies_changed: = false +var _update_stats_on_enemies_changed_timer:Timer = null +var _last_gold_amount_used_to_reload_stats: = 0 +var _elite_killed_bonus: = 0 +var _elite_killed: = false + +var _convert_stats_half_wave_proced: = false + +onready var _entities_container = $Entities +onready var _effects_manager = $EffectsManager +onready var _wave_manager = $WaveManager +onready var _floating_text_manager = $FloatingTextManager +onready var _camera = $Camera +onready var _screenshaker = $Camera / Screenshaker +onready var _items_container = $Items +onready var _consumables_container = $Consumables +onready var _births_container = $Births +onready var _pause_menu = $UI / PauseMenu +onready var _end_wave_timer = $EndWaveTimer +onready var _upgrades_ui = $UI / UpgradesUI as UpgradesUI +onready var _item_box_ui = $UI / ItemBoxUI as ItemBoxUI +onready var _wave_timer = $WaveTimer +onready var _player_life_bar = $"%PlayerLifeBar" +onready var _player_life_bar_container = $PlayerLifeBarContainer + +onready var _wave_cleared_label = $UI / WaveClearedLabel +onready var _life_label = $UI / HUD / LifeContainer / UILifeBar / MarginContainer / LifeLabel +onready var _level_label = $UI / HUD / LifeContainer / UIXPBar / MarginContainer / LevelLabel +onready var _hud = $UI / HUD +onready var _life_bar = $UI / HUD / LifeContainer / UILifeBar +onready var _xp_bar = $UI / HUD / LifeContainer / UIXPBar +onready var _ui_gold = $UI / HUD / LifeContainer / UIGold +onready var _ui_bonus_gold = $UI / HUD / LifeContainer / UIBonusGold +onready var _ui_bonus_gold_pos = $UI / HUD / LifeContainer / UIBonusGold / Position2D +onready var _current_wave_label = $UI / HUD / WaveContainer / CurrentWaveLabel +onready var _wave_timer_label = $UI / HUD / WaveContainer / WaveTimerLabel +onready var _ui_wave_container = $UI / HUD / WaveContainer +onready var _ui_upgrades_to_process = $UI / HUD / ThingsToProcessContainer / Upgrades +onready var _ui_consumables_to_process = $UI / HUD / ThingsToProcessContainer / Consumables +onready var _ui_dim_screen = $UI / DimScreen +onready var _tile_map = $TileMap +onready var _tile_map_limits = $TileMapLimits +onready var _background = $CanvasLayer / Background +onready var _harvesting_timer = $HarvestingTimer +onready var _challenge_completed_ui = $UI / ChallengeCompletedUI + +onready var _damage_vignette = $UI / DamageVignette +onready var _info_popup = $UI / InfoPopup + +onready var game_controller = $"/root/GameController" + +const refresh_time = 1.0 / 100.0 +var update_timer = refresh_time + +var health_tracker +const HealthTracker = preload("res://mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.tscn") + + +func _physics_process(delta:float)->void : + update_timer -= delta + if update_timer <= 0 and not _cleaning_up: + send_player_position() + update_timer = refresh_time + +func send_player_position(): + game_controller.send_client_position() + +func _ready()->void : + _convert_stats_half_wave_proced = false + + _background.texture.gradient.colors[1] = RunData.get_background_gradient_color() + _tile_map.tile_set.tile_set_texture(0, RunData.get_background().tiles_sprite) + _tile_map.outline.modulate = RunData.get_background().outline_color + + TempStats.reset() + + var current_zone = ZoneService.get_zone_data(RunData.current_zone).duplicate() + var current_wave_data = ZoneService.get_wave_data(RunData.current_zone, RunData.current_wave) + + var map_size_coef = (1 + (RunData.effects["map_size"] / 100.0)) + + current_zone.width = max(MIN_MAP_SIZE, (current_zone.width * map_size_coef)) as int + current_zone.height = max(MIN_MAP_SIZE, (current_zone.height * map_size_coef)) as int + + _tile_map.init(current_zone) + _tile_map_limits.init(current_zone) + + ZoneService.current_zone_max_position = Vector2(current_zone.width * Utils.TILE_SIZE, current_zone.height * Utils.TILE_SIZE) + + _current_wave_label.text = Text.text("WAVE", [str(RunData.current_wave)]).to_upper() + +# _wave_timer.wait_time = 15 + _wave_timer.wait_time = 1 if RunData.instant_waves else current_wave_data.wave_duration + _wave_timer.start() + _wave_timer_label.wave_timer = _wave_timer + var _error_wave_timer = _wave_timer.connect("tick_started", self, "on_tick_started") + + _wave_manager.init(_wave_timer, current_wave_data) + + on_bonus_gold_changed(RunData.bonus_gold) + + init_camera() + set_level_label() + _ui_dim_screen.color.a = 0 + + DebugService.log_run_info() + RunData.reset_weapons_dmg_dealt() + RunData.reset_cache() + + game_controller = $"/root/GameController" + + health_tracker = HealthTracker.instance() + health_tracker.set_name("HealthTracker") + $UI.add_child(health_tracker) + health_tracker.init(game_controller.tracked_players) + + var run_data = game_controller.tracked_players[game_controller.self_peer_id].run_data + var current_level = run_data.current_level + var current_xp = run_data.current_xp + var next_level_xp = RunData.get_xp_needed(current_level + 1) + _xp_bar.update_value(current_xp, next_level_xp) + + var _error_gold_ui = RunData.connect("gold_changed", _ui_gold, "on_gold_changed") + + var _error_upg_select = _upgrades_ui.connect("upgrade_selected", self, "on_upgrade_selected") + var _error_consumable_added = connect("consumable_to_process_added", _ui_consumables_to_process, "add_element") + + var _error_take_button = _item_box_ui.connect("item_take_button_pressed", self, "on_item_box_take_button_pressed") + var _error_discard_button = _item_box_ui.connect("item_discard_button_pressed", self, "on_item_box_discard_button_pressed") + + game_controller.send_client_started() + game_controller.send_client_started() + for player_id in game_controller.tracked_players: + game_controller.tracked_players[player_id].erase("player") + +func _input(event:InputEvent)->void : + var is_right_stick_motion = event is InputEventJoypadMotion and (event.axis == JOY_AXIS_2 or event.axis == JOY_AXIS_3) and abs(event.axis_value) > 0.5 + var is_mouse_press = event is InputEventMouseButton + + if (is_mouse_press or is_right_stick_motion) and not _cleaning_up: + if ProgressData.settings.manual_aim_on_mouse_press and not ProgressData.settings.manual_aim: + ProgressData.update_mouse_cursor() + + if is_mouse_press: + if event.pressed: + InputService.hide_mouse = false + else : + InputService.hide_mouse = true + else : + InputService.hide_mouse = false + + if InputService.using_gamepad and not InputService.hide_mouse and not ProgressData.is_manual_aim(): + InputService.hide_mouse = true + + +func add_timer(cooldown:int)->Timer: + var timer = Timer.new() + timer.wait_time = cooldown + add_child(timer) + timer.start() + return timer + +func on_stat_updated(_stat_name:String, _value:int, _db_mod:float = 0.0)->void : + reload_stats() + +func reload_stats()->void : + _player.update_player_stats() + + LinkedStats.reset() + + for weapon in _player.current_weapons: + if is_instance_valid(weapon): + weapon.init_stats(false) + + _proj_on_death_stat_cache.clear() + +func on_tick_started()->void : + _wave_timer_label.modulate = Color.red + + +func on_bonus_gold_changed(value:int)->void : + if value == 0: + _ui_bonus_gold.hide() + + +func init_camera()->void : + var max_pos = ZoneService.current_zone_max_position + var min_pos = ZoneService.current_zone_min_position + + var zone_w = max_pos.x - min_pos.x + var zone_h = max_pos.y - min_pos.y + + _camera.center_horizontal_pos = zone_w / 2 + _camera.center_vertical_pos = zone_h / 2 + + if zone_w + EDGE_SIZE * 2 <= Utils.project_width: + _camera.center_horizontal = true + else : + _camera.limit_right = max_pos.x + EDGE_SIZE + _camera.limit_left = min_pos.x - EDGE_SIZE + + if zone_h + EDGE_SIZE * 2 <= Utils.project_height: + _camera.center_vertical = true + else : + _camera.limit_bottom = max_pos.y + EDGE_SIZE + _camera.limit_top = min_pos.y - EDGE_SIZE * 2 + +func _on_player_died(_p_player:Player)->void : + _player_life_bar.hide() + + if RunData.current_wave <= RunData.nb_of_waves: + _is_run_lost = true + else : + _is_run_won = true + clean_up_room(false, _is_run_lost, _is_run_won) + _end_wave_timer.start() + ProgressData.reset_run_state() + ChallengeService.complete_challenge("chal_rookie") + +func on_levelled_up()->void : + SoundManager.play(level_up_sound, 0, 0, true) + var level = RunData.current_level + emit_signal("upgrade_to_process_added", ItemService.upgrade_to_process_icon, level) + _upgrades_to_process.push_back(level) + set_level_label() + RunData.add_stat("stat_max_hp", 1) + RunData.emit_signal("healing_effect", 1) + +func set_level_label()->void : + _level_label.text = "LV." + str(RunData.current_level) + +func set_life_label(current_health:int, max_health:int)->void : + _life_label.text = str(max(current_health, 0.0)) + " / " + str(max_health) + +func connect_visual_effects(unit:Unit)->void : + var _error_effects = unit.connect("took_damage", _effects_manager, "_on_unit_took_damage") + var _error_floating_text = unit.connect("took_damage", _floating_text_manager, "_on_unit_took_damage") + +func clean_up_room(is_last_wave:bool = false, is_run_lost:bool = false, is_run_won:bool = false)->void : + + if is_run_lost: + DebugService.log_data("is_run_lost") + _wave_timer_label.is_run_lost = true + _wave_timer.stop() + _wave_timer.tick_timer.stop() + _end_wave_timer.wait_time = 2.5 + SoundManager.play(Utils.get_rand_element(run_lost_sounds), - 5, 0, true) + _ui_dim_screen.dim() + MusicManager.tween( - 20) + elif is_run_won: + DebugService.log_data("is_run_won") + apply_run_won() + + if RunData.is_endless_run: + + if RunData.current_wave % 10 == 0 and RunData.current_wave >= 20: + RunData.init_elites_spawn(RunData.current_wave, 0.0) + + DebugService.log_data("is_endless_run") + + if RunData.current_wave >= 20: + var character_difficulty = ProgressData.get_character_difficulty_info(RunData.current_character.my_id, RunData.current_zone) + + character_difficulty.max_endless_wave_beaten.set_info( + RunData.get_current_difficulty(), + RunData.current_wave, + RunData.current_run_accessibility_settings.health, + RunData.current_run_accessibility_settings.damage, + RunData.current_run_accessibility_settings.speed, + true + ) + + # TODO maybe replace +# SoundManager.play(Utils.get_rand_element(end_wave_sounds)) + _cleaning_up = true + _effects_manager.clean_up_room() + _floating_text_manager.clean_up_room() + + for entity in _entities_container.get_children(): + if not is_instance_valid(entity): + continue + if entity.get("_boost_zone") and is_instance_valid(entity._boost_zone): + continue + if not entity is EntityBirth and not entity is Player: + entity.die() + + if entity.has_node("MovementBehavior"): + var movemenent_node = entity.get_node("MovementBehavior") + entity.remove_child(movemenent_node) + + movemenent_node = ClientMovementBehavior.new() + movemenent_node.set_name("MovementBehavior") + entity.add_child(movemenent_node) + + _entities_container.remove_child(entity) + + health_tracker.hide() + _wave_cleared_label.hide() + _wave_timer_label.hide() + + _wave_manager.clean_up_room() + + if is_instance_valid(_player): + var movemenent_node = _player.get_node("MovementBehavior") + _player.remove_child(movemenent_node) + + _player.clean_up() + + DebugService.log_data("start wave_cleared_label...") + _wave_cleared_label.start(is_last_wave, is_run_lost, is_run_won) + DebugService.log_data("wave_cleared_label started...") + +func get_gold_bag_pos()->Vector2: + return get_viewport().get_canvas_transform().affine_inverse().xform(_ui_bonus_gold_pos.global_position) + +func on_focus_lost()->void : + if _end_wave_timer_timedout: + if _item_box_ui.visible: + _item_box_ui.focus() + elif _upgrades_ui.visible: + _upgrades_ui.focus() + +func on_upgrade_selected(upgrade_data:UpgradeData)->void : + game_controller.on_upgrade_selected(upgrade_data) + +func on_item_box_take_button_pressed(item_data:ItemParentData)->void : + game_controller.on_item_box_take_button_pressed(item_data) + +func on_item_box_discard_button_pressed(item_data:ItemParentData) -> void: + game_controller.discard_item_box(item_data) + +func _on_PauseMenu_paused()->void : + reload_stats() + +func _on_PauseMenu_unpaused()->void : + on_focus_lost() + +func apply_run_won()->void : + _wave_timer.stop() + _wave_timer.tick_timer.stop() + _end_wave_timer.wait_time = 3 + _ui_dim_screen.dim() + SoundManager.play(Utils.get_rand_element(run_won_sounds), - 5, 0, true) + + var character_chal_name = "chal_" + RunData.current_character.name.to_lower().replace("character_", "") + ChallengeService.complete_challenge(character_chal_name) + + var character_difficulty = ProgressData.get_character_difficulty_info(RunData.current_character.my_id, RunData.current_zone) + + if character_difficulty.max_selectable_difficulty < RunData.get_current_difficulty() + 1 and RunData.get_current_difficulty() + 1 <= ProgressData.MAX_DIFFICULTY: + RunData.difficulty_unlocked = RunData.get_current_difficulty() + 1 + + ChallengeService.complete_challenge("chal_difficulty_" + str(RunData.get_current_difficulty())) + + for char_diff in ProgressData.difficulties_unlocked: + for zone_difficulty_info in char_diff.zones_difficulty_info: + zone_difficulty_info.max_selectable_difficulty = clamp(RunData.get_current_difficulty() + 1, zone_difficulty_info.max_selectable_difficulty, ProgressData.MAX_DIFFICULTY) + + character_difficulty.max_difficulty_beaten.set_info( + RunData.get_current_difficulty(), + RunData.current_wave, + RunData.current_run_accessibility_settings.health, + RunData.current_run_accessibility_settings.damage, + RunData.current_run_accessibility_settings.speed, + false + ) + + ProgressData.reset_run_state() + +func is_last_wave()->bool: + var is_last_wave = RunData.current_wave == ZoneService.get_zone_data(RunData.current_zone).waves_data.size() + if RunData.is_endless_run:is_last_wave = false + return is_last_wave + +func manage_harvesting()->void : + pass + +func _on_UIBonusGold_mouse_entered()->void : + if _cleaning_up: + _info_popup.display(_ui_bonus_gold, Text.text("INFO_BONUS_GOLD", [str(RunData.bonus_gold)])) + + +func _on_UIBonusGold_mouse_exited()->void : + _info_popup.hide() + +func _on_HarvestingTimer_timeout()->void : + if RunData.current_wave > RunData.nb_of_waves: + var val = ceil(Utils.get_stat("stat_harvesting") * (RunData.ENDLESS_HARVESTING_DECREASE / 100.0)) + RunData.remove_stat("stat_harvesting", val) + else : + var val = ceil(Utils.get_stat("stat_harvesting") * (RunData.effects["harvesting_growth"] / 100.0)) + + var has_crown = false + var crown_value = 0 + + for item in RunData.items: + if item.my_id == "item_crown": + has_crown = true + crown_value = item.effects[0].value + break + + if has_crown: + RunData.tracked_item_effects["item_crown"] += (Utils.get_stat("stat_harvesting") * (crown_value / 100.0)) as int + + if val > 0: + RunData.add_stat("stat_harvesting", val) + +func on_gold_changed(gold:int)->void : + if _update_stats_on_gold_changed: + if (gold / 30.0) as int != _last_gold_amount_used_to_reload_stats: + reload_stats() + _last_gold_amount_used_to_reload_stats = (gold / 30.0) as int + + +func _on_WaveTimer_timeout() -> void: + clean_up_room(false, false, false) + +func _on_EndWaveTimer_timeout() -> void: + game_controller.send_complete_player_request() + yield(game_controller, "complete_player_update") + + if _is_run_won: + apply_run_won() + RunData.run_won = true + var _error = get_tree().change_scene("res://ui/menus/run/end_run.tscn") + return + + InputService.hide_mouse = true + + var consumables = game_controller.tracked_players[game_controller.self_peer_id].consumables_to_process + if consumables.size() > 0: + for consumable in consumables: + var fixed_tier = - 1 + + if consumable.my_id == "consumable_legendary_item_box": + fixed_tier = Tier.LEGENDARY + + var item_data = ItemService.process_item_box(RunData.current_wave, consumable, fixed_tier) + _item_box_ui.set_item_data(item_data) + yield (_item_box_ui, "item_box_processed") + + game_controller.send_complete_player_request() + yield(game_controller, "complete_player_update") + + _ui_consumables_to_process.remove_element(consumable) + game_controller.tracked_players[game_controller.self_peer_id].consumables_to_process = [] + + if _upgrades_to_process.size() > 0: + for upgrade_to_process in _upgrades_to_process: + _upgrades_ui.show_upgrade_options(upgrade_to_process) + yield (_upgrades_ui, "upgrade_selected") + + game_controller.send_complete_player_request() + yield(game_controller, "complete_player_update") + + _ui_upgrades_to_process.remove_element(upgrade_to_process) + + var _error = get_tree().change_scene("res://mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_shop.tscn") diff --git a/src/mods-unpacked/Brogether-Brogether/client/client_main.tscn b/src/mods-unpacked/Brogether-Brogether/client/client_main.tscn new file mode 100644 index 0000000..4e8b7f7 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/client/client_main.tscn @@ -0,0 +1,460 @@ +[gd_scene load_steps=81 format=2] + +[ext_resource path="res://particles/heal_particles.tscn" type="PackedScene" id=1] +[ext_resource path="res://particles/directional_hit_particles.tscn" type="PackedScene" id=2] +[ext_resource path="res://visual_effects/floating_text/floating_text.tscn" type="PackedScene" id=3] +[ext_resource path="res://visual_effects/hit_effect/hit_effect.tscn" type="PackedScene" id=4] +[ext_resource path="res://particles/pickup_gold_particles.tscn" type="PackedScene" id=5] +[ext_resource path="res://particles/stats_boost_particles.tscn" type="PackedScene" id=6] +[ext_resource path="res://ui/hud/ui_upgrade_to_process_element.tscn" type="PackedScene" id=7] +[ext_resource path="res://particles/speed_removed_particles.tscn" type="PackedScene" id=8] +[ext_resource path="res://particles/hit_particles.tscn" type="PackedScene" id=9] +[ext_resource path="res://resources/themes/base_theme.tres" type="Theme" id=11] +[ext_resource path="res://weapons/melee/dagger/dagger_icon.png" type="Texture" id=12] +[ext_resource path="res://ui/hud/ui_progress_bar.tscn" type="PackedScene" id=13] +[ext_resource path="res://items/materials/harvesting_icon.png" type="Texture" id=14] +[ext_resource path="res://ui/hud/ui_wave_timer.gd" type="Script" id=15] +[ext_resource path="res://zones/zone_1/000_all/group_2.tres" type="Resource" id=16] +[ext_resource path="res://zones/zone_1/000_elites/group_1.tres" type="Resource" id=17] +[ext_resource path="res://zones/zone_1/000_hordes/group_1.tres" type="Resource" id=18] +[ext_resource path="res://zones/zone_1/000_hordes/group_3.tres" type="Resource" id=19] +[ext_resource path="res://zones/zone_1/000_all/group_1.tres" type="Resource" id=20] +[ext_resource path="res://zones/zone_1/000_hordes/group_2.tres" type="Resource" id=21] +[ext_resource path="res://zones/zone_1/000_extra/group_1.tres" type="Resource" id=22] +[ext_resource path="res://zones/zone_1/000_hordes/group_4.tres" type="Resource" id=23] +[ext_resource path="res://zones/zone_1/000_hordes/group_5.tres" type="Resource" id=24] +[ext_resource path="res://items/materials/alt_sounds/coin_bag_ring_gemstone_item_03.wav" type="AudioStream" id=28] +[ext_resource path="res://resources/sounds/harvest/cartoon_boing_jump_01.wav" type="AudioStream" id=29] +[ext_resource path="res://resources/sounds/Madness 10.mp3" type="AudioStream" id=30] +[ext_resource path="res://resources/sounds/Madness 9.mp3" type="AudioStream" id=31] +[ext_resource path="res://resources/sounds/Madness 7.mp3" type="AudioStream" id=32] +[ext_resource path="res://resources/sounds/Madness 3.mp3" type="AudioStream" id=33] +[ext_resource path="res://resources/sounds/harvest/cartoon_boing_jump_07.wav" type="AudioStream" id=34] +[ext_resource path="res://resources/sounds/harvest/cartoon_boing_jump_09.wav" type="AudioStream" id=35] +[ext_resource path="res://resources/sounds/harvest/cartoon_boing_jump_06.wav" type="AudioStream" id=36] +[ext_resource path="res://ui/hud/ui_consumable_to_process_list.gd" type="Script" id=37] +[ext_resource path="res://global/my_tile_map_limits.gd" type="Script" id=38] +[ext_resource path="res://zones/backgrounds/background_gradient.tres" type="Gradient" id=39] +[ext_resource path="res://resources/tiles/ground_tiles.tres" type="TileSet" id=40] +[ext_resource path="res://global/my_camera.gd" type="Script" id=41] +[ext_resource path="res://resources/sounds/Madness 1.mp3" type="AudioStream" id=42] +[ext_resource path="res://items/materials/alt_sounds/coin_bag_ring_gemstone_item_01.wav" type="AudioStream" id=43] +[ext_resource path="res://resources/sounds/level_up.wav" type="AudioStream" id=44] +[ext_resource path="res://resources/sounds/Madness 8.mp3" type="AudioStream" id=45] +[ext_resource path="res://items/materials/water_drop_drip_single_02.wav" type="AudioStream" id=46] +[ext_resource path="res://items/materials/alt_sounds/coin_bag_ring_gemstone_item_05.wav" type="AudioStream" id=47] +[ext_resource path="res://resources/sounds/Madness 6.mp3" type="AudioStream" id=48] +[ext_resource path="res://items/materials/alt_sounds/coin_bag_ring_gemstone_item_02.wav" type="AudioStream" id=49] +[ext_resource path="res://items/materials/water_drop_drip_single_04.wav" type="AudioStream" id=50] +[ext_resource path="res://ui/sounds/end_wave.wav" type="AudioStream" id=51] +[ext_resource path="res://items/materials/water_drop_drip_single_05.wav" type="AudioStream" id=52] +[ext_resource path="res://resources/sounds/Madness 4.mp3" type="AudioStream" id=53] +[ext_resource path="res://items/materials/water_drop_drip_single_01.wav" type="AudioStream" id=54] +[ext_resource path="res://ui/sounds/CGM3_Bubble_Button_01_4.wav" type="AudioStream" id=55] +[ext_resource path="res://resources/sounds/Fear 10.mp3" type="AudioStream" id=56] +[ext_resource path="res://resources/tiles/tiles_outline.png" type="Texture" id=57] +[ext_resource path="res://items/materials/alt_sounds/coin_bag_ring_gemstone_item_04.wav" type="AudioStream" id=58] +[ext_resource path="res://items/materials/water_drop_drip_single_03.wav" type="AudioStream" id=59] +[ext_resource path="res://ui/sounds/clock_tick_01.wav" type="AudioStream" id=60] +[ext_resource path="res://resources/sounds/Madness 2.mp3" type="AudioStream" id=61] +[ext_resource path="res://ui/sounds/CGM3_Bubble_Button_01_3.wav" type="AudioStream" id=62] +[ext_resource path="res://global/screenshaker.gd" type="Script" id=63] +[ext_resource path="res://mods-unpacked/Brogether-Brogether/client/client_main.gd" type="Script" id=64] +[ext_resource path="res://visual_effects/damage_vignette.gd" type="Script" id=65] +[ext_resource path="res://ui/hud/ui_dim_screen.gd" type="Script" id=67] +[ext_resource path="res://ui/hud/ui_upgrade_to_process_list.gd" type="Script" id=68] +[ext_resource path="res://global/effects_manager.gd" type="Script" id=69] +[ext_resource path="res://global/my_tile_map.gd" type="Script" id=70] +[ext_resource path="res://ui/hud/wave_timer.gd" type="Script" id=71] +[ext_resource path="res://zones/wave_manager.gd" type="Script" id=72] +[ext_resource path="res://visual_effects/floating_text/floating_text_manager.gd" type="Script" id=73] +[ext_resource path="res://ui/menus/ingame/wave_cleared_label.gd" type="Script" id=74] +[ext_resource path="res://ui/hud/ui_bonus_gold.tscn" type="PackedScene" id=75] +[ext_resource path="res://ui/hud/ui_item_list.tscn" type="PackedScene" id=76] +[ext_resource path="res://ui/hud/ui_gold.tscn" type="PackedScene" id=77] +[ext_resource path="res://ui/menus/ingame/pause_menu.tscn" type="PackedScene" id=78] +[ext_resource path="res://ui/menus/ingame/upgrades_ui.tscn" type="PackedScene" id=79] +[ext_resource path="res://ui/menus/ingame/challenge_completed_ui.tscn" type="PackedScene" id=80] +[ext_resource path="res://ui/menus/ingame/info_popup.tscn" type="PackedScene" id=81] +[ext_resource path="res://ui/menus/upgrades/item_box_ui.tscn" type="PackedScene" id=82] + +[sub_resource type="GradientTexture" id=3] +gradient = ExtResource( 39 ) +width = 1920 + +[sub_resource type="Shader" id=10] +code = "shader_type canvas_item; + +uniform vec4 color: hint_color; +uniform float multiplier = 0.2; +uniform float softness = 0.2; + +void fragment() { + float value = distance(UV, vec2(0.5)); + COLOR = vec4(color.rgb, smoothstep(multiplier, softness, value)); +}" + +[sub_resource type="ShaderMaterial" id=11] +shader = SubResource( 10 ) +shader_param/color = Color( 0, 0, 0, 1 ) +shader_param/multiplier = 0.8 +shader_param/softness = 0.8 + +[node name="ClientMain" type="Node"] +script = ExtResource( 64 ) +gold_pickup_sounds = [ ExtResource( 54 ), ExtResource( 46 ), ExtResource( 59 ), ExtResource( 50 ), ExtResource( 52 ) ] +gold_alt_pickup_sounds = [ ExtResource( 43 ), ExtResource( 49 ), ExtResource( 28 ), ExtResource( 58 ), ExtResource( 47 ) ] +level_up_sound = ExtResource( 44 ) +run_won_sounds = [ ExtResource( 56 ) ] +run_lost_sounds = [ ExtResource( 42 ), ExtResource( 61 ), ExtResource( 33 ), ExtResource( 53 ), ExtResource( 48 ), ExtResource( 32 ), ExtResource( 45 ), ExtResource( 31 ), ExtResource( 30 ) ] +end_wave_sounds = [ ExtResource( 51 ) ] + +[node name="CanvasLayer" type="CanvasLayer" parent="."] +layer = -10 + +[node name="Background" type="TextureRect" parent="CanvasLayer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 1920.0 +margin_right = 1080.0 +margin_bottom = 840.0 +rect_rotation = 89.9906 +texture = SubResource( 3 ) +expand = true + +[node name="WaveManager" type="Node2D" parent="."] +script = ExtResource( 72 ) +extra_groups = [ ExtResource( 22 ) ] +groups_data_in_all_waves = [ ExtResource( 20 ), ExtResource( 16 ) ] +horde_groups = [ ExtResource( 18 ), ExtResource( 21 ), ExtResource( 19 ), ExtResource( 23 ), ExtResource( 24 ) ] +elite_group = ExtResource( 17 ) + +[node name="EffectsManager" type="Node2D" parent="."] +script = ExtResource( 69 ) +heal_particles = ExtResource( 1 ) +stats_boost_particles = ExtResource( 6 ) +speed_removed_particles = ExtResource( 8 ) +hit_particles = ExtResource( 9 ) +directional_hit_particles = ExtResource( 2 ) +hit_effect = ExtResource( 4 ) +gold_pickup_particles = ExtResource( 5 ) + +[node name="FloatingTextManager" type="Node2D" parent="."] +script = ExtResource( 73 ) +_floating_text = ExtResource( 3 ) +gold_on_crit_kill_icon = ExtResource( 12 ) +harvesting_icon = ExtResource( 14 ) +stat_pos_sounds = [ ExtResource( 35 ) ] +stat_neg_sounds = [ ExtResource( 34 ) ] +harvest_pos_sounds = [ ExtResource( 29 ) ] +harvest_neg_sounds = [ ExtResource( 36 ) ] + +[node name="Camera" type="Camera2D" parent="."] +current = true +script = ExtResource( 41 ) +__meta__ = { +"_edit_lock_": true +} + +[node name="Screenshaker" type="Node" parent="Camera"] +script = ExtResource( 63 ) + +[node name="TileMap" type="TileMap" parent="."] +tile_set = ExtResource( 40 ) +cell_quadrant_size = 64 +collision_layer = 128 +collision_mask = 0 +format = 1 +tile_data = PoolIntArray( 7, 0, 65538, 65536, 0, 65538, 65537, 0, 65538 ) +script = ExtResource( 70 ) + +[node name="Outline" type="NinePatchRect" parent="TileMap"] +modulate = Color( 0.470588, 0.403922, 0.345098, 1 ) +show_behind_parent = true +margin_left = -64.0 +margin_top = -64.0 +margin_right = 640.0 +margin_bottom = 392.0 +texture = ExtResource( 57 ) +patch_margin_left = 64 +patch_margin_top = 64 +patch_margin_right = 64 +patch_margin_bottom = 64 +axis_stretch_horizontal = 1 +axis_stretch_vertical = 1 +__meta__ = { +"_edit_lock_": true, +"_edit_use_anchors_": false +} + +[node name="TileMapLimits" type="StaticBody2D" parent="."] +position = Vector2( -64, -64 ) +collision_layer = 128 +collision_mask = 0 +script = ExtResource( 38 ) + +[node name="Items" type="Node2D" parent="."] + +[node name="Consumables" type="Node2D" parent="."] + +[node name="Births" type="Node2D" parent="."] + +[node name="Entities" type="YSort" parent="."] + +[node name="Projectiles" type="Node2D" parent="."] + +[node name="PlayerLifeBarContainer" type="Node2D" parent="."] + +[node name="PlayerLifeBar" parent="PlayerLifeBarContainer" instance=ExtResource( 13 )] +unique_name_in_owner = true +visible = false +margin_left = -32.0 +margin_top = -72.0 +margin_right = 32.0 +margin_bottom = -56.0 +nine_patch_stretch = true +progress_color = Color( 0.721569, 0, 0, 1 ) + +[node name="UI" type="CanvasLayer" parent="."] + +[node name="DamageVignette" type="ColorRect" parent="UI"] +material = SubResource( 11 ) +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 65 ) + +[node name="DimScreen" type="ColorRect" parent="UI"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 40.0 +margin_right = 40.0 +color = Color( 0, 0, 0, 0 ) +script = ExtResource( 67 ) + +[node name="Tween" type="Tween" parent="UI/DimScreen"] + +[node name="WaveClearedLabel" type="Label" parent="UI"] +anchor_left = 0.5 +anchor_right = 0.5 +margin_left = -958.0 +margin_right = 960.0 +margin_bottom = 400.0 +theme = ExtResource( 11 ) +text = "WAVE_COMPLETED" +align = 1 +valign = 1 +uppercase = true +script = ExtResource( 74 ) +font_end_run = ExtResource( 26 ) +character_appearing_sounds = [ ExtResource( 62 ), ExtResource( 55 ) ] + +[node name="Timer" type="Timer" parent="UI/WaveClearedLabel"] +wait_time = 0.1 +one_shot = true + +[node name="ItemBoxUI" parent="UI" instance=ExtResource( 82 )] +visible = false +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 0.0 +margin_top = 0.0 +margin_right = 0.0 +margin_bottom = 0.0 + +[node name="UpgradesUI" parent="UI" instance=ExtResource( 79 )] +visible = false + +[node name="HUD" type="MarginContainer" parent="UI"] +anchor_right = 1.0 +margin_bottom = 284.0 +size_flags_horizontal = 0 +size_flags_vertical = 0 +custom_constants/margin_right = 24 +custom_constants/margin_top = 24 +custom_constants/margin_left = 24 +custom_constants/margin_bottom = 24 + +[node name="LifeContainer" type="VBoxContainer" parent="UI/HUD"] +margin_left = 24.0 +margin_top = 24.0 +margin_right = 344.0 +margin_bottom = 260.0 +size_flags_horizontal = 0 +size_flags_vertical = 0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="UILifeBar" parent="UI/HUD/LifeContainer" instance=ExtResource( 13 )] +margin_bottom = 48.0 +size_flags_horizontal = 0 +tint_progress = Color( 0.721569, 0, 0, 1 ) +progress_color = Color( 0.721569, 0, 0, 1 ) + +[node name="MarginContainer" type="MarginContainer" parent="UI/HUD/LifeContainer/UILifeBar"] +anchor_right = 1.0 +anchor_bottom = 1.0 +custom_constants/margin_right = 12 +custom_constants/margin_top = 7 +custom_constants/margin_left = 12 +custom_constants/margin_bottom = 7 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="LifeLabel" type="Label" parent="UI/HUD/LifeContainer/UILifeBar/MarginContainer"] +margin_left = 12.0 +margin_top = 8.0 +margin_right = 308.0 +margin_bottom = 39.0 +grow_horizontal = 0 +text = "8 / 8" +align = 1 +valign = 1 +uppercase = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="UIXPBar" parent="UI/HUD/LifeContainer" instance=ExtResource( 13 )] +margin_top = 52.0 +margin_bottom = 100.0 +size_flags_horizontal = 0 +value = 50.0 + +[node name="MarginContainer" type="MarginContainer" parent="UI/HUD/LifeContainer/UIXPBar"] +anchor_right = 1.0 +anchor_bottom = 1.0 +custom_constants/margin_right = 12 +custom_constants/margin_top = 7 +custom_constants/margin_left = 12 +custom_constants/margin_bottom = 7 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="LevelLabel" type="Label" parent="UI/HUD/LifeContainer/UIXPBar/MarginContainer"] +margin_left = 12.0 +margin_top = 8.0 +margin_right = 308.0 +margin_bottom = 39.0 +grow_horizontal = 0 +text = "LV. 1" +align = 2 +valign = 1 +uppercase = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="UIGold" parent="UI/HUD/LifeContainer" instance=ExtResource( 77 )] +anchor_left = 0.0 +anchor_right = 0.0 +margin_left = 0.0 +margin_top = 104.0 +margin_right = 320.0 +margin_bottom = 168.0 +size_flags_horizontal = 3 +alignment = 0 + +[node name="UIBonusGold" parent="UI/HUD/LifeContainer" instance=ExtResource( 75 )] +margin_top = 172.0 +margin_bottom = 236.0 +size_flags_horizontal = 0 + +[node name="WaveContainer" type="VBoxContainer" parent="UI/HUD"] +margin_left = 873.0 +margin_top = 24.0 +margin_right = 1046.0 +margin_bottom = 146.0 +size_flags_horizontal = 4 +size_flags_vertical = 0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="CurrentWaveLabel" type="Label" parent="UI/HUD/WaveContainer"] +margin_right = 173.0 +margin_bottom = 45.0 +theme = ExtResource( 11 ) +text = "WAVE {0}" +align = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="WaveTimerLabel" type="Label" parent="UI/HUD/WaveContainer"] +margin_top = 49.0 +margin_right = 173.0 +margin_bottom = 122.0 +theme = ExtResource( 11 ) +text = "60" +align = 1 +script = ExtResource( 15 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ThingsToProcessContainer" type="VBoxContainer" parent="UI/HUD"] +margin_left = 1896.0 +margin_top = 24.0 +margin_right = 1896.0 +margin_bottom = 36.0 +size_flags_horizontal = 8 +size_flags_vertical = 0 +custom_constants/separation = 12 + +[node name="Upgrades" parent="UI/HUD/ThingsToProcessContainer" instance=ExtResource( 76 )] +modulate = Color( 1, 1, 1, 0.501961 ) +script = ExtResource( 68 ) +element_scene = ExtResource( 7 ) + +[node name="Consumables" parent="UI/HUD/ThingsToProcessContainer" instance=ExtResource( 76 )] +modulate = Color( 1, 1, 1, 0.501961 ) +margin_top = 12.0 +margin_bottom = 12.0 +script = ExtResource( 37 ) + +[node name="PauseMenu" parent="UI" instance=ExtResource( 78 )] +pause_mode = 2 +visible = false + +[node name="ChallengeCompletedUI" parent="UI" instance=ExtResource( 80 )] +visible = false + +[node name="InfoPopup" parent="UI" instance=ExtResource( 81 )] +visible = false + +[node name="EndWaveTimer" type="Timer" parent="."] +wait_time = 0.75 +one_shot = true + +[node name="HarvestingTimer" type="Timer" parent="."] +wait_time = 0.66 +one_shot = true + +[node name="WaveTimer" type="Timer" parent="."] +one_shot = true +script = ExtResource( 71 ) +tick_sounds = [ ExtResource( 60 ) ] + +[node name="TickTimer" type="Timer" parent="WaveTimer"] + +[node name="CanvasLayer2" type="CanvasLayer" parent="."] + +[connection signal="timeout" from="UI/WaveClearedLabel/Timer" to="UI/WaveClearedLabel" method="_on_Timer_timeout"] +[connection signal="mouse_entered" from="UI/HUD/LifeContainer/UIBonusGold" to="." method="_on_UIBonusGold_mouse_entered"] +[connection signal="mouse_exited" from="UI/HUD/LifeContainer/UIBonusGold" to="." method="_on_UIBonusGold_mouse_exited"] +[connection signal="paused" from="UI/PauseMenu" to="." method="_on_PauseMenu_paused"] +[connection signal="unpaused" from="UI/PauseMenu" to="." method="_on_PauseMenu_unpaused"] +[connection signal="timeout" from="EndWaveTimer" to="." method="_on_EndWaveTimer_timeout"] +[connection signal="timeout" from="HarvestingTimer" to="." method="_on_HarvestingTimer_timeout"] +[connection signal="timeout" from="WaveTimer" to="." method="_on_WaveTimer_timeout"] +[connection signal="timeout" from="WaveTimer" to="WaveTimer" method="_on_WaveTimer_timeout"] +[connection signal="timeout" from="WaveTimer/TickTimer" to="WaveTimer" method="_on_TickTimer_timeout"] + +[editable path="UI/HUD/LifeContainer/UILifeBar"] +[editable path="UI/HUD/LifeContainer/UIXPBar"] +[editable path="UI/ChallengeCompletedUI"] diff --git a/src/mods-unpacked/Brogether-Brogether/client/client_movement_behavior.gd b/src/mods-unpacked/Brogether-Brogether/client/client_movement_behavior.gd new file mode 100644 index 0000000..b77af44 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/client/client_movement_behavior.gd @@ -0,0 +1,11 @@ +extends MovementBehavior + +signal detected_player + +func init(parent:Node)->Node: + var _init = .init(parent) + return self + + +func get_movement()->Vector2: + return Vector2.ZERO diff --git a/src/mods-unpacked/Brogether-Brogether/client/client_target_behavior.gd b/src/mods-unpacked/Brogether-Brogether/client/client_target_behavior.gd new file mode 100644 index 0000000..5c1efff --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/client/client_target_behavior.gd @@ -0,0 +1,4 @@ +extends TargetBehavior + +func update_target(): + pass diff --git a/src/mods-unpacked/Brogether-Brogether/client/waiting.gd b/src/mods-unpacked/Brogether-Brogether/client/waiting.gd new file mode 100644 index 0000000..d5723d0 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/client/waiting.gd @@ -0,0 +1,21 @@ +extends Node2D + +onready var message_label = $Message + +# Called when the node enters the scene tree for the first time. +func _ready(): + var messages = [] + + messages.push_back("THESE PRETZELS ARE MAKING ME THIRSTY") + messages.push_back("support the dev -> get a better screen here") + messages.push_back("SERENITY NOW") + messages.push_back("(1/3) We must go forward not backward") + messages.push_back("(2/3) Upward not forward") + messages.push_back("(3/3) And always twirling, twirling, twirling towards freedom") + messages.push_back("So you're telling me there's a chance") + messages.push_back("Great Success!") + messages.push_back("Bueller ... Bueller ... Bueller") + messages.push_back("The dude abides") + + + message_label.text = Utils.get_rand_element(messages) diff --git a/src/mods-unpacked/Brogether-Brogether/client/waiting.tscn b/src/mods-unpacked/Brogether-Brogether/client/waiting.tscn new file mode 100644 index 0000000..885d330 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/client/waiting.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://resources/themes/inventory_button_theme.tres" type="Theme" id=1] +[ext_resource path="res://mods-unpacked/Brogether-Brogether/client/waiting.gd" type="Script" id=2] + +[node name="Node2D" type="Node2D"] +script = ExtResource( 2 ) + +[node name="Label" type="Label" parent="."] +margin_left = 2.0 +margin_top = 6.0 +margin_right = 1918.0 +margin_bottom = 192.0 +size_flags_horizontal = 3 +theme = ExtResource( 1 ) +text = "Please wait for the host to shop and resume the game" + +[node name="Message" type="Label" parent="."] +margin_left = -1.0 +margin_top = 285.0 +margin_right = 1915.0 +margin_bottom = 471.0 +size_flags_horizontal = 3 +theme = ExtResource( 1 ) +text = "These Pretzels are making me Thirsty!" diff --git a/src/mods-unpacked/Brogether-Brogether/data_node.gd b/src/mods-unpacked/Brogether-Brogether/data_node.gd new file mode 100644 index 0000000..47cb2a4 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/data_node.gd @@ -0,0 +1,3 @@ +extends Node + +var data = {} diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/entities/birth/entity_birth.gd b/src/mods-unpacked/Brogether-Brogether/extensions/entities/birth/entity_birth.gd new file mode 100644 index 0000000..ae35a10 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/entities/birth/entity_birth.gd @@ -0,0 +1,17 @@ +extends "res://entities/birth/entity_birth.gd" + +var steam_connection +var brotatogether_options +var in_multiplayer_game = false +var network_id = 0 +var is_host = false + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + if in_multiplayer_game: + is_host = steam_connection.is_host() + network_id = brotatogether_options.current_network_id + brotatogether_options.current_network_id = brotatogether_options.current_network_id + 1 diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/entities/entity.gd b/src/mods-unpacked/Brogether-Brogether/extensions/entities/entity.gd new file mode 100644 index 0000000..88337f1 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/entities/entity.gd @@ -0,0 +1,18 @@ +extends "res://entities/entity.gd" + +var steam_connection +var brotatogether_options +var in_multiplayer_game = false +var network_id = 0 +var is_host = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + if in_multiplayer_game: + is_host = steam_connection.is_host() + network_id = brotatogether_options.current_network_id + brotatogether_options.current_network_id = brotatogether_options.current_network_id + 1 diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/entities/structures/turret/turret.gd b/src/mods-unpacked/Brogether-Brogether/extensions/entities/structures/turret/turret.gd new file mode 100644 index 0000000..58c5a58 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/entities/structures/turret/turret.gd @@ -0,0 +1,12 @@ +extends "res://entities/structures/turret/turret.gd" + + +func _on_AnimationPlayer_animation_finished(anim_name: String) -> void : + if self.in_multiplayer_game and not self.is_host: + return + ._on_AnimationPlayer_animation_finished(anim_name) + +func shoot() -> void : + if self.in_multiplayer_game and not self.is_host: + return + .shoot() diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/entities/units/movement_behaviors/player_movement_behavior.gd b/src/mods-unpacked/Brogether-Brogether/extensions/entities/units/movement_behaviors/player_movement_behavior.gd new file mode 100644 index 0000000..ae28aa0 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/entities/units/movement_behaviors/player_movement_behavior.gd @@ -0,0 +1,8 @@ +extends "res://entities/units/movement_behaviors/player_movement_behavior.gd" + + +func get_movement()->Vector2: + if device >= 50: + return get_parent()._current_movement + + return .get_movement() diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/entities/units/unit/unit.gd b/src/mods-unpacked/Brogether-Brogether/extensions/entities/units/unit/unit.gd new file mode 100644 index 0000000..dde87c9 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/entities/units/unit/unit.gd @@ -0,0 +1,123 @@ +extends "res://entities/units/unit/unit.gd" + +# THIS IS CURRENTLY A DUPE, hopefully that won't be a problem +enum EntityState { + ENTITY_STATE_NETWORK_ID, + + ENTITY_STATE_X_POS, + ENTITY_STATE_Y_POS, + ENTITY_STATE_X_MOVE, + ENTITY_STATE_Y_MOVE, + ENTITY_STATE_CURRENT_HP, + ENTITY_STATE_MAX_HP, + ENTITY_STATE_SPRITE_SCALE, + + # Player-specific entity state. + ENTITY_STATE_PLAYER_GOLD, + ENTITY_STATE_PLAYER_CURRENT_XP, + ENTITY_STATE_PLAYER_NEXT_LEVEL_XP, + ENTITY_STATE_PLAYER_NUM_UPGRADES, + ENTITY_STATE_PLAYER_WEAPONS, + ENTITY_STATE_PLAYER_LEVEL, +} + +enum WeaponState { + WEAPON_STATE_X_POS, + WEAPON_STATE_Y_POS, + WEAPON_STATE_ROTATION, + WEAPON_STATE_SPRITE_ROTATION, + WEAPON_STATE_IS_SHOOTING, +} + + +func take_damage(value: int, args: TakeDamageArgs) -> Array: + if self.in_multiplayer_game and not self.is_host: + return [] + return .take_damage(value, args) + + +func _on_Hurtbox_area_entered(hitbox: Area2D) -> void : + if self.in_multiplayer_game and not self.is_host: + return + ._on_Hurtbox_area_entered(hitbox) + + +func flash()->void : + if self.in_multiplayer_game and self.is_host: + self.brotatogether_options.batched_unit_flashes[self.network_id] = true + .flash() + + +func die(args: = Entity.DieArgs.new())->void : + if self.in_multiplayer_game and self.is_host: + self.brotatogether_options.batched_enemy_deaths[self.network_id] = true + .die(args) + + +func update_client_enemy(unit_dict : Dictionary) -> void: + set_deferred("position", Vector2(unit_dict["X_POS"], unit_dict["Y_POS"])) + + _current_movement.x = unit_dict["MOVE_X"] + _current_movement.y = unit_dict["MOVE_Y"] + + var modulate_a = unit_dict["MODULATE_A"] + var modulate_r = unit_dict["MODULATE_R"] + var modulate_g = unit_dict["MODULATE_G"] + var modulate_b = unit_dict["MODULATE_B"] + + sprite.self_modulate = Color8(modulate_r, modulate_g, modulate_b, modulate_a) + + call_deferred("update_animation", _current_movement) + + +func update_external_player_position(player_dict : Dictionary) -> void: + self.position.x = player_dict[EntityState.ENTITY_STATE_X_POS] + self.position.y = player_dict[EntityState.ENTITY_STATE_Y_POS] + + self.sprite.scale.x = player_dict[EntityState.ENTITY_STATE_SPRITE_SCALE] + + self._current_movement.x = player_dict[EntityState.ENTITY_STATE_X_MOVE] + self._current_movement.y = player_dict[EntityState.ENTITY_STATE_Y_MOVE] + + if not dead: + self.update_animation(_current_movement) + + +func update_client_player(player_dict : Dictionary, player_index : int) -> void: + var current_xp = player_dict[EntityState.ENTITY_STATE_PLAYER_CURRENT_XP] + var next_level_xp = player_dict[EntityState.ENTITY_STATE_PLAYER_NEXT_LEVEL_XP] + RunData.players_data[player_index].current_xp = current_xp + RunData.emit_signal("xp_added", current_xp, next_level_xp, player_index) + + var current_hp = player_dict[EntityState.ENTITY_STATE_CURRENT_HP] + var max_hp = player_dict[EntityState.ENTITY_STATE_MAX_HP] + var should_send_hp_signal = false + if current_hp != self.current_stats.health: + should_send_hp_signal = true + self.current_stats.health = current_hp + if max_hp != self.max_stats.health: + should_send_hp_signal = true + self.max_stats.health = max_hp + if should_send_hp_signal: + self.emit_signal("health_updated", self, self.current_stats.health, self.max_stats.health) + + var current_gold = player_dict[EntityState.ENTITY_STATE_PLAYER_GOLD] + var current_level = player_dict[EntityState.ENTITY_STATE_PLAYER_LEVEL] + RunData.players_data[player_index].gold = current_gold + RunData.players_data[player_index].current_level = current_level + + RunData.emit_signal("gold_changed", current_gold, player_index) + + + + var weapons_array = player_dict[EntityState.ENTITY_STATE_PLAYER_WEAPONS] + for weapon_index in weapons_array.size(): + var weapon_dict = weapons_array[weapon_index] + var weapon = self.current_weapons[weapon_index] + + weapon.sprite.position.x = weapon_dict[WeaponState.WEAPON_STATE_X_POS] + weapon.sprite.position.y = weapon_dict[WeaponState.WEAPON_STATE_Y_POS] + weapon.sprite.rotation = weapon_dict[WeaponState.WEAPON_STATE_SPRITE_ROTATION] + weapon.rotation = weapon_dict[WeaponState.WEAPON_STATE_ROTATION] + weapon._is_shooting = weapon_dict[WeaponState.WEAPON_STATE_IS_SHOOTING] + weapon._current_cooldown = 9999 diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/global/effects_manager.gd b/src/mods-unpacked/Brogether-Brogether/extensions/global/effects_manager.gd new file mode 100644 index 0000000..8b0d433 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/global/effects_manager.gd @@ -0,0 +1,39 @@ +extends "res://global/effects_manager.gd" + +var steam_connection +var brotatogether_options +var in_multiplayer_game = false +var is_host = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + if in_multiplayer_game: + is_host = steam_connection.is_host() + + +func play_hit_particles(effect_pos: Vector2, direction: Vector2, effect_scale: float) -> void: + .play_hit_particles(effect_pos, direction, effect_scale) + + if in_multiplayer_game and self.is_host: + var hit_particle_dict = {} + hit_particle_dict["X_POS"] = effect_pos.x + hit_particle_dict["Y_POS"] = effect_pos.y + hit_particle_dict["X_DIR"] = direction.x + hit_particle_dict["Y_DIR"] = direction.y + hit_particle_dict["SCALE"] = effect_scale + brotatogether_options.batched_hit_particles.push_back(hit_particle_dict) + + +func play_hit_effect(effect_pos: Vector2, _direction: Vector2, effect_scale: float)->void : + .play_hit_effect(effect_pos, _direction, effect_scale) + + if in_multiplayer_game and self.is_host: + var hit_effect_dict = {} + hit_effect_dict["X_POS"] = effect_pos.x + hit_effect_dict["Y_POS"] = effect_pos.y + hit_effect_dict["SCALE"] = effect_scale + brotatogether_options.batched_hit_effects.push_back(hit_effect_dict) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/global/entity_spawner.gd b/src/mods-unpacked/Brogether-Brogether/extensions/global/entity_spawner.gd new file mode 100644 index 0000000..0909ee6 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/global/entity_spawner.gd @@ -0,0 +1,21 @@ +extends "res://global/entity_spawner.gd" + +var steam_connection +var brotatogether_options + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + + +func spawn(queue_from: Array, player_index: = - 1) -> void: + if not brotatogether_options.in_multiplayer_game: + .spawn(queue_from, player_index) + return + + if not steam_connection.is_host(): + queue_from.pop_back() + return + + .spawn(queue_from, player_index) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/items/global/item.gd b/src/mods-unpacked/Brogether-Brogether/extensions/items/global/item.gd new file mode 100644 index 0000000..91f3bbb --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/items/global/item.gd @@ -0,0 +1,18 @@ +extends "res://items/global/item.gd" + +var steam_connection +var brotatogether_options +var in_multiplayer_game = false +var network_id = 0 +var is_host = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + if in_multiplayer_game: + is_host = steam_connection.is_host() + network_id = brotatogether_options.current_network_id + brotatogether_options.current_network_id = brotatogether_options.current_network_id + 1 diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/main.gd b/src/mods-unpacked/Brogether-Brogether/extensions/main.gd new file mode 100644 index 0000000..f6aa520 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/main.gd @@ -0,0 +1,1613 @@ +extends "res://main.gd" + +var ClientMovementBehavior = load("res://mods-unpacked/Brogether-Brogether/client/client_movement_behavior.gd") +var ClientAttackBehavior = load("res://mods-unpacked/Brogether-Brogether/client/client_attack_behavior.gd") +var ClientTargetBehavior = load("res://mods-unpacked/Brogether-Brogether/client/client_target_behavior.gd") +var explosion_scene = load("res://projectiles/explosion.tscn") +var DataNode = load("res://mods-unpacked/Brogether-Brogether/data_node.gd") + +var steam_connection +var brotatogether_options +var in_multiplayer_game = false + +# If true, the steam logic will be skipped to avoid duplicate rpc chains. +var is_self_call = false + +var player_in_scene = [true, false, false, false] +var waiting_to_start_round = false + +const SEND_RATE : float = 1.0 / 30.0 +var send_timer = SEND_RATE +var my_player_index + +var client_players = {} +var client_enemies = {} +var client_births = {} +var client_player_projectiles = {} +var client_enemy_projectiles = {} +var client_items = {} +var client_consumables = {} +var client_neutrals = {} +var client_structures = {} +var client_pets = {} + +# Used to dedupe spawns +var client_pending_ids = {} + +var _spawning_ghosts: bool = false +const GHOST_DELAY_MSEC: int = 100 +var _ghost_queue: Array = [] +var _ghost_players: Dictionary = {} +var _ghost_births: Dictionary = {} + +var server_player_projectile_ids = {} +var server_enemy_projectile_ids = {} + +var ENTITY_BIRTH_SCENE = load("res://entities/birth/entity_birth.tscn") +var TREE_SCENE = load("res://entities/units/neutral/tree.tscn") +var CLIENT_TURRET_STATS = load("res://entities/structures/turret/turret_stats.tres") + +const ENABLE_DEBUG = false +const LOG_SLOW_US: int = 5000 +const LOG_INTERVAL: int = 300 +const LOG_FRAME_SLOW_US: int = 25000 +const LOG_WAVE_END_WINDOW: float = 15.0 +const LOG_WAVE_END_INTERVAL: int = 30 + +var debug_frame_counter : int = 0 +var _frame_log_counter: int = 0 +var _physics_step_count: int = 0 + +# This is currently a half-completed attempt ot shrink some of the overhead +# in messaging. With a bit of testing, the gains weren't that great given +# compression so it may not get done. +enum EntityState { + ENTITY_STATE_NETWORK_ID, + + ENTITY_STATE_X_POS, + ENTITY_STATE_Y_POS, + ENTITY_STATE_X_MOVE, + ENTITY_STATE_Y_MOVE, + ENTITY_STATE_CURRENT_HP, + ENTITY_STATE_MAX_HP, + ENTITY_STATE_SPRITE_SCALE, + + # Player-specific entity state. + ENTITY_STATE_PLAYER_GOLD, + ENTITY_STATE_PLAYER_CURRENT_XP, + ENTITY_STATE_PLAYER_NEXT_LEVEL_XP, + ENTITY_STATE_PLAYER_NUM_UPGRADES, + ENTITY_STATE_PLAYER_WEAPONS, + ENTITY_STATE_PLAYER_LEVEL, +} + +enum WeaponState { + WEAPON_STATE_X_POS, + WEAPON_STATE_Y_POS, + WEAPON_STATE_ROTATION, + WEAPON_STATE_SPRITE_ROTATION, + WEAPON_STATE_IS_SHOOTING, +} + +enum ProjectileState { + PROJECTILE_STATE_NETWORK_ID, + + PROJECTILE_STATE_X_POS, + PROJECTILE_STATE_Y_POS, + PROJECTILE_STATE_ROTATION, + PROJECTILE_STATE_FILENAME, +} + + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + if in_multiplayer_game: + steam_connection.connect("client_status_received", self, "_client_status_received") + steam_connection.connect("host_starts_round", self, "_host_starts_round") + steam_connection.connect("state_update", self, "_state_update") + steam_connection.connect("client_position", self, "_update_player_position") + steam_connection.connect("client_menu_focus", self, "_update_client_focus") + + steam_connection.connect("client_main_scene_reroll_button_pressed", self, "_client_reroll_button_pressed") + steam_connection.connect("client_main_scene_choose_upgrade_pressed", self, "_client_choose_upgrade_button_pressed") + steam_connection.connect("client_main_scene_take_button_pressed", self, "_client_take_button_pressed") + steam_connection.connect("client_main_scene_discard_button_pressed", self, "_client_discard_button_pressed") + + steam_connection.connect("host_entered_shop", self, "_host_entered_shop") + + my_player_index = steam_connection.get_my_index() + + call_deferred("multiplayer_ready") + + +func _physics_process(delta : float): + if not in_multiplayer_game: + return + + send_timer -= delta + if send_timer <= 0.0: + send_timer = SEND_RATE + if steam_connection.is_host(): + if ENABLE_DEBUG: + var send_before_us = Time.get_ticks_usec() + _send_game_state() + var send_us = Time.get_ticks_usec() - send_before_us + if send_us > LOG_SLOW_US: + var enemy_count = _entity_spawner.enemies.size() if is_instance_valid(_entity_spawner) else -1 + print("BTPROF type=send send_us=%d enemies=%d" % [send_us, enemy_count]) + else: + _send_game_state() + else: + _send_client_position() + + if ENABLE_DEBUG: + _physics_step_count += 1 + + if brotatogether_options.is_solo_test: + var now = Time.get_ticks_msec() + while _ghost_queue.size() > 0 and now - _ghost_queue[0]["time"] >= GHOST_DELAY_MSEC: + _spawning_ghosts = true + var entry = _ghost_queue.pop_front() + _ghost_state_update(entry["state"], now - entry["time"]) + _spawning_ghosts = false + + +func _process(_delta): + if in_multiplayer_game: + if ENABLE_DEBUG: + var frame_us = int(_delta * 1_000_000) + var wave_time_left = _wave_timer.time_left if is_instance_valid(_wave_timer) else -1.0 + var near_wave_end = wave_time_left >= 0.0 and wave_time_left < LOG_WAVE_END_WINDOW + _frame_log_counter += 1 + if frame_us > LOG_FRAME_SLOW_US or (near_wave_end and _frame_log_counter % LOG_WAVE_END_INTERVAL == 0): + print("BTPROF type=frame frame_us=%d wave_time=%.1f physics_steps=%d" % [frame_us, wave_time_left, _physics_step_count]) + _physics_step_count = 0 + if waiting_to_start_round: + if steam_connection.is_host(): + var all_players_entered = true + for player_index in RunData.get_player_count(): + if not player_in_scene[player_index]: + all_players_entered = false + break + if all_players_entered: + waiting_to_start_round = false + _wave_timer.start() + _wave_timer.paused = false + steam_connection.send_round_start() + + +func _send_game_state() -> void: + var state_dict = {} + + state_dict["WAVE_TIME"] = _wave_timer.time_left + state_dict["BONUS_GOLD"] = RunData.bonus_gold + + var players = [] + for player_index in _players.size(): + var player = _players[player_index] + players.push_back(_dictionary_for_player(player, player_index)) + state_dict["PLAYERS"] = players + + var enemies = [] + for enemy in _entity_spawner.enemies: + if is_instance_valid(enemy): + enemies.push_back(_dictionary_for_enemy(enemy)) + state_dict["ENEMIES"] = enemies + + var bosses = [] + for boss in _entity_spawner.bosses: + if is_instance_valid(boss): + bosses.push_back(_dictionary_for_enemy(boss)) + state_dict["BOSSES"] = bosses + + state_dict["BATCHED_ENEMY_DEATHS"] = brotatogether_options.batched_enemy_deaths.duplicate() + brotatogether_options.batched_enemy_deaths.clear() + + state_dict["BATCHED_HIT_EFFECTS"] = brotatogether_options.batched_hit_effects.duplicate() + brotatogether_options.batched_hit_effects.clear() + + state_dict["BATCHED_HIT_PARTICLES"] = brotatogether_options.batched_hit_particles.duplicate() + brotatogether_options.batched_hit_particles.clear() + + state_dict["BATCHED_FLOATING_TEXT"] = brotatogether_options.batched_floating_text.duplicate() + brotatogether_options.batched_floating_text.clear() + + state_dict["BATCHED_UNIT_FLASHES"] = brotatogether_options.batched_unit_flashes.duplicate() + brotatogether_options.batched_unit_flashes.clear() + + state_dict["BATCHED_2D_SOUNDS"] = brotatogether_options.batched_2d_sounds.duplicate() + brotatogether_options.batched_2d_sounds.clear() + + state_dict["BATCHED_SOUNDS"] = brotatogether_options.batched_sounds.duplicate() + brotatogether_options.batched_sounds.clear() + + state_dict["BATCHED_EXPLOSIONS"] = brotatogether_options.batched_explosions.duplicate() + brotatogether_options.batched_explosions.clear() + + state_dict["BIRTHS"] = _host_births_array() + state_dict["PLAYER_PROJECTILES"] = _host_player_projectiles_array() + state_dict["ITEMS"] = _host_items_array() + state_dict["CONSUMABLES"] = _host_consumables_array() + state_dict["NEUTRALS"] = _host_neutrals_array() + state_dict["STRUCTURES"] = _host_structures_array() + state_dict["PETS"] = _host_pets_array() + state_dict["ENEMY_PROJECTILES"] = _host_enemy_projectiles_array() + state_dict["UPGRADE_MENU_STATUS"] = _host_menu_status() + + if ENABLE_DEBUG: + var size_by_key = {} + for key in state_dict: + var compressed_data: PoolByteArray = var2bytes(state_dict[key]).compress(File.COMPRESSION_GZIP) + var compressed_size = compressed_data.size() + if compressed_size > 200: + if state_dict[key] is Array: + size_by_key[key] = "%d (%.1f) " % [compressed_size, compressed_size / state_dict[key].size()] + else: + size_by_key[key] = compressed_size + + state_dict["SENT_AT_MS"] = Time.get_ticks_msec() + steam_connection.send_game_state(state_dict) + + if brotatogether_options.is_solo_test: + _ghost_queue.push_back({"time": Time.get_ticks_msec(), "state": state_dict}) + + +func _state_update(state_dict : Dictionary) -> void: + var start_time : int = Time.get_ticks_usec() + var before : int + + before = Time.get_ticks_usec() + var wait_time = float(state_dict["WAVE_TIME"]) + if wait_time > 0: + _wave_timer.start(wait_time) + var _wave_timer_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + var bonus_gold = state_dict["BONUS_GOLD"] + if bonus_gold > 0: + _ui_bonus_gold.show() + RunData.bonus_gold = bonus_gold + _ui_bonus_gold.update_value(bonus_gold) + on_bonus_gold_changed(bonus_gold) + var _bonus_gold_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + var players_array = state_dict["PLAYERS"] + for player_index in players_array.size(): + _update_player_position(players_array[player_index], player_index) + var _player_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + var enemies_array = state_dict["ENEMIES"] + for enemy in enemies_array: + _update_enemy(enemy) + var enemies_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + var bosses_array = state_dict["BOSSES"] + for boss in bosses_array: + _update_enemy(boss) + var bosses_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for enemy_id in state_dict["BATCHED_ENEMY_DEATHS"]: + if client_enemies.has(enemy_id): + if not client_enemies[enemy_id].dead: + client_enemies[enemy_id].die() + else: + push_warning("BT: ghost enemy %s already dead at death batch time" % str(enemy_id)) + client_enemies.erase(enemy_id) + var enemy_deaths_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for flashing_unit_id in state_dict["BATCHED_UNIT_FLASHES"]: + var flashing_unit + if client_enemies.has(flashing_unit_id): + flashing_unit = client_enemies[flashing_unit_id] + elif client_players.has(flashing_unit_id): + flashing_unit = client_players[flashing_unit_id] + elif client_neutrals.has(flashing_unit_id): + flashing_unit = client_neutrals[flashing_unit_id] + if flashing_unit and is_instance_valid(flashing_unit): + flashing_unit.flash() + var _flashing_units_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for explosion in state_dict["BATCHED_EXPLOSIONS"]: + call_deferred("spawn_explosion", explosion) + var _explosion_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_player_projectiles(state_dict["PLAYER_PROJECTILES"]) + var player_projectiles_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + call_deferred("_update_births", state_dict["BIRTHS"]) + var births_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_items(state_dict["ITEMS"]) + var items_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_consumables(state_dict["CONSUMABLES"]) + var consumables_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_neutrals(state_dict["NEUTRALS"]) + var neutrals_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_structures(state_dict["STRUCTURES"]) + var structures_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_pets(state_dict["PETS"]) + var pets_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_enemy_projectiles(state_dict["ENEMY_PROJECTILES"]) + var enemy_projectiles_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_batched_hit_effects(state_dict["BATCHED_HIT_EFFECTS"]) + var enemy_hit_effects_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_batched_hit_particles(state_dict["BATCHED_HIT_PARTICLES"]) + var enemy_hit_particles_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_batched_floating_text(state_dict["BATCHED_FLOATING_TEXT"]) + var floating_text_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for sound in state_dict["BATCHED_SOUNDS"]: + SoundManager.call_deferred("play", load(sound["RESOURCE_PATH"])) + var sound_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for sound in state_dict["BATCHED_2D_SOUNDS"]: + SoundManager2D.call_deferred("play", load(sound["RESOURCE_PATH"]), Vector2(sound["X_POS"], sound["Y_POS"])) + var sound_2d_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_menu(state_dict["UPGRADE_MENU_STATUS"]) + var _menu_update_time = Time.get_ticks_usec() - before + + if ENABLE_DEBUG: + var total_us = Time.get_ticks_usec() - start_time + var net_ms = Time.get_ticks_msec() - state_dict.get("SENT_AT_MS", Time.get_ticks_msec()) + debug_frame_counter += 1 + if debug_frame_counter % LOG_INTERVAL == 0 or total_us > LOG_SLOW_US: + print("BTPROF type=client frame=%d net_ms=%d total_us=%d enemies_us=%d bosses_us=%d items_us=%d consumables_us=%d proj_us=%d births_us=%d neutrals_us=%d structures_us=%d pets_us=%d effects_us=%d sounds_us=%d deaths_us=%d players=%d enemies=%d bosses=%d projectiles=%d items=%d consumables=%d neutrals=%d structures=%d pets=%d births=%d" % [ + debug_frame_counter, net_ms, total_us, + enemies_update_time, bosses_update_time, + items_update_time, consumables_update_time, + player_projectiles_update_time + enemy_projectiles_update_time, + births_update_time, neutrals_update_time, structures_update_time, pets_update_time, + enemy_hit_effects_update_time + enemy_hit_particles_update_time + floating_text_update_time, + sound_update_time + sound_2d_update_time, + enemy_deaths_update_time, + state_dict["PLAYERS"].size(), state_dict["ENEMIES"].size(), state_dict["BOSSES"].size(), + state_dict["PLAYER_PROJECTILES"].size() + state_dict["ENEMY_PROJECTILES"].size(), + state_dict["ITEMS"].size(), state_dict["CONSUMABLES"].size(), + state_dict["NEUTRALS"].size(), state_dict["STRUCTURES"].size(), + state_dict["PETS"].size(), state_dict["BIRTHS"].size()]) + + +func _input(event) -> void: + if not ENABLE_DEBUG: + return + if event is InputEventKey: + if event.scancode == KEY_F1: +# on_levelled_up(0) + var consumable = consumable_scene.instance() + var consumable_to_spawn = load("res://items/consumables/item_box/item_box_data.tres") + consumable.consumable_data = consumable_to_spawn + consumable.set_texture(consumable_to_spawn.icon) + on_consumable_picked_up(consumable , 0) + + +func _send_client_position() -> void: + var in_postwave_menu = _coop_upgrades_ui.visible + if in_postwave_menu: + var player_container = _coop_upgrades_ui._get_player_container(my_player_index) + var focus_string = _string_for_menu_focus(player_container) + steam_connection.send_client_menu_focus( + { + "FOCUS" : focus_string + } + ) + else: + steam_connection.send_client_position(_dictionary_for_player(_players[my_player_index], my_player_index)) + + +func _dictionary_for_player(player, player_index) -> Dictionary: + var player_run_data = RunData.players_data[player_index] + var player_dict = { + EntityState.ENTITY_STATE_NETWORK_ID : player.network_id, + + EntityState.ENTITY_STATE_X_POS : player.position.x, + EntityState.ENTITY_STATE_Y_POS : player.position.y, + + EntityState.ENTITY_STATE_X_MOVE : player._current_movement.x, + EntityState.ENTITY_STATE_Y_MOVE : player._current_movement.y, + + EntityState.ENTITY_STATE_SPRITE_SCALE: player.sprite.scale.x, + EntityState.ENTITY_STATE_PLAYER_LEVEL: player_run_data.current_level + } + + var weapons_array : Array = [] + var weapons = player.current_weapons + for weapon in weapons: + var weapon_dict = {} + + weapon_dict[WeaponState.WEAPON_STATE_SPRITE_ROTATION] = weapon.sprite.rotation + weapon_dict[WeaponState.WEAPON_STATE_ROTATION] = weapon.rotation + weapon_dict[WeaponState.WEAPON_STATE_X_POS] = weapon.sprite.position.x + weapon_dict[WeaponState.WEAPON_STATE_Y_POS] = weapon.sprite.position.y + weapon_dict[WeaponState.WEAPON_STATE_IS_SHOOTING] = weapon._is_shooting + + weapons_array.push_back(weapon_dict) + player_dict[EntityState.ENTITY_STATE_PLAYER_WEAPONS] = weapons_array + + if steam_connection.is_host(): + player_dict[EntityState.ENTITY_STATE_PLAYER_GOLD] = RunData.players_data[player_index].gold + player_dict[EntityState.ENTITY_STATE_PLAYER_CURRENT_XP] = RunData.players_data[player_index].current_xp + player_dict[EntityState.ENTITY_STATE_PLAYER_NEXT_LEVEL_XP] = RunData.get_next_level_xp_needed(player_index) + player_dict[EntityState.ENTITY_STATE_CURRENT_HP] = player.current_stats.health + player_dict[EntityState.ENTITY_STATE_MAX_HP] = player.max_stats.health + + var things_to_process = _things_to_process_player_containers[player_index] + player_dict[EntityState.ENTITY_STATE_PLAYER_NUM_UPGRADES] = things_to_process.upgrades._elements.size() + + return player_dict + + +func _update_player_position(player_dict : Dictionary, player_index : int) -> void: + var player = _players[player_index] + var network_id = player_dict[EntityState.ENTITY_STATE_NETWORK_ID] + + if not client_players.has(network_id): + client_players[network_id] = player + + if player_index != my_player_index: + if player.dead: + return + player.call_deferred("update_external_player_position", player_dict) + + if not steam_connection.is_host(): + _players_ui[player_index].call_deferred("update_level_label") + player.call_deferred("update_client_player", player_dict, player_index) + + var things_to_process = _things_to_process_player_containers[player_index] + if player_dict[EntityState.ENTITY_STATE_PLAYER_NUM_UPGRADES] > things_to_process.upgrades._elements.size(): + things_to_process.upgrades.add_element(ItemService.get_icon(Keys.icon_upgrade_to_process_hash), 1) + + +func _dictionary_for_enemy(enemy) -> Dictionary: + var enemy_dict = {} + + enemy_dict["NETWORK_ID"] = enemy.network_id + enemy_dict["RESOURCE_PATH"] = enemy.stats.resource_path + enemy_dict["FILENAME"] = enemy.filename + enemy_dict["X_POS"] = enemy.position.x + enemy_dict["Y_POS"] = enemy.position.y + enemy_dict["MOVE_X"] = enemy._current_movement.x + enemy_dict["MOVE_Y"] = enemy._current_movement.y + + enemy_dict["MODULATE_A"] = enemy.sprite.self_modulate.a8 + enemy_dict["MODULATE_R"] = enemy.sprite.self_modulate.r8 + enemy_dict["MODULATE_G"] = enemy.sprite.self_modulate.g8 + enemy_dict["MODULATE_B"] = enemy.sprite.self_modulate.b8 + + return enemy_dict + + +func _update_enemy(enemy_dict : Dictionary) -> void: + var enemy_id = enemy_dict["NETWORK_ID"] + if client_enemies.has(enemy_id): + var enemy = client_enemies[enemy_id] + + enemy.call_deferred("update_client_enemy", enemy_dict) + else: + if not client_pending_ids.has(enemy_id): + client_pending_ids[enemy_id] = true + call_deferred("spawn_enemy", enemy_dict) + + +func spawn_enemy(enemy_dict) -> void: + if not is_instance_valid(_entities_container): + return + + var filename = enemy_dict["FILENAME"] + var resource_path = enemy_dict["RESOURCE_PATH"] + var enemy_id = enemy_dict["NETWORK_ID"] + + var enemy = load(filename).instance() + enemy._entity_spawner_ref = _entity_spawner + + var position : Vector2 = Vector2(enemy_dict["X_POS"], enemy_dict["Y_POS"]) + enemy.position = position + enemy.stats = load(resource_path) + + client_enemies[enemy_id] = enemy + client_pending_ids.erase(enemy_id) + + var movement_behavior = enemy.get_node("MovementBehavior") + enemy.remove_child(movement_behavior) + var client_movement_behavior = ClientMovementBehavior.new() + client_movement_behavior.set_name("MovementBehavior") + enemy.add_child(client_movement_behavior, true) + enemy._current_movement_behavior = client_movement_behavior + + var attack_behaviors_to_replace = [] + for child in enemy.get_children(): + if child is AttackBehavior: + attack_behaviors_to_replace.push_back(child) + for ab in attack_behaviors_to_replace: + var ab_name = ab.name + enemy.remove_child(ab) + var client_ab = ClientAttackBehavior.new() + client_ab.set_name(ab_name) + enemy.add_child(client_ab, true) + var replaced_ab = enemy.get_node_or_null("AttackBehavior") + if replaced_ab: + enemy._current_attack_behavior = replaced_ab + + if brotatogether_options.is_solo_test: + enemy.collision_layer = 0 + enemy.collision_mask = 0 + var hurtbox = enemy.get_node_or_null("Hurtbox") + if hurtbox: + hurtbox.monitoring = false + hurtbox.monitorable = false + for shape in hurtbox.get_children(): + if shape is CollisionShape2D or shape is CollisionPolygon2D: + shape.disabled = true + + _entities_container.add_child(enemy) + enemy.set_meta("btg_ghost_enemy", true) + + +func _ghost_state_update(state_dict: Dictionary, queue_delay_ms: int = 0) -> void: + var ghost_start_us: int = Time.get_ticks_usec() + var before: int + + before = Time.get_ticks_usec() + _update_ghost_players(state_dict["PLAYERS"]) + var _player_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for enemy_dict in state_dict["ENEMIES"]: + _update_enemy(enemy_dict) + var enemies_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for boss_dict in state_dict["BOSSES"]: + _update_enemy(boss_dict) + var bosses_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + for enemy_id in state_dict["BATCHED_ENEMY_DEATHS"]: + if client_enemies.has(enemy_id): + if not client_enemies[enemy_id].dead: + client_enemies[enemy_id].die() + else: + push_warning("BT: ghost enemy %s already dead at death batch time" % str(enemy_id)) + client_enemies.erase(enemy_id) + var enemy_deaths_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_enemy_projectiles(state_dict["ENEMY_PROJECTILES"]) + _update_player_projectiles(state_dict["PLAYER_PROJECTILES"]) + var proj_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + call_deferred("_update_ghost_births", state_dict["BIRTHS"]) + var births_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_items(state_dict["ITEMS"]) + var items_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_consumables(state_dict["CONSUMABLES"]) + var consumables_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_neutrals(state_dict["NEUTRALS"]) + var neutrals_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_structures(state_dict["STRUCTURES"]) + var structures_update_time = Time.get_ticks_usec() - before + + before = Time.get_ticks_usec() + _update_pets(state_dict["PETS"]) + var pets_update_time = Time.get_ticks_usec() - before + + for explosion in state_dict["BATCHED_EXPLOSIONS"]: + call_deferred("spawn_explosion", explosion) + + before = Time.get_ticks_usec() + _apply_ghost_modulate() + var modulate_us = Time.get_ticks_usec() - before + + if ENABLE_DEBUG: + var total_us = Time.get_ticks_usec() - ghost_start_us + debug_frame_counter += 1 + var wave_time_left = _wave_timer.time_left if is_instance_valid(_wave_timer) else -1.0 + var near_wave_end = wave_time_left >= 0.0 and wave_time_left < LOG_WAVE_END_WINDOW + if debug_frame_counter % LOG_INTERVAL == 0 or total_us > LOG_SLOW_US or near_wave_end: + print("BTPROF type=ghost frame=%d queue_delay_ms=%d total_us=%d modulate_us=%d enemies_us=%d bosses_us=%d items_us=%d consumables_us=%d proj_us=%d births_us=%d neutrals_us=%d structures_us=%d pets_us=%d deaths_us=%d wave_time=%.1f players=%d enemies=%d bosses=%d projectiles=%d items=%d consumables=%d neutrals=%d structures=%d pets=%d births=%d" % [ + debug_frame_counter, queue_delay_ms, total_us, modulate_us, + enemies_update_time, bosses_update_time, + items_update_time, consumables_update_time, + proj_update_time, births_update_time, + neutrals_update_time, structures_update_time, pets_update_time, + enemy_deaths_update_time, wave_time_left, + state_dict["PLAYERS"].size(), state_dict["ENEMIES"].size(), state_dict["BOSSES"].size(), + state_dict["PLAYER_PROJECTILES"].size() + state_dict["ENEMY_PROJECTILES"].size(), + state_dict["ITEMS"].size(), state_dict["CONSUMABLES"].size(), + state_dict["NEUTRALS"].size(), state_dict["STRUCTURES"].size(), + state_dict["PETS"].size(), state_dict["BIRTHS"].size()]) + + +func _apply_ghost_modulate() -> void: + var ghost_color = Color(0.5, 0.8, 1.0, 0.35) + for entity in client_enemies.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + if entity is CollisionObject2D: + entity.collision_layer = 0 + entity.collision_mask = 0 + for entity in client_enemy_projectiles.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + if entity is CollisionObject2D: + entity.collision_layer = 0 + entity.collision_mask = 0 + for entity in client_player_projectiles.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + if entity is CollisionObject2D: + entity.collision_layer = 0 + entity.collision_mask = 0 + for entity in client_items.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + entity.set_physics_process(false) + for entity in client_consumables.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + entity.set_physics_process(false) + for entity in client_neutrals.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + for entity in client_structures.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + for entity in _ghost_births.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + for entity in client_pets.values(): + if is_instance_valid(entity): + entity.modulate = ghost_color + + +func _update_ghost_players(players_array: Array) -> void: + for player_index in players_array.size(): + var player_dict = players_array[player_index] + var player = _players[player_index] + + if not is_instance_valid(player) or player.dead: + if _ghost_players.has(player_index): + if is_instance_valid(_ghost_players[player_index]): + _ghost_players[player_index].queue_free() + _ghost_players.erase(player_index) + continue + + var ghost + if _ghost_players.has(player_index) and is_instance_valid(_ghost_players[player_index]): + ghost = _ghost_players[player_index] + else: + ghost = Node2D.new() + var ghost_sprite = _make_ghost_player_sprite(player) + ghost_sprite.name = "GhostSprite" + ghost.add_child(ghost_sprite) + _entities_container.add_child(ghost) + _ghost_players[player_index] = ghost + + ghost.position = Vector2( + player_dict[EntityState.ENTITY_STATE_X_POS], + player_dict[EntityState.ENTITY_STATE_Y_POS] + ) + var ghost_sprite = ghost.get_node_or_null("GhostSprite") + if ghost_sprite: + ghost_sprite.scale.x = player_dict[EntityState.ENTITY_STATE_SPRITE_SCALE] + ghost.modulate = Color(0.5, 0.8, 1.0, 0.35) + + +func _make_ghost_player_sprite(player) -> Node2D: + var src = player.sprite + if src is AnimatedSprite: + var frames = src.frames + if frames and frames.has_animation(src.animation): + var texture = frames.get_frame(src.animation, src.frame) + if texture: + var s = Sprite.new() + s.texture = texture + return s + elif src is Sprite: + var s = Sprite.new() + s.texture = src.texture + return s + var fallback = Polygon2D.new() + var pts = PoolVector2Array() + for i in 12: + var a = i * TAU / 12.0 + pts.append(Vector2(cos(a) * 16.0, sin(a) * 16.0)) + fallback.set_polygon(pts) + return fallback + + +func _update_ghost_births(births_array: Array) -> void: + var current_births = {} + for birth_dict in births_array: + var network_id = birth_dict["NETWORK_ID"] + current_births[network_id] = true + if not _ghost_births.has(network_id): + var birth = ENTITY_BIRTH_SCENE.instance() + birth.network_id = network_id + _entities_container.add_child(birth) + _ghost_births[network_id] = birth + birth.start(birth_dict["TYPE"], ENTITY_BIRTH_SCENE, + Vector2(birth_dict["X_POS"], birth_dict["Y_POS"])) + birth._collision_shape.set_deferred("disabled", true) + + var to_remove = [] + for network_id in _ghost_births: + if not current_births.has(network_id): + to_remove.push_back(network_id) + for network_id in to_remove: + if is_instance_valid(_ghost_births[network_id]): + _ghost_births[network_id].queue_free() + _ghost_births.erase(network_id) + + +func multiplayer_ready(): + _wave_timer.stop() + waiting_to_start_round = true + + +func _client_status_received(client_data : Dictionary, player_index : int) -> void: + if waiting_to_start_round: + if client_data["CURRENT_SCENE"] == "Main": + player_in_scene[player_index] = true + + +func _host_starts_round() -> void: + waiting_to_start_round = false + _wave_timer.start() + _wave_timer.paused = false + + +func _on_WaveTimer_timeout() -> void: + if in_multiplayer_game: + if steam_connection.is_host(): + ._on_WaveTimer_timeout() + return + else: + _ui_dim_screen.dim() + _wave_cleared_label.hide() + _wave_timer_label.hide() + _hud.hide() + + _coop_upgrades_ui.propagate_call("set_process_input", [true]) + DebugService.log_data("_on_EndWaveTimer_timeout") + SoundManager.clear_queue() + SoundManager2D.clear_queue() + InputService.set_gamepad_echo_processing(true) + _cleaning_up = true + + for player in _players: + if is_instance_valid(player): + player._can_move = false + player.set_physics_process(false) + else: + ._on_WaveTimer_timeout() + + +func _on_EndWaveTimer_timeout()->void : + if in_multiplayer_game: + if steam_connection.is_host(): + if _is_run_lost or RunData.is_last_wave() or _is_run_won: + steam_connection.leave_game_lobby() + ._on_EndWaveTimer_timeout() + else: + ._on_EndWaveTimer_timeout() + + +func _host_births_array() -> Array: + var births_array = [] + + for birth in _births_container.get_children(): + if birth._collision_shape.disabled: + continue + var birth_dict = {} + + birth_dict["NETWORK_ID"] = birth.network_id + + birth_dict["X_POS"] = birth.global_position.x + birth_dict["Y_POS"] = birth.global_position.y + + birth_dict["COLOR_R"] = birth._color.r + birth_dict["COLOR_G"] = birth._color.g + birth_dict["COLOR_B"] = birth._color.b + birth_dict["COLOR_A"] = birth._color.a + birth_dict["TYPE"] = birth.type + + births_array.push_back(birth_dict) + + return births_array + + +func _update_births(births_array : Array) -> void: + var current_births : Dictionary = {} + + for birth_dict in births_array: + var network_id = birth_dict["NETWORK_ID"] + current_births[network_id] = true + + var birth + if client_births.has(network_id): + birth = client_births[network_id] + else: + birth = ENTITY_BIRTH_SCENE.instance() + birth.network_id = network_id + _births_container.add_child(birth) + client_births[network_id] = birth + birth.start(birth_dict["TYPE"], ENTITY_BIRTH_SCENE, Vector2(birth_dict["X_POS"], birth_dict["Y_POS"])) + + # TODO, I don't think we need these colors anymore + birth._color.r = birth_dict["COLOR_R"] + birth._color.g = birth_dict["COLOR_G"] + birth._color.b = birth_dict["COLOR_B"] + birth._color.a = birth_dict["COLOR_A"] + + for network_id in client_births: + var old_birth = client_births[network_id] + if not current_births.has(network_id): + client_births.erase(network_id) + old_birth.queue_free() + + +func _host_player_projectiles_array() -> Array: + var player_projectiles_array = [] + + for child in _player_projectiles.get_children(): + if child is PlayerProjectile and child._hitbox.active: + var projectile_dict = {} + var network_id + if server_player_projectile_ids.has(child): + network_id = server_player_projectile_ids[child] + else: + network_id = brotatogether_options.current_network_id + brotatogether_options.current_network_id = brotatogether_options.current_network_id + 1 + server_player_projectile_ids[child] = network_id + + projectile_dict["NETWORK_ID"] = network_id + projectile_dict["X_POS"] = child.position.x + projectile_dict["Y_POS"] = child.position.y + projectile_dict["ROTATION"] = child.rotation + projectile_dict["FILENAME"] = child.filename + player_projectiles_array.push_back(projectile_dict) + return player_projectiles_array + + +func _update_player_projectiles(player_projectiles_array : Array) -> void: + var current_projectiles = {} + + for player_projectile_dict in player_projectiles_array: + var network_id = player_projectile_dict["NETWORK_ID"] + current_projectiles[network_id] = true + + if client_player_projectiles.has(network_id): + var projectile = client_player_projectiles[network_id] + + if is_instance_valid(projectile): + projectile.position.x = player_projectile_dict["X_POS"] + projectile.position.y = player_projectile_dict["Y_POS"] + projectile.rotation = player_projectile_dict["ROTATION"] + else: + if not client_pending_ids.has(network_id): + client_pending_ids[network_id] = true + call_deferred("_spawn_player_projectile", player_projectile_dict) + + for network_id in client_player_projectiles: + if not current_projectiles.has(network_id): + var proj = client_player_projectiles[network_id] + if is_instance_valid(proj) and proj.get_parent() != null: + proj.queue_free() + client_player_projectiles.erase(network_id) + + +func _spawn_player_projectile(player_projectile_dict : Dictionary) -> void: + if not is_instance_valid(_player_projectiles): + return + + var network_id = player_projectile_dict["NETWORK_ID"] + var projectile = load(player_projectile_dict["FILENAME"]).instance() + + client_pending_ids.erase(network_id) + client_player_projectiles[network_id] = projectile + + projectile.position.x = player_projectile_dict["X_POS"] + projectile.position.y = player_projectile_dict["Y_POS"] + projectile.spawn_position.x = player_projectile_dict["X_POS"] + projectile.spawn_position.y = player_projectile_dict["Y_POS"] + + # The projectile will despawn if max_range is too low, the host will + # erase the projectile at the right time so set max range very high to + # disable client-side logic in this regard. + projectile._max_range = 9999 + + projectile.rotation = player_projectile_dict["ROTATION"] + + projectile.set_meta("pool_id", Keys.generate_hash(player_projectile_dict["FILENAME"])) + _player_projectiles.add_child(projectile) + + projectile._hitbox.active = false + projectile.set_physics_process(false) + projectile.show() + + +func _host_items_array() -> Array: + var items_array : Array = [] + for item in _active_golds: + if not is_instance_valid(item): + continue + var item_dict = {} + item_dict["NETWORK_ID"] = item.network_id + item_dict["X_POS"] = item.global_position.x + item_dict["Y_POS"] = item.global_position.y + item_dict["X_SCALE"] = item.scale.x + item_dict["Y_SCALE"] = item.scale.y + items_array.push_back(item_dict) + return items_array + + +func _update_items(items_array : Array) -> void: + var current_items = {} + + for item_dict in items_array: + var network_id = item_dict["NETWORK_ID"] + current_items[network_id] = true + if client_items.has(network_id): + var item = client_items[network_id] + item.global_position.x = item_dict["X_POS"] + item.global_position.y = item_dict["Y_POS"] + else: + call_deferred("_spawn_item", item_dict) + + for network_id in client_items: + if not current_items.has(network_id): + client_items[network_id].queue_free() + client_items.erase(network_id) + + +func _spawn_item(item_dict : Dictionary) -> void: + var network_id = item_dict["NETWORK_ID"] + var gold = gold_scene.instance() + gold.set_texture(gold_sprites[Utils.randi() % 11]) + gold.scale.x = item_dict["X_SCALE"] + gold.scale.y = item_dict["Y_SCALE"] + _materials_container.add_child(gold) + if not brotatogether_options.is_solo_test: + _active_golds.push_back(gold) + client_items[network_id] = gold + gold.call_deferred("show") + + +func _host_consumables_array() -> Array: + var consumables_array = [] + + for consumable in _consumables_container.get_children(): + if consumable.visible and consumable.consumable_data != null: + var consumable_dict = {} + consumable_dict["NETWORK_ID"] = consumable.network_id + consumable_dict["X_POS"] = consumable.global_position.x + consumable_dict["Y_POS"] = consumable.global_position.y + consumable_dict["LOAD_PATH"] = consumable.consumable_data.icon.load_path + consumables_array.push_back(consumable_dict) + + return consumables_array + + +func _update_consumables(consumables_array : Array) -> void: + var current_consumables = {} + + for consumable_dict in consumables_array: + var network_id = consumable_dict["NETWORK_ID"] + current_consumables[network_id] = true + if client_consumables.has(network_id): + var consumable = client_consumables[network_id] + consumable.global_position.x = consumable_dict["X_POS"] + consumable.global_position.y = consumable_dict["Y_POS"] + else: + call_deferred("_spawn_consumable", consumable_dict) + + for network_id in client_consumables: + if not current_consumables.has(network_id): + client_consumables[network_id].queue_free() + client_consumables.erase(network_id) + + +func _spawn_consumable(consumable_dict : Dictionary) -> void: + var consumable = get_node_from_pool(_consumable_pool_id, _consumables_container) + if consumable == null: + consumable = consumable_scene.instance() + _consumables_container.add_child(consumable) + + var file_path = consumable_dict["LOAD_PATH"] + consumable.set_texture(load(file_path)) + consumable.global_position.x = consumable_dict["X_POS"] + consumable.global_position.y = consumable_dict["Y_POS"] + consumable.call_deferred("show") + client_consumables[consumable_dict["NETWORK_ID"]] = consumable + + +func _host_neutrals_array() -> Array: + var neutrals_array = [] + + for neutral in _entity_spawner.neutrals: + var neutral_dict = {} + neutral_dict["NETWORK_ID"] = neutral.network_id + neutral_dict["X_POS"] = neutral.global_position.x + neutral_dict["Y_POS"] = neutral.global_position.y + neutrals_array.push_back(neutral_dict) + + return neutrals_array + + +func _update_neutrals(neutrals_array : Array) -> void: + var current_neutrals = {} + + for neutral_dict in neutrals_array: + var network_id = neutral_dict["NETWORK_ID"] + current_neutrals[network_id] = true + if client_neutrals.has(network_id): + pass + else: + call_deferred("_spawn_neutral", neutral_dict) + + for network_id in client_neutrals: + if not network_id in current_neutrals: + client_neutrals[network_id].queue_free() + client_neutrals.erase(network_id) + + +func _spawn_neutral(neutral_dict : Dictionary) -> void: + var neutral = TREE_SCENE.instance() + + neutral.collision_layer = 0 + neutral.collision_mask = 0 + neutral.mode = RigidBody2D.MODE_STATIC + var hurtbox = neutral.get_node_or_null("Hurtbox") + if hurtbox: + hurtbox.collision_layer = 0 + hurtbox.collision_mask = 0 + + client_neutrals[neutral_dict["NETWORK_ID"]] = neutral + + Utils.get_scene_node().add_child(neutral) + neutral.global_position = Vector2(neutral_dict["X_POS"], neutral_dict["Y_POS"]) + + if hurtbox: + hurtbox.monitoring = false + hurtbox.monitorable = false + for shape in hurtbox.get_children(): + if shape is CollisionShape2D or shape is CollisionPolygon2D: + shape.set_deferred("disabled", true) + + +func _host_structures_array() -> Array: + var structures_array = [] + + for structure in _entity_spawner.structures: + var structure_dict = {} + + structure_dict["NETWORK_ID"] = structure.network_id + structure_dict["FILENAME"] = structure.filename + structure_dict["X_POS"] = structure.position.x + structure_dict["Y_POS"] = structure.position.y + structures_array.push_back(structure_dict) + + return structures_array + + +func _update_structures(structures_array : Array) -> void: + var current_structures = {} + + for structure_dict in structures_array: + var network_id = structure_dict["NETWORK_ID"] + current_structures[network_id] = true + if client_structures.has(network_id): + pass + else: + call_deferred("_spawn_structure", structure_dict) + + for network_id in client_structures: + if not network_id in current_structures: + client_structures[network_id].queue_free() + client_structures.erase(network_id) + + +func _spawn_structure(structure_dict : Dictionary) -> void: + var filename = structure_dict["FILENAME"] + var structure = load(filename).instance() + + structure.position.x = structure_dict["X_POS"] + structure.position.y = structure_dict["Y_POS"] + + structure.stats = CLIENT_TURRET_STATS + + client_structures[structure_dict["NETWORK_ID"]] = structure + + Utils.get_scene_node().add_child(structure) + structure.set_physics_process(false) + structure.set_process(false) + + +func _host_pets_array() -> Array: + var pets_array = [] + for pet in _entity_spawner.pets: + if not is_instance_valid(pet) or pet.dead: + continue + var pet_dict = {} + pet_dict["NETWORK_ID"] = pet.network_id + pet_dict["FILENAME"] = pet.filename + pet_dict["X_POS"] = pet.position.x + pet_dict["Y_POS"] = pet.position.y + pet_dict["MOVE_X"] = pet._current_movement.x + pet_dict["MOVE_Y"] = pet._current_movement.y + pet_dict["MODULATE_R"] = pet.sprite.self_modulate.r8 + pet_dict["MODULATE_G"] = pet.sprite.self_modulate.g8 + pet_dict["MODULATE_B"] = pet.sprite.self_modulate.b8 + pet_dict["MODULATE_A"] = pet.sprite.self_modulate.a8 + pets_array.push_back(pet_dict) + return pets_array + + +func _update_pets(pets_array: Array) -> void: + var current_pets = {} + for pet_dict in pets_array: + var network_id = pet_dict["NETWORK_ID"] + current_pets[network_id] = true + if client_pets.has(network_id): + var pet = client_pets[network_id] + if is_instance_valid(pet): + pet.call_deferred("update_client_enemy", pet_dict) + else: + if not client_pending_ids.has(network_id): + client_pending_ids[network_id] = true + call_deferred("_spawn_pet", pet_dict) + var to_remove = [] + for network_id in client_pets: + if not current_pets.has(network_id): + to_remove.push_back(network_id) + for network_id in to_remove: + if is_instance_valid(client_pets[network_id]): + client_pets[network_id].queue_free() + client_pets.erase(network_id) + + +func _spawn_pet(pet_dict: Dictionary) -> void: + if not is_instance_valid(_entities_container): + return + + var network_id = pet_dict["NETWORK_ID"] + var pet = load(pet_dict["FILENAME"]).instance() + pet.position = Vector2(pet_dict["X_POS"], pet_dict["Y_POS"]) + + var movement_behavior = pet.get_node_or_null("MovementBehavior") + if movement_behavior: + pet.remove_child(movement_behavior) + var client_mb = ClientMovementBehavior.new() + client_mb.set_name("MovementBehavior") + pet.add_child(client_mb, true) + pet._current_movement_behavior = client_mb + + var target_behavior = pet.get_node_or_null("TargetBehavior") + if target_behavior: + pet.remove_child(target_behavior) + var client_tb = ClientTargetBehavior.new() + client_tb.set_name("TargetBehavior") + pet.add_child(client_tb, true) + pet._current_target_behavior = client_tb + + var attack_behavior = pet.get_node_or_null("AttackBehavior") + if attack_behavior: + pet.remove_child(attack_behavior) + var client_ab = ClientAttackBehavior.new() + client_ab.set_name("AttackBehavior") + pet.add_child(client_ab, true) + pet._current_attack_behavior = client_ab + + for child in pet.get_children(): + if child is CollisionObject2D: + child.collision_layer = 0 + child.collision_mask = 0 + + client_pets[network_id] = pet + client_pending_ids.erase(network_id) + _entities_container.add_child(pet) + pet.set_physics_process(false) + pet.set_process(false) + for child in pet.get_children(): + if child is Area2D: + child.monitoring = false + child.monitorable = false + + +func _host_enemy_projectiles_array() -> Array: + var enemy_projectiles_array = [] + + if is_instance_valid(_enemy_projectiles): + for enemy_projectile in _enemy_projectiles.get_children(): + if enemy_projectile is Projectile and enemy_projectile._hitbox.active: + var projectile_dict = {} + var network_id + if server_enemy_projectile_ids.has(enemy_projectile): + network_id = server_enemy_projectile_ids[enemy_projectile] + else: + network_id = brotatogether_options.current_network_id + brotatogether_options.current_network_id = brotatogether_options.current_network_id + 1 + server_enemy_projectile_ids[enemy_projectile] = network_id + + projectile_dict[ProjectileState.PROJECTILE_STATE_NETWORK_ID] = network_id + projectile_dict[ProjectileState.PROJECTILE_STATE_X_POS] = enemy_projectile.global_position.x + projectile_dict[ProjectileState.PROJECTILE_STATE_Y_POS] = enemy_projectile.global_position.y + projectile_dict[ProjectileState.PROJECTILE_STATE_ROTATION] = enemy_projectile.rotation + projectile_dict[ProjectileState.PROJECTILE_STATE_FILENAME] = enemy_projectile.filename + enemy_projectiles_array.push_back(projectile_dict) + return enemy_projectiles_array + + +func _update_enemy_projectiles(enemy_projectiles_array : Array) -> void: + var current_enemy_projectiles = {} + for enemy_projectile_dict in enemy_projectiles_array: + var network_id = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_NETWORK_ID] + current_enemy_projectiles[network_id] = true + + if client_enemy_projectiles.has(network_id): + var enemy_projectile = client_enemy_projectiles[network_id] + + if not is_instance_valid(enemy_projectile): + continue + + enemy_projectile.position.x = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_X_POS] + enemy_projectile.position.y = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_Y_POS] + enemy_projectile.rotation = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_ROTATION] + else: + call_deferred("_spawn_enemy_projectile", enemy_projectile_dict) + + for network_id in client_enemy_projectiles: + if not current_enemy_projectiles.has(network_id): + var proj = client_enemy_projectiles[network_id] + if is_instance_valid(proj) and proj.get_parent() != null: + proj.queue_free() + client_enemy_projectiles.erase(network_id) + + +func _spawn_enemy_projectile(enemy_projectile_dict : Dictionary) -> void: + if not is_instance_valid(_enemy_projectiles): + return + + var network_id = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_NETWORK_ID] + var enemy_projectile = load(enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_FILENAME]).instance() + client_enemy_projectiles[network_id] = enemy_projectile + + enemy_projectile.position.x = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_X_POS] + enemy_projectile.position.y = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_Y_POS] + + enemy_projectile.rotation = enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_ROTATION] + + enemy_projectile.set_meta("pool_id", Keys.generate_hash(enemy_projectile_dict[ProjectileState.PROJECTILE_STATE_FILENAME])) + _enemy_projectiles.add_child(enemy_projectile) + + enemy_projectile._hitbox.active = false + enemy_projectile.set_physics_process(false) + _enemy_projectiles.show() + + +func _update_batched_hit_particles(batched_hit_effects_array : Array) -> void: + for hit_effect_dict in batched_hit_effects_array: + var effect_pos = Vector2(hit_effect_dict["X_POS"], hit_effect_dict["Y_POS"]) + var direction = Vector2(hit_effect_dict["X_DIR"], hit_effect_dict["Y_DIR"]) + var effect_scale = hit_effect_dict["SCALE"] + _effects_manager.play_hit_effect(effect_pos, direction, effect_scale) + + +func _update_batched_hit_effects(batched_hit_particles_array : Array) -> void: + for hit_particles_dict in batched_hit_particles_array: + var effect_pos = Vector2(hit_particles_dict["X_POS"], hit_particles_dict["Y_POS"]) + var effect_scale = hit_particles_dict["SCALE"] + _effects_manager.play_hit_particles(effect_pos, Vector2.ZERO, effect_scale) + + +func _update_batched_floating_text(batched_floating_text_array : Array) -> void: + for floating_text_dict in batched_floating_text_array: + var value = floating_text_dict["VALUE"] + var text_pos = Vector2(floating_text_dict["X_POS"], floating_text_dict["Y_POS"]) + var color = Color8(floating_text_dict["R_COLOR"], floating_text_dict["G_COLOR"], floating_text_dict["B_COLOR"] ,floating_text_dict["A_COLOR"]) + var duration = floating_text_dict["DURATION"] +# _floating_text_manager.display(value, text_pos, color, null, duration) + _floating_text_manager.call_deferred("display", value, text_pos, color, null, duration) + + +func _host_menu_status() -> Dictionary: + var menu_dict = {} + + var showing_upgrade_menus : bool = _coop_upgrades_ui.visible + menu_dict["MENU_VISIBLE"] = showing_upgrade_menus + + if showing_upgrade_menus: + var player_menus = [] + for player_index in RunData.get_player_count(): + var player_menu_dict = {} +# _coop_upgrades_ui + var player_container = _coop_upgrades_ui._get_player_container(player_index) + + var showing_player_items_container = player_container._items_container.visible + var showing_player_upgrades_container = player_container._upgrades_container.visible + player_menu_dict["SHOWING_ITEMS"] = showing_player_items_container + player_menu_dict["SHOWING_UPGRADES"] = showing_player_upgrades_container + player_menu_dict["PLAYER_FINISHED"] = player_container._checkmark_group.visible + + if showing_player_upgrades_container: + var upgrade_options = [] + for upgrade in player_container._old_upgrades: + var upgrade_data = {} + upgrade_data["UPGRADE_ID"] = upgrade.my_id + upgrade_options.push_back(upgrade_data) + player_menu_dict["UPGRADE_OPTIONS"] = upgrade_options + player_menu_dict["REROLL_PRICE"] = player_container._reroll_price + elif showing_player_items_container: + player_menu_dict["ITEM_ID"] = player_container._item_data.my_id + + player_menu_dict["FOCUS"] = _string_for_menu_focus(player_container) + player_menus.push_back(player_menu_dict) + menu_dict["PLAYER_MENUS"] = player_menus + + return menu_dict + + +func _update_menu(menu_dict : Dictionary) -> void: + var showing_upgrade_menus : bool = menu_dict["MENU_VISIBLE"] + + if showing_upgrade_menus and not _coop_upgrades_ui.visible: + _coop_upgrades_ui.show() + elif not showing_upgrade_menus: + if _coop_upgrades_ui.visible: + _coop_upgrades_ui.hide() + return + + var player_menus = menu_dict["PLAYER_MENUS"] + for player_index in RunData.get_player_count(): + var player_container = _coop_upgrades_ui._get_player_container(player_index) + var player_menu_dict = player_menus[player_index] + + var showing_player_items_container = player_menu_dict["SHOWING_ITEMS"] + var showing_player_upgrades_container = player_menu_dict["SHOWING_UPGRADES"] + var player_finished = player_menu_dict["PLAYER_FINISHED"] + if showing_player_upgrades_container: + var upgrade_options : Array = player_menu_dict["UPGRADE_OPTIONS"] + var should_update_upgrades : bool = false + for i in upgrade_options.size(): + if i >= player_container._old_upgrades.size(): + should_update_upgrades = true + else: + var upgrade_data = upgrade_options[i] + if upgrade_data["UPGRADE_ID"] != player_container._old_upgrades[i].my_id: + should_update_upgrades = true + if should_update_upgrades: + var upgrades = [] + for i in upgrade_options.size(): + var upgrade_data = upgrade_options[i] + for dupe_candidate in ItemService.upgrades: + if dupe_candidate.my_id == upgrade_data["UPGRADE_ID"]: + upgrades.push_back(dupe_candidate.duplicate()) + player_container._old_upgrades = upgrades + var upgrade_uis = player_container._get_upgrade_uis() + for i in upgrade_options.size(): + var upgrade_ui = upgrade_uis[i] + upgrade_ui.visible = i < upgrades.size() + if upgrade_ui.visible: + upgrade_ui.set_upgrade(upgrades[i], player_index) + var reroll_price = player_menu_dict["REROLL_PRICE"] + player_container._reroll_button.init(reroll_price, player_index) + player_container._update_gold_label() + + player_container._items_container.hide() + player_container._upgrades_container.show() + player_container.focus() + + player_container._items_container.hide() + player_container._upgrades_container.show() + elif showing_player_items_container: + var item_id = player_menu_dict["ITEM_ID"] + + if player_container._item_data == null or item_id != player_container._item_data.my_id: + var item_data + for item_candidate in ItemService.items: + if item_id == item_candidate.my_id: + item_data = item_candidate.duplicate() + break + for item_candidate in ItemService.weapons: + if item_id == item_candidate.my_id: + item_data = item_candidate.duplicate() + break + player_container.show_item(item_data) + player_container.focus() + + player_container._items_container.show() + player_container._upgrades_container.hide() + if player_index != my_player_index: + _focus_for_string(player_container, player_menu_dict["FOCUS"]) + if player_finished: + player_container._items_container.hide() + player_container._upgrades_container.hide() + player_container._checkmark_group.show() + + +func _update_client_focus(data : Dictionary, player_index : int) -> void: + if steam_connection.is_host() and player_index != my_player_index: + var player_container = _coop_upgrades_ui._get_player_container(player_index) + _focus_for_string(player_container, data["FOCUS"]) + + +func _string_for_menu_focus(player_container : CoopUpgradesUIPlayerContainer) -> String: + var focus_emulator = player_container.focus_emulator + if not focus_emulator.focused_control: + return "" + + var focused_control = focus_emulator.focused_control + if focused_control == player_container._take_button: + return "take" + elif focused_control == player_container._discard_button: + return "discard" + elif focused_control == player_container._upgrade_ui_1.button: + return "upgrade_1" + elif focused_control == player_container._upgrade_ui_2.button: + return "upgrade_2" + elif focused_control == player_container._upgrade_ui_3.button: + return "upgrade_3" + elif focused_control == player_container._upgrade_ui_4.button: + return "upgrade_4" + elif focused_control == player_container._reroll_button: + return "reroll" + + return "" + + +func _focus_for_string(player_container : CoopUpgradesUIPlayerContainer, focus_key: String) -> void: + var requested_focused_control = null + + if focus_key == "take": + requested_focused_control = player_container._take_button + elif focus_key == "discard": + requested_focused_control = player_container._discard_button + elif focus_key == "upgrade_1": + requested_focused_control = player_container._upgrade_ui_1.button + elif focus_key == "upgrade_2": + requested_focused_control = player_container._upgrade_ui_2.button + elif focus_key == "upgrade_3": + requested_focused_control = player_container._upgrade_ui_3.button + elif focus_key == "upgrade_4": + requested_focused_control = player_container._upgrade_ui_4.button + elif focus_key == "reroll": + requested_focused_control = player_container._reroll_button + + if requested_focused_control: + player_container.focus_emulator.focused_control = requested_focused_control + + +func _client_reroll_button_pressed(player_index : int) -> void: + var player_container = _coop_upgrades_ui._get_player_container(player_index) + player_container._on_RerollButton_pressed() + + +func _client_choose_upgrade_button_pressed(upgrade_dict : Dictionary, player_index : int) -> void: + var player_container = _coop_upgrades_ui._get_player_container(player_index) + for upgrade_candidate in ItemService.upgrades: + if upgrade_candidate.my_id == upgrade_dict["UPGRADE_ID"]: + player_container._on_choose_button_pressed(upgrade_candidate.duplicate()) + + +func _client_take_button_pressed(player_index : int) -> void: + var player_container = _coop_upgrades_ui._get_player_container(player_index) + player_container._on_TakeButton_pressed() + + +func _client_discard_button_pressed(player_index : int) -> void: + var player_container = _coop_upgrades_ui._get_player_container(player_index) + player_container._on_DiscardButton_pressed() + + +func _host_entered_shop() -> void: + _change_scene(RunData.get_shop_scene_path()) + + +func _check_for_pause() -> void: + if not in_multiplayer_game: + ._check_for_pause() + + +func connect_visual_effects(unit: Unit)->void : + .connect_visual_effects(unit) + + if in_multiplayer_game: + if steam_connection.is_host(): + var _error_floating_text = unit.connect("took_damage", self, "_on_unit_took_damage") + + +func _on_unit_took_damage(unit: Unit, value: int, _knockback_direction: Vector2, is_crit: bool, is_dodge: bool, is_protected: bool, armor_did_something: bool, _args: TakeDamageArgs, _hit_type: int, _is_one_shot: bool)->void : + var color: Color = Color.white + var text = str(value) + + if unit is Player: + if value > 0: + color = Color.red + text = "-" + text + elif is_dodge: + text = "DODGE" + elif is_protected: + text = "NULLIFIED" + elif is_crit: + color = Color.yellow + elif armor_did_something: + color = Color.gray + + var text_dict = {} + var text_pos = unit.global_position + text_dict["X_POS"] = text_pos.x + text_dict["Y_POS"] = text_pos.y + text_dict["R_COLOR"] = color.r8 + text_dict["G_COLOR"] = color.g8 + text_dict["B_COLOR"] = color.b8 + text_dict["A_COLOR"] = color.a8 + text_dict["VALUE"] = text + text_dict["DURATION"] = _floating_text_manager.duration + + brotatogether_options.batched_floating_text.push_back(text_dict) + + +func spawn_explosion(explosion_dict : Dictionary) -> void: + var instance = explosion_scene.instance() + instance.is_ghost_explosion = true + call_deferred("add_explosion", instance) + + var explosion_scale = Vector2(explosion_dict["SCALE"],explosion_dict["SCALE"]) + instance.set_deferred("global_position", Vector2(explosion_dict["X_POS"], explosion_dict["Y_POS"])) + instance.set_deferred("scale", explosion_scale) + instance.call_deferred("start_explosion") + + +func add_node_to_pool(node: Node, id) -> void: + if node.has_meta("btg_ghost_enemy"): + if node.get_parent() != null: + node.get_parent().remove_child(node) + node.queue_free() + return + # id may be null for client-spawned projectiles that have no pool_id meta + if id != null and _pool.has(id): + .add_node_to_pool(node, id) + elif node.get_parent() != null: + node.get_parent().remove_child(node) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/main_ext.gd b/src/mods-unpacked/Brogether-Brogether/extensions/main_ext.gd deleted file mode 100644 index 454f332..0000000 --- a/src/mods-unpacked/Brogether-Brogether/extensions/main_ext.gd +++ /dev/null @@ -1,44 +0,0 @@ -extends "res://main.gd" - -var _brogether_snapshot_elapsed := 0.0 - - -func _ready() -> void: - ._ready() - var brogether = _get_brogether() - if brogether != null: - brogether.on_run_scene_ready(self) - - -func _physics_process(delta: float) -> void: - ._physics_process(delta) - _brogether_snapshot_elapsed += delta - if _brogether_snapshot_elapsed < 0.25: - return - _brogether_snapshot_elapsed = 0.0 - var brogether = _get_brogether() - if brogether != null: - brogether.on_run_snapshot_tick(_players) - - -func _on_EntitySpawner_players_spawned(players: Array) -> void: - ._on_EntitySpawner_players_spawned(players) - var brogether = _get_brogether() - if brogether != null: - brogether.on_run_players_spawned(players) - - -func _exit_tree() -> void: - var brogether = _get_brogether() - if brogether != null: - brogether.on_run_scene_exiting() - - -func _get_brogether(): - var root: Viewport = get_tree().get_root() - if root == null: - return null - var mod_loader: Node = root.get_node_or_null("ModLoader") - if mod_loader == null: - return null - return mod_loader.get_node_or_null("Brogether-Brogether") diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/main_menu_ext.gd b/src/mods-unpacked/Brogether-Brogether/extensions/main_menu_ext.gd deleted file mode 100644 index f9d6dd2..0000000 --- a/src/mods-unpacked/Brogether-Brogether/extensions/main_menu_ext.gd +++ /dev/null @@ -1,41 +0,0 @@ -extends "res://ui/menus/pages/main_menu.gd" - -var _brogether_button: Button = null - - -func setup_menu() -> void: - .setup_menu() - _add_brogether_button() - - -func _add_brogether_button() -> void: - if _brogether_button != null and is_instance_valid(_brogether_button): - return - if right_container == null: - return - _brogether_button = Button.new() - _brogether_button.name = "BrogetherButton" - _brogether_button.text = "BROGETHER" - _brogether_button.rect_min_size = mods_button.rect_min_size - _brogether_button.size_flags_horizontal = mods_button.size_flags_horizontal - _brogether_button.size_flags_vertical = mods_button.size_flags_vertical - _brogether_button.theme = mods_button.theme - _brogether_button.connect("pressed", self, "_on_BrogetherButton_pressed") - right_container.add_child(_brogether_button) - right_container.move_child(_brogether_button, right_container.get_child_count() - 1) - - -func _on_BrogetherButton_pressed() -> void: - var brogether = _get_brogether() - if brogether != null: - brogether.open_debug_panel() - - -func _get_brogether(): - var root: Viewport = get_tree().get_root() - if root == null: - return null - var mod_loader: Node = root.get_node_or_null("ModLoader") - if mod_loader == null: - return null - return mod_loader.get_node_or_null("Brogether-Brogether") diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/projectiles/player_explosion.gd b/src/mods-unpacked/Brogether-Brogether/extensions/projectiles/player_explosion.gd new file mode 100644 index 0000000..7a8c40e --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/projectiles/player_explosion.gd @@ -0,0 +1,18 @@ +extends "res://projectiles/player_explosion.gd" + +var is_ghost_explosion = false + +func start_explosion() -> void: + var steam_connection = $"/root/SteamConnection" + var brotatogether_options = $"/root/BrotogetherOptions" + if brotatogether_options.in_multiplayer_game and not is_ghost_explosion: + if steam_connection.is_host(): + var explosion_dict = { + "X_POS" : global_position.x, + "Y_POS" : global_position.y, + "SCALE" : scale.x, + } + brotatogether_options.batched_explosions.push_back(explosion_dict) + .start_explosion() + else: + .start_explosion() diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/singletons/coop_service.gd b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/coop_service.gd new file mode 100644 index 0000000..1f1434e --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/coop_service.gd @@ -0,0 +1,8 @@ +extends "res://singletons/coop_service.gd" + + +func is_player_using_gamepad(player_index) -> bool: + if get_remapped_player_device(player_index) >= 50: + return false + + return .is_player_using_gamepad(player_index) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/singletons/run_data.gd b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/run_data.gd new file mode 100644 index 0000000..b447ef3 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/run_data.gd @@ -0,0 +1,31 @@ +extends "res://singletons/run_data.gd" + +var steam_connection +var brotatogether_options + +var is_multiplayer_lobby = false + + +func init_multiplayer() -> void: + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + + steam_connection.leave_game_lobby() + + +func lock_player_shop_item(item_data: ItemParentData, wave_value: int, player_index: int)->void : + if brotatogether_options.in_multiplayer_game: + if steam_connection.is_host(): + .lock_player_shop_item(item_data, wave_value, player_index) + steam_connection.shop_lock_item(item_data.my_id, wave_value) + else: + .lock_player_shop_item(item_data, wave_value, player_index) + + +func unlock_player_shop_item(item_data: ItemParentData, player_index: int)->void : + if brotatogether_options.in_multiplayer_game: + if steam_connection.is_host(): + .unlock_player_shop_item(item_data, player_index) + steam_connection.shop_unlock_item(item_data.my_id) + else: + .unlock_player_shop_item(item_data, player_index) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/singletons/sound_manager.gd b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/sound_manager.gd new file mode 100644 index 0000000..00603e5 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/sound_manager.gd @@ -0,0 +1,22 @@ +extends "res://singletons/sound_manager.gd" + +var steam_connection +var brotatogether_options + +var is_multiplayer_lobby = false + + +func init_multiplayer() -> void: + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + + +func play(sound: Resource, volume_mod: float = 0.0, pitch_rand: float = 0.0, always_play: bool = false)->void : + if steam_connection and brotatogether_options: + if brotatogether_options.in_multiplayer_game: + if steam_connection.is_host(): + var sound_dict = { + "RESOURCE_PATH" : sound.resource_path, + } + brotatogether_options.batched_sounds.push_back(sound_dict) + .play(sound, volume_mod, pitch_rand, always_play) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/singletons/sound_manager_2d.gd b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/sound_manager_2d.gd new file mode 100644 index 0000000..ffedd5f --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/sound_manager_2d.gd @@ -0,0 +1,24 @@ +extends "res://singletons/sound_manager_2d.gd" + +var steam_connection +var brotatogether_options + +var is_multiplayer_lobby = false + + +func init_multiplayer() -> void: + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + + +func play(sound: Resource, pos: Vector2, volume_mod: float = 0.0, pitch_rand: float = 0.0, always_play: bool = false)->void : + if steam_connection and brotatogether_options: + if brotatogether_options.in_multiplayer_game: + if steam_connection.is_host(): + var sound_dict = { + "RESOURCE_PATH" : sound.resource_path, + "X_POS" : pos.x, + "Y_POS" : pos.y + } + brotatogether_options.batched_2d_sounds.push_back(sound_dict) + .play(sound, pos, volume_mod, pitch_rand, always_play) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/singletons/utils.gd b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/utils.gd new file mode 100644 index 0000000..19a2baa --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/singletons/utils.gd @@ -0,0 +1,8 @@ +extends "res://singletons/utils.gd" + + +func is_player_action_released(event: InputEvent, player_index: int, action: String)->bool: + if CoopService.get_remapped_player_device(player_index) >= 50: + return false + + return .is_player_action_released(event, player_index, action) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/global/focus_emulator.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/global/focus_emulator.gd new file mode 100644 index 0000000..4eb1525 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/global/focus_emulator.gd @@ -0,0 +1,46 @@ +extends "res://ui/menus/global/focus_emulator.gd" + +var global_focused_control +var steam_connection +var brotatogether_options + + +func ready() -> void: + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + + var _err = get_viewport().connect("gui_focus_changed", self, "_on_focus_changed_multiplayer") + + +func _on_focus_changed_multiplayer(control:Control) -> void: + if control != null: + global_focused_control = control + + +# Have line edit eat inputs so that you can send messages +func _handle_input(event:InputEvent) -> bool: + if not brotatogether_options: + brotatogether_options = $"/root/BrotogetherOptions" + + if not steam_connection: + steam_connection = $"/root/SteamConnection" + + if get_tree().current_scene.name == "CoopShop" and brotatogether_options.in_multiplayer_game: + if event is InputEventKey: + if event.pressed: + var carousel = get_tree().current_scene._get_coop_player_container(steam_connection.get_my_index()).carousel + if event.scancode == KEY_R: + carousel._on_ArrowRight_pressed() + if event.scancode == KEY_L: + carousel._on_ArrowLeft_pressed() + + if global_focused_control != null and global_focused_control is LineEdit: + return false + return ._handle_input(event) + + +func _get_focus_neighbour_for_event(event: InputEvent, target: Control)->GetFocusNeighbourForEventResult: + if _device >= 50: + return GetFocusNeighbourForEventResult.new() + + return ._get_focus_neighbour_for_event(event,target) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/ingame/coop_upgrades_ui_player_container.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/ingame/coop_upgrades_ui_player_container.gd new file mode 100644 index 0000000..5b9f47f --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/ingame/coop_upgrades_ui_player_container.gd @@ -0,0 +1,54 @@ +extends "res://ui/menus/ingame/coop_upgrades_ui_player_container.gd" + +var steam_connection +var brotatogether_options +var in_multiplayer_game = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + +func _on_RerollButton_pressed() -> void: + if in_multiplayer_game: + if steam_connection.is_host(): + ._on_RerollButton_pressed() + else: + steam_connection.send_main_scene_client_reroll_button_ressed() + else: + ._on_RerollButton_pressed() + + +func _on_choose_button_pressed(upgrade: UpgradeData) -> void: + if in_multiplayer_game: + if steam_connection.is_host(): + ._on_choose_button_pressed(upgrade) + else: + var upgrade_dict = { + "UPGRADE_ID" : upgrade.my_id + } + steam_connection.send_main_scene_client_choose_upgrade_pressed(upgrade_dict) + else: + ._on_choose_button_pressed(upgrade) + + +func _on_TakeButton_pressed(): + if in_multiplayer_game: + if steam_connection.is_host(): + ._on_TakeButton_pressed() + else: + steam_connection.send_main_scene_client_take_button_pressed() + else: + ._on_TakeButton_pressed() + + +func _on_DiscardButton_pressed(): + if in_multiplayer_game: + if steam_connection.is_host(): + ._on_DiscardButton_pressed() + else: + steam_connection.send_main_scene_client_discard_button_pressed() + else: + ._on_DiscardButton_pressed() diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/ingame/pause_menu.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/ingame/pause_menu.gd new file mode 100644 index 0000000..bd5b6a5 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/ingame/pause_menu.gd @@ -0,0 +1,18 @@ +extends "res://ui/menus/ingame/pause_menu.gd" + +var steam_connection +var brotatogether_options + +var is_multiplayer_lobby = false + + +# Called when the node enters the scene tree for the first time. +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + + +func on_game_lost_focus()->void : + if steam_connection.game_lobby_id > 0: + return + .on_game_lost_focus() diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/pages/main_menu.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/pages/main_menu.gd new file mode 100644 index 0000000..cc362b2 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/pages/main_menu.gd @@ -0,0 +1,45 @@ +extends "res://ui/menus/pages/main_menu.gd" + + +var multiplayer_button: Button + +# Add a multiplayer button to the main menu +func _ready(): + RunData.init_multiplayer() + SoundManager.init_multiplayer() + SoundManager2D.init_multiplayer() + + var buttons_node = $"MarginContainer/VBoxContainer/HBoxContainer/ButtonsLeft" + + # Duplicate a Button to get the styling + multiplayer_button = start_button.duplicate() + multiplayer_button.text = "Multiplayer" + multiplayer_button.name = "MultiplayerButton" + + var _unused = multiplayer_button.connect("pressed", self, "_on_MultiplayerButton_pressed") + multiplayer_button.disconnect("pressed", self, "_on_StartButton_pressed") + + buttons_node.add_child_below_node(buttons_node.get_children()[0], multiplayer_button) + buttons_node.move_child(multiplayer_button, 0) + + remove_game_controller() + + $"/root/BrotogetherOptions".joining_multiplayer_lobby = false + $"/root/BrotogetherOptions".is_solo_test = false + + +func init() -> void: + .init() + if continue_button.visible: + continue_button.focus_neighbour_top = multiplayer_button.get_path() + else: + start_button.focus_neighbour_top = multiplayer_button.get_path() + + +func _on_MultiplayerButton_pressed(): + var _error = get_tree().change_scene("res://mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.tscn") + +func remove_game_controller(): + if $"/root".has_node("GameController"): + var game_controller = $"/root/GameController" + $"/root".remove_child(game_controller) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/character_selection.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/character_selection.gd new file mode 100644 index 0000000..039a146 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/character_selection.gd @@ -0,0 +1,342 @@ +extends "res://ui/menus/run/character_selection.gd" + +const ChatPanel = preload("res://mods-unpacked/Brogether-Brogether/ui/chat_panel.tscn") +const ChatMessage = preload("res://mods-unpacked/Brogether-Brogether/ui/chat/chat_message.tscn") +const UsernameLabel = preload("res://mods-unpacked/Brogether-Brogether/ui/username_label.tscn") + +const MULTIPLAYER_CLIENT_PLAYER_TYPE = 10 + +var steam_connection +var brotatogether_options + +var is_multiplayer_lobby = false + +var global_chat_panel +var global_chat_messsages +var global_chat_input + +var lobby_chat_panel +var lobby_chat_messsages +var lobby_chat_input + +var panel_parent_container + +var panels_array +var visible_panel_index : int = 0 + +# Mapped inventory item by item keys. Used to reset focus based on external +# calls. +var inventory_by_string_key : Dictionary + +# Mapped Selections by item keys. Kept independent of itnentory items to +# avoid complications around randoms, locked items, etc. +var selections_by_string_key : Dictionary + +var username_labels = [] +var external_focus = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + steam_connection.connect("global_chat_received", self, "_received_global_chat") + steam_connection.connect("game_lobby_chat_received", self, "_received_lobby_chat") + steam_connection.connect("player_focused_character", self, "_player_focused_character") + steam_connection.connect("player_selected_character", self, "_player_selected_character") + steam_connection.connect("character_lobby_update", self, "_lobby_characters_updated") + steam_connection.connect("request_character_lobby_update", self, "_character_lobby_update_requested") + steam_connection.connect("character_selection_complete", self, "_host_character_selection_complete") + + # TODO gracefully add new players + steam_connection.connect("lobby_players_updated", self, "reload_scene") + + brotatogether_options = $"/root/BrotogetherOptions" + is_multiplayer_lobby = brotatogether_options.joining_multiplayer_lobby + + if is_multiplayer_lobby: + RunData.is_coop_run = true + _coop_button.init() + _coop_button.hide() + var run_options_top_panel = _run_options_panel.get_node("MarginContainer/VBoxContainer/HBoxContainer") + run_options_top_panel.remove_child(run_options_top_panel.get_node("Icon")) + + # Add left and right buttons to the heading + var run_options_left_arrow : TextureButton = TextureButton.new() + run_options_left_arrow.texture_normal = load("res://ui/menus/global/arrow_left.png") + run_options_top_panel.add_child(run_options_left_arrow) + run_options_top_panel.move_child(run_options_left_arrow, 0) + var _err = run_options_left_arrow.connect("pressed", self, "_on_pressed_left_menu_arrow") + + var run_options_right_arrow : TextureButton = TextureButton.new() + run_options_right_arrow.texture_normal = load("res://ui/menus/global/arrow_right.png") + run_options_top_panel.add_child(run_options_right_arrow) + _err = run_options_right_arrow.connect("pressed", self, "_on_pressed_right_menu_arrow") + + panel_parent_container = _run_options_panel.get_parent() + + var username_label_player_1 : Label = UsernameLabel.instance() + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel1/vboxContainer".add_child(username_label_player_1) + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel1/vboxContainer".move_child(username_label_player_1, 0) + username_labels.push_back(username_label_player_1) + + var username_label_player_2 : Label = UsernameLabel.instance() + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel2/vboxContainer".add_child(username_label_player_2) + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel2/vboxContainer".move_child(username_label_player_2, 0) + username_labels.push_back(username_label_player_2) + + var username_label_player_3 : Label = UsernameLabel.instance() + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel3/vboxContainer".add_child(username_label_player_3) + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel3/vboxContainer".move_child(username_label_player_3, 0) + username_labels.push_back(username_label_player_3) + + var username_label_player_4 : Label = UsernameLabel.instance() + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel4/vboxContainer".add_child(username_label_player_4) + $"MarginContainer/VBoxContainer/DescriptionContainer/HBoxContainer/Panel4/vboxContainer".move_child(username_label_player_4, 0) + username_labels.push_back(username_label_player_4) + + for index in steam_connection.lobby_member_names.size(): + username_labels[index].text = steam_connection.lobby_member_names[index] + + global_chat_panel = ChatPanel.instance() + global_chat_panel.get_node("MarginContainer/VBoxContainer/HBoxContainer/ChatTitle").text = "Global Chat" + var global_chat_left_arrow = global_chat_panel.get_node("MarginContainer/VBoxContainer/HBoxContainer/LeftArrow") + _err = global_chat_left_arrow.connect("pressed", self, "_on_pressed_left_menu_arrow") + var global_chat_right_arrow = global_chat_panel.get_node("MarginContainer/VBoxContainer/HBoxContainer/RightArrow") + _err = global_chat_right_arrow.connect("pressed", self, "_on_pressed_right_menu_arrow") + panel_parent_container.add_child_below_node(_run_options_panel, global_chat_panel) + + global_chat_messsages = global_chat_panel.get_node("MarginContainer/VBoxContainer/ScrollContainer/ChatMessages") + global_chat_input = global_chat_panel.get_node("MarginContainer/VBoxContainer/ChatInput") + global_chat_input.connect("text_entered", self, "_on_global_chat_text_entered") + + lobby_chat_panel = ChatPanel.instance() + lobby_chat_panel.get_node("MarginContainer/VBoxContainer/HBoxContainer/ChatTitle").text = "Lobby Chat" + var lobby_chat_left_arrow = lobby_chat_panel.get_node("MarginContainer/VBoxContainer/HBoxContainer/LeftArrow") + _err = lobby_chat_left_arrow.connect("pressed", self, "_on_pressed_left_menu_arrow") + var lobby_chat_right_arrow = lobby_chat_panel.get_node("MarginContainer/VBoxContainer/HBoxContainer/RightArrow") + _err = lobby_chat_right_arrow.connect("pressed", self, "_on_pressed_right_menu_arrow") + panel_parent_container.add_child_below_node(_run_options_panel, lobby_chat_panel) + + lobby_chat_messsages = lobby_chat_panel.get_node("MarginContainer/VBoxContainer/ScrollContainer/ChatMessages") + lobby_chat_input = lobby_chat_panel.get_node("MarginContainer/VBoxContainer/ChatInput") + lobby_chat_input.connect("text_entered", self, "_on_lobby_chat_text_entered") + + panels_array = [_run_options_panel, global_chat_panel, lobby_chat_panel] + update_panel_visiblility() + + for member_index in steam_connection.lobby_members.size(): + var member_id = steam_connection.lobby_members[member_index] + if member_id == steam_connection.steam_id: + CoopService._add_player(0, MULTIPLAYER_CLIENT_PLAYER_TYPE) + else: + CoopService._add_player(100 + member_index, MULTIPLAYER_CLIENT_PLAYER_TYPE) + + for character_data in _get_all_possible_elements(0): + selections_by_string_key[character_item_to_string(character_data)] = character_data + + # Find the random element in the inventory + for character_data in _get_inventories()[0].get_children(): + if character_data.is_random: + inventory_by_string_key[character_item_to_string(character_data)] = character_data + selections_by_string_key[character_item_to_string(character_data)] = character_data + else: + inventory_by_string_key[character_item_to_string(character_data.item)] = character_data + + for panel in _get_panels(): + panel._small(true) + + +func _on_pressed_left_menu_arrow() -> void: + visible_panel_index = (visible_panel_index - 1 + panels_array.size()) % panels_array.size() + update_panel_visiblility() + + +func _on_pressed_right_menu_arrow() -> void: + visible_panel_index = (visible_panel_index + 1) % panels_array.size() + update_panel_visiblility() + + +func update_panel_visiblility() -> void: + for index in panels_array.size(): + if index == visible_panel_index: + panels_array[index].visible = true + else: + panels_array[index].visible = false + + +func _received_global_chat(user, message) -> void: + var new_message_node = ChatMessage.instance() + new_message_node.message = message + new_message_node.username = user + global_chat_messsages.add_child(new_message_node) + + +func _received_lobby_chat(user, message) -> void: + var new_message_node = ChatMessage.instance() + new_message_node.message = message + new_message_node.username = user + lobby_chat_messsages.add_child(new_message_node) + + +func _on_global_chat_text_entered(message): + steam_connection.send_global_chat_message(message) + global_chat_input.clear() + + +func _on_lobby_chat_text_entered(message): + steam_connection.send_lobby_chat_message(message) + lobby_chat_input.clear() + + +func _on_element_focused(element:InventoryElement, inventory_player_index:int, _displayPanelData: bool = true) -> void: + ._on_element_focused(element, inventory_player_index) + + if is_multiplayer_lobby: + var element_string = "" + if element.item != null: + element_string = element.item.name + elif element.is_random: + element_string = "RANDOM" + if not external_focus: + steam_connection.character_focused(element_string) + else: + external_focus = false + + +func character_item_to_string(item : Resource) -> String: + if item == null: + return "RANDOM" + return item.name + + +func _player_focused_character(player_index : int , character : String) -> void: + var selected_item = null + var focused_element = null + + if selections_by_string_key.has(character): + selected_item = selections_by_string_key[character] + + if inventory_by_string_key.has(character): + focused_element = inventory_by_string_key[character] + + _clear_selected_element(player_index) + _player_characters[player_index] = selected_item + if player_index < RunData.get_player_count() and player_index != steam_connection.get_my_index(): + if focused_element != null: + if Utils.get_focus_emulator(player_index).focused_control != focused_element: + external_focus = true + Utils.get_focus_emulator(player_index).focused_control = focused_element + + var panel = _get_panels()[player_index] + + if panel.visible: + if selected_item != null: + if character == "RANDOM": + panel.set_custom_data("RANDOM", selected_item.get_inventory_icon()) + else: + panel.set_data(selected_item, player_index) + + +func _player_selected_character(player_index : int) -> void: + _set_selected_element(player_index) + + +func reload_scene() -> void: + $"/root/BrotogetherOptions".joining_multiplayer_lobby = true + var _error = get_tree().change_scene(MenuData.character_selection_scene) + + +func _lobby_characters_updated(player_characters : Array, has_player_selected : Array) -> void: + for player_index in RunData.get_player_count(): + if player_characters[player_index] != null: + _player_focused_character(player_index, player_characters[player_index]) + + var all_selected = true + for player_index in RunData.get_player_count(): + if has_player_selected[player_index]: + _set_selected_element(player_index) + else: + all_selected = false + _clear_selected_element(player_index) + + if all_selected and steam_connection.is_host(): + _selections_completed_timer.start() + + +func _set_selected_element(player_index:int) -> void: + if _has_player_selected[player_index]: + return + + ._set_selected_element(player_index) + + if brotatogether_options.is_solo_test: + var all_selected = true + for i in RunData.get_player_count(): + if not _has_player_selected[i]: + all_selected = false + break + if all_selected: + CoopService.listening_for_inputs = false + _selections_completed_timer.start() + + if steam_connection.get_lobby_index_for_player(steam_connection.steam_id) == player_index: + steam_connection.character_selected() + + +func _character_lobby_update_requested() -> void: + var currently_focused_characters = [] + for panel in _get_panels(): + var selected_item = panel.item_data + if selected_item == null: + currently_focused_characters.push_back("RANDOM") + else: + currently_focused_characters.push_back(character_item_to_string(selected_item)) + + steam_connection.send_character_lobby_update(currently_focused_characters, _has_player_selected) + + +func _on_selections_completed() -> void: + if is_multiplayer_lobby: + if not steam_connection.is_host(): + return + else: + ._on_selections_completed() + return + + var currently_focused_characters = [] + # Sad dupe of inner logic to make things simpler + for player_index in RunData.get_player_count(): + var chosen_item = _get_panels()[player_index] + + var character = _player_characters[player_index] + if chosen_item.item_data == null: + var available_elements: = [] + for element in ItemService.characters: + if not element.is_locked: + available_elements.push_back(element) + character = Utils.get_rand_element(available_elements) + _player_characters[player_index] = character + + RunData.add_character(character, player_index) + currently_focused_characters.push_back(character_item_to_string(character)) + + steam_connection.send_character_selection_completed(RunData.some_player_has_weapon_slots(), currently_focused_characters) + if RunData.some_player_has_weapon_slots(): + _change_scene(MenuData.weapon_selection_scene) + else: + _change_scene(MenuData.difficulty_selection_scene) + + +func _host_character_selection_complete(some_player_has_weapon_slots : bool, selected_characters : Array) -> void: + # Sad dupe of inner logic to make things simpler + for player_index in selected_characters.size(): + if selected_characters[player_index] != null: + _player_focused_character(player_index, selected_characters[player_index]) + + for player_index in RunData.get_player_count(): + var character = _player_characters[player_index] + RunData.add_character(character, player_index) + + if some_player_has_weapon_slots: + _change_scene(MenuData.weapon_selection_scene) + else: + _change_scene(MenuData.difficulty_selection_scene) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/difficulty_selection/difficulty_selection.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/difficulty_selection/difficulty_selection.gd new file mode 100644 index 0000000..1f550ce --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/difficulty_selection/difficulty_selection.gd @@ -0,0 +1,73 @@ +extends "res://ui/menus/run/difficulty_selection/difficulty_selection.gd" + +var steam_connection +var brotatogether_options +var is_multiplayer_lobby = false + +var inventory_by_string_key : Dictionary +var selection_by_string_key : Dictionary + + +func _ready(): + steam_connection = $"/root/SteamConnection" + steam_connection.connect("difficulty_focused", self, "_difficulty_focused") + steam_connection.connect("difficulty_selected", self, "_difficulty_selected") + + brotatogether_options = $"/root/BrotogetherOptions" + is_multiplayer_lobby = brotatogether_options.joining_multiplayer_lobby + + if is_multiplayer_lobby: + brotatogether_options.joining_multiplayer_lobby = false + brotatogether_options.in_multiplayer_game = true + + for difficulty_data in ItemService.difficulties: + selection_by_string_key[difficulty_data.value] = difficulty_data + + for inventory_item in _get_inventories()[0].get_children(): + inventory_by_string_key[inventory_item.item.value] = inventory_item + + +func _on_element_focused(element:InventoryElement, inventory_player_index:int, _displayPanelData: bool = true) -> void: + # Disregard difficulty updates from clients + if is_multiplayer_lobby: + if not steam_connection.is_host(): + return + + ._on_element_focused(element, inventory_player_index) + + if is_multiplayer_lobby: + steam_connection.difficulty_focused() + + +func _on_element_pressed(element: InventoryElement, _inventory_player_index: int) -> void: + # Disregard difficulty updates from clients + if is_multiplayer_lobby: + if not steam_connection.is_host(): + return + + ._on_element_pressed(element, _inventory_player_index) + + if is_multiplayer_lobby: + steam_connection.difficulty_pressed() + + +func _difficulty_selected(_difficutly : int) -> void: + var _error = get_tree().change_scene(MenuData.game_scene) + + +func _difficulty_focused(difficutly : int) -> void: + + # Hosts don't respect update calls + if is_multiplayer_lobby: + if steam_connection.is_host(): + return + + var selected_item = null + if selection_by_string_key.has(difficutly): + selected_item = selection_by_string_key[difficutly] + + Utils.get_focus_emulator(0).focused_control = inventory_by_string_key[difficutly] + + if selected_item != null: + _get_panels()[0].visible = true + _get_panels()[0].set_data(selected_item, 0) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/weapon_selection.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/weapon_selection.gd new file mode 100644 index 0000000..580291b --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/run/weapon_selection.gd @@ -0,0 +1,172 @@ +extends "res://ui/menus/run/weapon_selection.gd" + +var steam_connection +var brotatogether_options +var is_multiplayer_lobby = false + +var selections_by_string_key : Dictionary +var external_focus = false + + +var inventory_maps : Array + +func _ready(): + steam_connection = $"/root/SteamConnection" + + steam_connection.connect("player_focused_weapon", self, "_player_focused_weapon") + steam_connection.connect("player_selected_weapon", self, "_player_selected_weapon") + steam_connection.connect("weapon_lobby_update", self, "_lobby_weapons_updated") + steam_connection.connect("weapon_selection_completed", self, "_weapon_selection_completed") + + brotatogether_options = $"/root/BrotogetherOptions" + is_multiplayer_lobby = brotatogether_options.joining_multiplayer_lobby + + if is_multiplayer_lobby: + for weapon_data in ItemService.weapons: + selections_by_string_key[weapon_item_to_string(weapon_data)] = weapon_data + + inventory_maps = [] + for player_index in RunData.get_player_count(): + var inventory_map = {} + for inventory_item in _get_inventories()[player_index].get_children(): + inventory_map[weapon_item_to_string(inventory_item.item)] = inventory_item + inventory_maps.push_back(inventory_map) + + +func weapon_item_to_string(item : Resource) -> String: + if item == null: + return "RANDOM" + return item.my_id + + +func _lobby_weapons_updated(player_weapons : Array, has_player_selected : Array) -> void: + for player_index in player_weapons.size(): + if player_weapons[player_index] != null: + _player_focused_weapon(player_index, player_weapons[player_index]) + + var all_selected = true + for player_index in RunData.get_player_count(): + if has_player_selected[player_index]: + _set_selected_element(player_index) + else: + all_selected = false + _clear_selected_element(player_index) + + if all_selected: + _selections_completed_timer.start() + + +func _on_element_focused(element:InventoryElement, inventory_player_index:int, _displayPanelData: bool = true) -> void: + ._on_element_focused(element, inventory_player_index) + + if is_multiplayer_lobby: + var element_string = "" + if element.item != null: + element_string = element.item.my_id + elif element.is_random: + element_string = "RANDOM" + + if not external_focus: + steam_connection.weapon_focused(element_string) + else: + external_focus = false + + +func _player_focused_weapon(player_index : int , weapon : String) -> void: + var selected_item = null + if selections_by_string_key.has(weapon): + selected_item = selections_by_string_key[weapon] + + var focused_weapon = null + if inventory_maps[player_index].has(weapon): + focused_weapon = inventory_maps[player_index][weapon] + + if focused_weapon != null: + if Utils.get_focus_emulator(player_index).focused_control != focused_weapon: + external_focus = true + Utils.get_focus_emulator(player_index).focused_control = focused_weapon + + _player_weapons[player_index] = selected_item + _clear_selected_element(player_index) + + var panel = _get_panels()[player_index] + if panel.visible: + # TODO handle randoms + if selected_item != null: + panel.set_data(selected_item, player_index) + + +func _player_selected_weapon(player_index : int) -> void: + _set_selected_element(player_index) + + +func _set_selected_element(p_player_index:int) -> void: + if _has_player_selected[p_player_index]: + return + + # This happens during the base _ready for characters with no weapon slots. + if not steam_connection: + _has_player_selected[p_player_index] = true + return + + ._set_selected_element(p_player_index) + + if brotatogether_options.is_solo_test: + var all_selected = true + for i in RunData.get_player_count(): + if not _has_player_selected[i]: + all_selected = false + break + if all_selected: + CoopService.listening_for_inputs = false + _selections_completed_timer.start() + + if steam_connection.get_lobby_index_for_player(steam_connection.steam_id) == p_player_index: + steam_connection.weapon_selected() + + +func _on_selections_completed() -> void: + if is_multiplayer_lobby: + if not steam_connection.is_host(): + return + else: + ._on_selections_completed() + return + + + for player_index in RunData.get_player_count(): + var chosen_item = _get_panels()[player_index] + var weapon = _player_weapons[player_index] + + if chosen_item.item_data == null or weapon == null: + var available_elements: = [] + for element in displayed_elements[player_index]: + if not element.is_locked: + available_elements.push_back(element) + weapon = Utils.get_rand_element(available_elements) + _player_weapons[player_index] = weapon + + if weapon: + var _weapon = RunData.add_weapon(weapon, player_index, true) + + RunData.add_starting_items_and_weapons() + + var all_selected_weapons = [] + for player_index in RunData.get_player_count(): + var selected_weapons = [] + for owned_weapon in RunData.players_data[player_index].weapons: + selected_weapons.push_back(weapon_item_to_string(owned_weapon)) + all_selected_weapons.push_back(selected_weapons) + + steam_connection.send_weapon_selection_completed(all_selected_weapons) + + _change_scene(MenuData.difficulty_selection_scene) + + +func _weapon_selection_completed(selected_weapons : Array) -> void: + for player_index in RunData.get_player_count(): + RunData.players_data[player_index].weapons.clear() + for weapon_index in selected_weapons[player_index].size(): + RunData.players_data[player_index].weapons.push_back(selections_by_string_key[selected_weapons[player_index][weapon_index]]) + + _change_scene(MenuData.difficulty_selection_scene) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/coop_shop.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/coop_shop.gd new file mode 100644 index 0000000..a34db69 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/coop_shop.gd @@ -0,0 +1,643 @@ +extends "res://ui/menus/shop/coop_shop.gd" + + +var steam_connection +var brotatogether_options + +var in_multiplayer_game = false + +# If true, the steam logic will be skipped to avoid duplicate rpc chains. +var is_self_call = false + +var player_in_scene = [true, false, false, false] +var waiting_to_start_shop = false + +var focusing_reroll_button = false +var focusing_go_button = false + +# Force clients to update their focus, this syncs up focus after inventory +# changes (buys, discards, etc) and prevents the client focus from disappearing +var pending_force_focus = [] + + +func _ready(): + steam_connection = $"/root/SteamConnection" + + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + if in_multiplayer_game: + waiting_to_start_shop = true + + steam_connection.connect("client_status_received", self, "_client_status_received") + steam_connection.connect("shop_lobby_update", self, "_update_shop") + steam_connection.connect("client_shop_focus_updated", self, "_client_shop_focus_updated") + steam_connection.connect("client_shop_go_button_pressed", self, "_client_shop_go_button_pressed") + steam_connection.connect("client_shop_discard_weapon", self, "_client_shop_discard_weapon") + steam_connection.connect("client_shop_combine_weapon", self, "_client_shop_combine_weapon") + steam_connection.connect("client_shop_buy_item", self, "_client_shop_buy_item") + steam_connection.connect("client_shop_reroll", self, "_client_shop_reroll") + steam_connection.connect("client_shop_lock_item", self, "_client_shop_lock_item") + steam_connection.connect("client_shop_unlock_item", self, "_client_shop_unlock_item") + steam_connection.connect("client_shop_focus_inventory_element", self, "_client_focused_inventory_element") + steam_connection.connect("client_shop_requested", self, "send_shop_state") + steam_connection.connect("close_popup", self, "_remote_close_client_popup") + steam_connection.connect("receive_leave_shop", self, "_receive_leave_shop") + + if steam_connection.is_host(): + steam_connection.send_host_entered_shop() + + +func _process(_delta): + if in_multiplayer_game: + if waiting_to_start_shop: + if steam_connection.is_host(): + var all_players_entered = true + for player_index in RunData.get_player_count(): + if not player_in_scene[player_index]: + all_players_entered = false + break + if all_players_entered: + waiting_to_start_shop = false + + # Initial shop state, all players can break focus + send_shop_state([0,1,2,3]) + _check_for_focus_change() + + +func _client_status_received(client_data : Dictionary, player_index : int) -> void: + if waiting_to_start_shop: + if client_data["CURRENT_SCENE"] == get_tree().current_scene.name: + player_in_scene[player_index] = true + + +func _on_element_focused(element: InventoryElement, player_index: int)->void : + ._on_element_focused(element,player_index) + + if in_multiplayer_game: + if is_self_call: + is_self_call = false + else: + var focus_string = _dictionary_for_focus_inventory_element(element, player_index) + steam_connection.shop_focus_inventory_element(focus_string) + + +func _client_shop_focus_updated(shop_item_string : String, player_index : int) -> void: + is_self_call = true + + var shop_item : ShopItem = _shop_item_for_string(shop_item_string, player_index) + if shop_item: + Utils.get_focus_emulator(player_index).focused_control = shop_item._button + + +func _on_shop_item_focused(shop_item: ShopItem, player_index : int) -> void: + if in_multiplayer_game: + var is_me = player_index == steam_connection.get_my_index() + if waiting_to_start_shop and not is_me: + return + ._on_shop_item_focused(shop_item, player_index) + if is_self_call: + is_self_call = false + elif is_me or steam_connection.is_host(): + var players_to_force = pending_force_focus.duplicate() + pending_force_focus.clear() + steam_connection.shop_item_focused(_string_for_shop_item(shop_item), players_to_force) + else: + ._on_shop_item_focused(shop_item, player_index) + + +func _on_item_discard_button_pressed(weapon_data: WeaponData, player_index: int)->void : + if in_multiplayer_game: + if steam_connection.is_host(): + ._on_item_discard_button_pressed(weapon_data, player_index) + pending_force_focus.push_back(player_index) + steam_connection.shop_weapon_discard(_string_for_weapon(weapon_data), player_index) + else: + ._on_item_discard_button_pressed(weapon_data, player_index) + + +func _client_shop_discard_weapon(weapon_string : String, player_index : int) -> void: + _on_item_discard_button_pressed(_weapon_for_string(weapon_string, player_index), player_index) + steam_connection.request_close_client_shop_popup(player_index) + + +func _remote_close_client_popup() -> void: + _get_coop_player_container(steam_connection.get_my_index()).item_popup.cancel() + + +func _on_GoButton_pressed(player_index: int) -> void: + if in_multiplayer_game: + if steam_connection.is_host(): + _on_go_button_pressed_brotatogether(player_index) + steam_connection.shop_go_button_pressed(not _player_pressed_go_button[player_index]) + else: + ._on_GoButton_pressed(player_index) + + +func _on_go_button_pressed_brotatogether(player_index: int)->void : + if get_tree().paused: + return + + if _player_pressed_go_button[player_index]: + _clear_go_button_pressed(player_index) + return + + _player_pressed_go_button[player_index] = true + var checkmark = _get_checkmark(player_index) + if checkmark != null: + checkmark.show() + + for other_player_index in RunData.get_player_count(): + if not _player_pressed_go_button[other_player_index]: + return + + RunData.current_wave += 1 + steam_connection.leave_shop() + var _error = get_tree().change_scene(MenuData.game_scene) + + +func _receive_leave_shop() -> void: + RunData.current_wave += 1 + var _error = get_tree().change_scene(MenuData.game_scene) + + +func _clear_go_button_pressed(player_index: int) -> void : + if in_multiplayer_game: + if steam_connection.is_host(): + ._clear_go_button_pressed(player_index) + steam_connection.shop_go_button_pressed(false) + else: + ._clear_go_button_pressed(player_index) + + +func _client_shop_go_button_pressed(latest_go_state : bool, player_index : int) -> void: + # Make the go function change the state to the new state by setting to the + # opposite + _player_pressed_go_button[player_index] = not latest_go_state + _on_GoButton_pressed(player_index) + + +func on_shop_item_bought(shop_item: ShopItem, player_index: int) -> void: + if in_multiplayer_game: + if steam_connection.is_host(): + .on_shop_item_bought(shop_item, player_index) + pending_force_focus.push_back(player_index) + steam_connection.shop_buy_item(_string_for_shop_item(shop_item), player_index) + else: + .on_shop_item_bought(shop_item, player_index) + + +func _client_shop_buy_item(item_string : String, player_index : int) -> void: + _get_shop_items_container(player_index).on_shop_item_buy_button_pressed(_shop_item_for_string(item_string, player_index)) + + +func _on_item_combine_button_pressed(weapon_data: WeaponData, player_index: int)->void : + if in_multiplayer_game: + if steam_connection.is_host(): + pending_force_focus.push_back(player_index) + ._on_item_combine_button_pressed(weapon_data, player_index) + steam_connection.shop_combine_weapon(_string_for_weapon(weapon_data), false, player_index) + else: + ._on_item_combine_button_pressed(weapon_data, player_index) + + +func _client_shop_combine_weapon(weapon_string : String, _is_upgrade: bool, player_index : int) -> void: + _on_item_combine_button_pressed(_weapon_for_string(weapon_string, player_index), player_index) + steam_connection.request_close_client_shop_popup(player_index) + + +func _client_shop_lock_item(item_string : String, _wave_value: int, player_index: int) -> void: + _shop_item_for_string(item_string, player_index).change_lock_status(true) + + +func _client_shop_unlock_item(item_string : String, player_index : int) -> void: + _shop_item_for_string(item_string, player_index).change_lock_status(false) + + +func send_shop_state(changed_shop_player_indeces : Array = []) -> void: + var result_dict : Dictionary = {} + var abyssal_dlc = ProgressData.get_dlc_data("abyssal_terrors") + + var players_array = [] + var player_count: int = RunData.get_player_count() + for player_index in player_count: + var player_data = RunData.players_data[player_index] + var player_dict = {} + var shop_items = [] + for item in _shop_items[player_index]: + var item_dict : Dictionary = {} + + item_dict["ID"] = item[0].my_id + item_dict["WAVE_VALUE"] = item[1] + + if abyssal_dlc: + if item[0].is_cursed: + item_dict["CURSED"] = true + + shop_items.push_back(item_dict) + player_dict["SHOP_ITEMS"] = shop_items + + player_dict["EFFECTS"] = player_data._serialize_effects(player_data.effects) + var active_sets = {} + for set in player_data.active_sets: + active_sets[set] = player_data.active_sets[set] + player_dict["ACTIVE_SETS"] = active_sets + + var locked_items : Array = [] + for item in RunData.locked_shop_items[player_index]: + var item_dict : Dictionary = {} + item_dict["ID"] = item[0].my_id + item_dict["WAVE_VALUE"] = item[1] + locked_items.push_back(item_dict) + player_dict["LOCKED_ITEMS"] = locked_items + + var weapons_array = [] + for weapon in RunData.get_player_weapons(player_index): + weapons_array.push_back(_dictionary_for_weapon(weapon)) + player_dict["WEAPONS"] = weapons_array + player_dict["GO_PRESSED"] = _player_pressed_go_button[player_index] + + var gear_container : PlayerGearContainer = _get_gear_container(player_index) + var items_array = [] + for element in gear_container.items_container._elements.get_children(): + items_array.push_back(_dictionary_for_inventory_item(element)) +# print_debug("shop container has ", element.item.my_id) + player_dict["ITEMS"] = items_array + + player_dict["GOLD"] = RunData.get_player_gold(player_index) + player_dict["FOCUS"] = _focus_dictionary_for_player(player_index) + + player_dict["REROLL_PRICE"] = _reroll_price[player_index] + player_dict["REROLL_DICSOUNT"] = _reroll_discount[player_index] + player_dict["HAS_BONUS_FREE_REROLL"] = _has_bonus_free_reroll[player_index] + + + players_array.push_back(player_dict) + + result_dict["PLAYERS"] = players_array + result_dict["CHANGED_PLAYERS"] = changed_shop_player_indeces + +# print_debug("sending shop state with dict ", result_dict) + steam_connection.send_shop_update(result_dict) + + +func _update_shop(shop_dictionary : Dictionary) -> void: + if not shop_dictionary.has("PLAYERS"): + print("WARNING - received shop update without PLAYERS element, ignoring; data:", shop_dictionary) + return + + if steam_connection.is_host(): + print("WARNING - host shouldn't be updating for remote, returning; data:", shop_dictionary) + return + +# print_debug("updating shop for dictionary - ", shop_dictionary) + + for player_index in shop_dictionary["PLAYERS"].size(): + var player_dict = shop_dictionary["PLAYERS"][player_index] + + var is_me = steam_connection.get_my_index() == player_index + var can_update = not is_me or shop_dictionary["CHANGED_PLAYERS"].has(player_index) + + var go_pressed = player_dict["GO_PRESSED"] + _player_pressed_go_button[player_index] = go_pressed + var checkmark = _get_checkmark(player_index) + if checkmark != null: + if go_pressed: + checkmark.show() + else: + checkmark.hide() + + if not can_update: +# print_debug("skipping update for player ", player_index) + continue + + Utils.get_focus_emulator(player_index)._clear_focused_control() + _shop_items[player_index].clear() + Utils.reset_stat_cache(player_index) + var player_data = RunData.players_data[player_index] + + var reserilaized_effects = {} + for key in player_dict["EFFECTS"]: + reserilaized_effects[str(key)] = player_dict["EFFECTS"][key] + player_data.effects = player_data._deserialize_effects(reserilaized_effects, {}) + + player_data.active_sets.clear() + var active_sets_dict = player_dict["ACTIVE_SETS"] + for active_set in player_dict["ACTIVE_SETS"]: + player_data.active_sets[active_set] = active_sets_dict[active_set] + + for shop_item_dict in player_dict["SHOP_ITEMS"]: + _shop_items[player_index].push_back(_shop_item_for_dictionary(shop_item_dict)) + _get_shop_items_container(player_index).set_shop_items(_shop_items[player_index]) + + var player_gear_container = _get_gear_container(player_index) + + RunData.players_data[player_index].weapons.clear() + for weapon in player_dict["WEAPONS"]: + RunData.players_data[player_index].weapons.push_back(_weapon_for_dictionary(weapon)) + player_gear_container.set_weapons_data(RunData.players_data[player_index].weapons) + + RunData.players_data[player_index].items.clear() + for item in player_dict["ITEMS"]: + for _i in item["NUMBER"]: + RunData.players_data[player_index].items.push_back(_inventory_item_for_dictionary(item)) + player_gear_container.set_items_data(RunData.players_data[player_index].items) + + + if player_dict.has("FOCUS"): + _set_client_focus_for_player(player_dict["FOCUS"], player_index) + else: + print_debug("WARN - missing focus for player ", player_index, " ", player_dict) + + _reroll_price[player_index] = player_dict["REROLL_PRICE"] + _reroll_discount[player_index] = player_dict["REROLL_DICSOUNT"] + _has_bonus_free_reroll[player_index] = player_dict["HAS_BONUS_FREE_REROLL"] + set_reroll_button_price(player_index) + + var locked_items : Array = player_dict["LOCKED_ITEMS"] + for item in _get_shop_items_container(player_index).get_children(): + if not item is ShopItem: + continue + + var is_locked = false + for locked_item in locked_items: + if locked_item["ID"] == item.item_data.my_id: + is_locked = true + break + + if is_locked: + item.lock_visually() + else: + item.unlock_visually() + + RunData.players_data[player_index].gold = player_dict["GOLD"] + _get_gold_label(player_index).update_value(RunData.players_data[player_index].gold) + + waiting_to_start_shop = false + + +func _dictionary_for_inventory_item(inventory_element : InventoryElement) -> Dictionary: + var item_data : ItemData = inventory_element.item + var item_dict = { + "ID" : item_data.my_id, + "NUMBER" : inventory_element.current_number, + } + + var dlc = ProgressData.get_dlc_data("abyssal_terrors") + if dlc: + if item_data.is_cursed: + item_dict["CURSED"] = true + + return item_dict + + +func _client_focused_inventory_element(data : Dictionary, player_index : int) -> void: + var focused_element = _focus_inventory_item_for_dictionary(data, player_index) + + is_self_call = true + Utils.get_focus_emulator(player_index).focused_control = focused_element + + +func _dictionary_for_focus_inventory_element(element: InventoryElement, player_index : int) -> Dictionary: + var gear_container : PlayerGearContainer = _get_gear_container(player_index) + + var weapons : Array = gear_container.weapons_container._elements.get_children() + for weapon_index in weapons.size(): + if element == weapons[weapon_index]: + return { + "TYPE" : "WEAPON", + "INDEX" : weapon_index + } + + var items : Array = gear_container.items_container._elements.get_children() + for item_index in items.size(): + if element == items[item_index]: + return { + "TYPE" : "ITEM", + "INDEX" : item_index + } + + return {} + + +func _focus_inventory_item_for_dictionary(item_dict : Dictionary, player_index : int) -> Control: + var gear_container : PlayerGearContainer = _get_gear_container(player_index) + + var focus_type = item_dict["TYPE"] + if focus_type == "WEAPON": + var weapon_index = item_dict["INDEX"] + if weapon_index < gear_container.weapons_container._elements.get_children().size(): + return gear_container.weapons_container._elements.get_children()[weapon_index] + elif focus_type == "ITEM": + return gear_container.items_container._elements.get_children()[item_dict["INDEX"]] + elif focus_type == "GO": + return _get_go_button(player_index) + elif focus_type == "REROLL": + return _get_reroll_button(player_index) + elif focus_type == "SHOP_ITEM": + var shop_item : ShopItem = _shop_item_for_string(item_dict["ID"], player_index) + if shop_item: + return shop_item._button + else: + print("ERR - Focusing inventgory element of unknown type ", item_dict) + + print("ERR - failed to found focus for dict ", item_dict, " ", player_index) + return null + + +func _inventory_item_for_dictionary(item_dict : Dictionary) -> ItemData: + var query_id = item_dict["ID"] + + var result + for item in ItemService.items: + if item.my_id == query_id: + result = item.duplicate() + + for character in ItemService.characters: + if character.my_id == query_id: + result = character.duplicate() + + var dlc = ProgressData.get_dlc_data("abyssal_terrors") + if dlc: + if item_dict.has("CURSED") and item_dict["CURSED"]: + result.is_cursed = true + + return result + + +func _dictionary_for_shop_item(shop_item : Array) -> Dictionary: + return { + "ID" : shop_item[0].my_id, + "WAVE_VALUE": shop_item[1], + } + + +func _shop_item_for_dictionary(shop_item_dict : Dictionary) -> Array: + var item_element + + var query_id = shop_item_dict["ID"] + for item in ItemService.items: + if item.my_id == query_id: + item_element = item.duplicate() + for weapon in ItemService.weapons: + if weapon.my_id == query_id: + item_element = weapon.duplicate() + + var abyssal_dlc = ProgressData.get_dlc_data("abyssal_terrors") + if abyssal_dlc: + if shop_item_dict.has("CURSED") and shop_item_dict["CURSED"]: + item_element.is_cursed = true + + return [item_element, shop_item_dict["WAVE_VALUE"]] + + +func _dictionary_for_weapon(weapon_data : WeaponData) -> Dictionary: + var weapon_dict = { + "ID" : weapon_data.my_id + } + + var dlc = ProgressData.get_dlc_data("abyssal_terrors") + if dlc: + if weapon_data.is_cursed: + weapon_dict["CURSED"] = true + + return weapon_dict + + +func _weapon_for_dictionary(weapon_dict : Dictionary) -> WeaponData: + var query_id = weapon_dict["ID"] + + var result + for weapon in ItemService.weapons: + if weapon.my_id == query_id: + result = weapon.duplicate() + + var dlc = ProgressData.get_dlc_data("abyssal_terrors") + if dlc and result: + if weapon_dict.has("CURSED") and weapon_dict["CURSED"]: + result.is_cursed = true + + return result + + +func _string_for_weapon(weapon_data : WeaponData) -> String: + return weapon_data.my_id + + +func _weapon_for_string(weapon_string : String, player_index : int) -> WeaponData: + for element in _get_gear_container(player_index).weapons_container._elements.get_children(): + if _string_for_weapon(element.item) == weapon_string: + return element.item + return null + + +func _string_for_shop_item(shop_item : ShopItem) -> String: + return shop_item.item_data.my_id + + +func _shop_item_for_string(shop_item_string : String, player_index : int) -> ShopItem: + for item in _get_shop_items_container(player_index).get_children(): + if not item is ShopItem: + continue + if item.item_data.my_id == shop_item_string: + return item + + return null + + +func fill_shop_items(player_locked_items: Array, player_index: int, just_entered_shop: bool = false) -> void: + if $"/root/BrotogetherOptions".in_multiplayer_game: + if $"/root/SteamConnection".is_host(): + .fill_shop_items(player_locked_items, player_index, just_entered_shop) + else: + .fill_shop_items(player_locked_items, player_index, just_entered_shop) + + +func _focus_dictionary_for_player(player_index : int) -> Dictionary: + var focused_control : Control = Utils.get_focus_emulator(player_index).focused_control + for shop_item in _get_shop_items_container(player_index).get_children(): + if not shop_item is ShopItem: + continue + if shop_item._button == focused_control: + return { + "TYPE" : "SHOP_ITEM", + "ID" : _string_for_shop_item(shop_item) + } + + var gear_container : PlayerGearContainer = _get_gear_container(player_index) + var weapons : Array = gear_container.weapons_container._elements.get_children() + for weapon_index in weapons.size(): + if focused_control == weapons[weapon_index]: + return { + "TYPE" : "WEAPON", + "INDEX" : weapon_index + } + + var items : Array = gear_container.items_container._elements.get_children() + for item_index in items.size(): + if focused_control == items[item_index]: + return { + "TYPE" : "ITEM", + "INDEX" : item_index + } + + if focused_control == _get_go_button(player_index): + return { + "TYPE" : "GO", + } + + if focused_control == _get_reroll_button(player_index): + return { + "TYPE" : "REROLL", + } + + return {} + + +func _set_client_focus_for_player(focus_dict : Dictionary, player_index : int) -> void: + if not focus_dict.has("TYPE"): + print("ERR - unkown client focus ", player_index, " ", focus_dict) + return + + var focus_control = _focus_inventory_item_for_dictionary(focus_dict, player_index) + if focus_control != null and is_instance_valid(focus_control): + var focus_type : String = focus_dict["TYPE"] + if focus_type in ["SHOP_ITEM", "ITEM", "WEAPON"]: + is_self_call = true + Utils.get_focus_emulator(player_index).focused_control = focus_control + else: + print("ERR - Invalid focus dict", focus_dict, " ", player_index) + + +# Process time check for focusing reroll or go buttons which don't have +# exisiting connected functions +func _check_for_focus_change() -> void: + var player_index : int = steam_connection.get_my_index() + var focused_control = Utils.get_focus_emulator(player_index).focused_control + + if focused_control == _get_reroll_button(player_index): + if not focusing_reroll_button and not waiting_to_start_shop: + steam_connection.shop_focus_inventory_element(_focus_dictionary_for_player(player_index)) + focusing_reroll_button = true + else: + focusing_reroll_button = false + + if focused_control == _get_go_button(player_index): + if not focusing_go_button and not waiting_to_start_shop: + steam_connection.shop_focus_inventory_element(_focus_dictionary_for_player(player_index)) + focusing_go_button = true + else: + focusing_go_button = false + + +func _client_shop_reroll(player_index : int) -> void: + _on_RerollButton_pressed(player_index) + + +func _on_RerollButton_pressed(player_index: int)->void : + if in_multiplayer_game: + if steam_connection.is_host(): + ._on_RerollButton_pressed(player_index) + steam_connection.shop_reroll(player_index) + else: + ._on_RerollButton_pressed( player_index) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/player_gear_container.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/player_gear_container.gd new file mode 100644 index 0000000..6cabcd0 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/player_gear_container.gd @@ -0,0 +1,19 @@ +extends "res://ui/menus/shop/player_gear_container.gd" + +var steam_connection +var brotatogether_options + +var in_multiplayer_game = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + +func set_items_data(items: Array)->void : + if in_multiplayer_game: + items_container.set_data("ITEMS", Category.ITEM, items, false, true) + else: + .set_items_data(items) diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/shop.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/shop.gd new file mode 100644 index 0000000..b3ec7a3 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/shop.gd @@ -0,0 +1,3 @@ +extends "res://ui/menus/shop/shop.gd" + +# Empty extension to help some extension chaos diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/shop_item.gd b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/shop_item.gd new file mode 100644 index 0000000..ffabeba --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/ui/menus/shop/shop_item.gd @@ -0,0 +1,28 @@ +extends "res://ui/menus/shop/shop_item.gd" + +var steam_connection +var brotatogether_options + +var in_multiplayer_game = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + +func deactivate()->void : + if in_multiplayer_game: + if steam_connection.is_host(): + .deactivate() + return + else: + modulate = Color(1, 1, 1, 0) + _button.disable() + _lock_icon.hide() + locked = false + active = false + emit_signal("shop_item_deactivated", self) + else: + .deactivate() diff --git a/src/mods-unpacked/Brogether-Brogether/extensions/visual_effects/floating_text/floating_text_manager.gd b/src/mods-unpacked/Brogether-Brogether/extensions/visual_effects/floating_text/floating_text_manager.gd new file mode 100644 index 0000000..cf1dc6e --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/extensions/visual_effects/floating_text/floating_text_manager.gd @@ -0,0 +1,33 @@ +extends "res://visual_effects/floating_text/floating_text_manager.gd" + +var steam_connection +var brotatogether_options +var in_multiplayer_game = false +var is_host = false + + +func _ready(): + steam_connection = $"/root/SteamConnection" + brotatogether_options = $"/root/BrotogetherOptions" + in_multiplayer_game = brotatogether_options.in_multiplayer_game + + if in_multiplayer_game: + is_host = steam_connection.is_host() + + +func display(value: String, text_pos: Vector2, color: Color = Color.white, icon: Resource = null, p_duration: float = duration, always_display: bool = false, p_direction: Vector2 = direction, need_translate: bool = true, icon_scale: Vector2 = Vector2(0.5, 0.5))->void : + .display(value, text_pos, color, icon, p_duration, always_display, p_direction, need_translate, icon_scale) + + if in_multiplayer_game and self.is_host: + var text_dict = {} + + text_dict["X_POS"] = text_pos.x + text_dict["Y_POS"] = text_pos.y + text_dict["R_COLOR"] = color.r8 + text_dict["G_COLOR"] = color.g8 + text_dict["B_COLOR"] = color.b8 + text_dict["A_COLOR"] = color.a8 + text_dict["VALUE"] = value + text_dict["DURATION"] = p_duration + +# brotatogether_options.batched_floating_text.push_back(text_dict) diff --git a/src/mods-unpacked/Brogether-Brogether/manifest.json b/src/mods-unpacked/Brogether-Brogether/manifest.json index c7a60f6..cff3f25 100644 --- a/src/mods-unpacked/Brogether-Brogether/manifest.json +++ b/src/mods-unpacked/Brogether-Brogether/manifest.json @@ -1,7 +1,7 @@ { "name": "Brogether", "namespace": "Brogether", - "version_number": "0.1.0", + "version_number": "0.1.6", "description": "Experimental 2-player online multiplayer foundation for Brotato using Steam lobbies and P2P packets.", "website_url": "https://git.mischlabs.de/MrDiderot/Brogether", "dependencies": [], diff --git a/src/mods-unpacked/Brogether-Brogether/mod_main.gd b/src/mods-unpacked/Brogether-Brogether/mod_main.gd index 87eec50..9bfaff4 100644 --- a/src/mods-unpacked/Brogether-Brogether/mod_main.gd +++ b/src/mods-unpacked/Brogether-Brogether/mod_main.gd @@ -1,1282 +1,63 @@ -extends Node +extends Node -const MOD_ID := "Brogether-Brogether" -const LOG_PATH := "user://brogether.log" -const BROGETHER_VERSION := "0.1.0" -const MAX_LOBBY_MEMBERS := 2 -const FAKE_PEER_ID := 48000000000000001 +const SteamConnection = preload("res://mods-unpacked/Brogether-Brogether/steam_connection.gd") +const BrotogetherOptions = preload("res://mods-unpacked/Brogether-Brogether/brotatogether_options.gd") -var steam = null -var lobby_id := 0 -var lobby_members := [] -var available_lobbies := [] -var selected_lobby_id := 0 -var pending_lobby_name := "" -var pending_lobby_password_hash := "" -var lobby_ready_states := {} -var fake_peer_enabled := false -var steam_id := 0 -var is_host := false -var packet_sequence := 0 -var status_layer: CanvasLayer = null -var status_label: Label = null -var dim_background: ColorRect = null -var panel: PanelContainer = null -var panel_status_label: Label = null -var panel_log_label: Label = null -var panel_default_focus: Control = null -var browser_view: Control = null -var lobby_view: Control = null -var lobby_list_container: VBoxContainer = null -var lobby_empty_label: Label = null -var lobby_name_input: LineEdit = null -var lobby_password_input: LineEdit = null -var lobby_password_check: CheckBox = null -var join_password_input: LineEdit = null -var selected_lobby_label: Label = null -var lobby_members_container: GridContainer = null -var lobby_title_label: Label = null -var lobby_detail_label: Label = null -var ready_button: Button = null -var fake_peer_button: Button = null -var fake_ready_button: Button = null -var status_time_left := 0.0 -var restore_paused_value := false -var restore_focus_control = null -var current_run_id := "" -var current_run_active := false -var current_players := [] -var snapshot_sequence := 0 +const MOD_DIR = "Brogether-Brogether/" +var dir = "" +var ext_dir = "" +var trans_dir = "" -func _init() -> void: - ModLoaderMod.install_script_extension("res://mods-unpacked/Brogether-Brogether/extensions/main_ext.gd") - ModLoaderMod.install_script_extension("res://mods-unpacked/Brogether-Brogether/extensions/main_menu_ext.gd") +func _init(): + Steam.steamInit() -func _ready() -> void: - name = MOD_ID - pause_mode = Node.PAUSE_MODE_PROCESS - set_process(true) - set_process_input(true) - _clear_log() - _create_status_overlay() - _create_debug_panel() - _init_steam() - _log("loaded. F5=toggle modal, F6=create lobby, F7=find/join lobby, F9=leave, F10=ping, F11=loopback test") + dir = ModLoaderMod.get_unpacked_dir() + MOD_DIR + ext_dir = dir + "extensions/" + trans_dir = dir + "translations/" + # Add extensions + ModLoaderMod.install_script_extension(ext_dir + "entities/units/unit/unit.gd") + ModLoaderMod.install_script_extension(ext_dir + "entities/structures/turret/turret.gd") + ModLoaderMod.install_script_extension(ext_dir + "entities/entity.gd") + ModLoaderMod.install_script_extension(ext_dir + "entities/birth/entity_birth.gd") + ModLoaderMod.install_script_extension(ext_dir + "entities/units/movement_behaviors/player_movement_behavior.gd") -func _exit_tree() -> void: - set_process(false) - set_process_input(false) - _disconnect_steam_signals() - if steam != null and lobby_id != 0 and steam.has_method("leaveLobby"): - steam.call("leaveLobby", lobby_id) - lobby_id = 0 - is_host = false - lobby_members.clear() - available_lobbies.clear() - lobby_ready_states.clear() - fake_peer_enabled = false - current_players.clear() - _free_ui() - steam = null + # TODO replace this +# ModLoaderMod.install_script_extension(ext_dir + "global/effects_manager.gd") + ModLoaderMod.install_script_extension(ext_dir + "global/entity_spawner.gd") + ModLoaderMod.install_script_extension(ext_dir + "items/global/item.gd") + ModLoaderMod.install_script_extension(ext_dir + "main.gd") -func _process(_delta: float) -> void: - if status_time_left > 0.0: - status_time_left -= _delta - if status_time_left <= 0.0 and status_label != null: - status_label.visible = false - if steam == null: - return - if steam.has_method("run_callbacks"): - steam.call("run_callbacks") - if lobby_id != 0: - _read_all_p2p_packets() + ModLoaderMod.install_script_extension(ext_dir + "projectiles/player_explosion.gd") + ModLoaderMod.install_script_extension(ext_dir + "singletons/coop_service.gd") + ModLoaderMod.install_script_extension(ext_dir + "singletons/run_data.gd") + ModLoaderMod.install_script_extension(ext_dir + "singletons/sound_manager.gd") + ModLoaderMod.install_script_extension(ext_dir + "singletons/sound_manager_2d.gd") + ModLoaderMod.install_script_extension(ext_dir + "singletons/utils.gd") -func _input(event: InputEvent) -> void: - if not event is InputEventKey or not event.pressed or event.echo: - return - if panel != null and panel.visible and event.scancode == KEY_ESCAPE: - close_debug_panel() - get_tree().set_input_as_handled() - return - match event.scancode: - KEY_F5: - toggle_debug_panel() - get_tree().set_input_as_handled() - KEY_F6: - create_lobby() - get_tree().set_input_as_handled() - KEY_F7: - find_lobbies() - get_tree().set_input_as_handled() - KEY_F9: - leave_lobby() - get_tree().set_input_as_handled() - KEY_F10: - send_ping() - get_tree().set_input_as_handled() - KEY_F11: - run_loopback_test() - get_tree().set_input_as_handled() + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/ingame/coop_upgrades_ui_player_container.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/ingame/pause_menu.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/pages/main_menu.gd") -func create_lobby(password: String = "", lobby_name: String = "") -> void: - if steam == null: - _log("cannot create lobby: Steam singleton missing") - return - if lobby_id != 0: - _log("already in lobby %s" % lobby_id) - _update_debug_panel() - return - pending_lobby_name = lobby_name.strip_edges() - if pending_lobby_name.empty(): - pending_lobby_name = "%s's Brogether Lobby" % _get_persona_name(steam_id) - pending_lobby_password_hash = _hash_password(password) - var lobby_type := 2 - _log("creating lobby '%s'" % pending_lobby_name) - steam.call("createLobby", lobby_type, MAX_LOBBY_MEMBERS) + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/run/character_selection.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/run/difficulty_selection/difficulty_selection.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/run/weapon_selection.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/shop/coop_shop.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/shop/shop.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/shop/shop_item.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/shop/player_gear_container.gd") + ModLoaderMod.install_script_extension(ext_dir + "ui/menus/global/focus_emulator.gd") -func find_lobbies() -> void: - if steam == null: - _log("cannot find lobbies: Steam singleton missing") - return - available_lobbies.clear() - selected_lobby_id = 0 - _update_lobby_browser() - if steam.has_method("addRequestLobbyListStringFilter"): - steam.call("addRequestLobbyListStringFilter", "brogether", "1", 0) - if steam.has_method("addRequestLobbyListDistanceFilter"): - steam.call("addRequestLobbyListDistanceFilter", 2) - _log("refreshing lobby browser") - steam.call("requestLobbyList") +func _ready(): + var steam_connection = SteamConnection.new() + steam_connection.set_name("SteamConnection") + $"/root".call_deferred("add_child",steam_connection) - -func join_lobby(target_lobby_id: int) -> void: - if steam == null: - return - if lobby_id != 0: - leave_lobby() - _log("joining lobby %s" % target_lobby_id) - steam.call("joinLobby", target_lobby_id) - - -func leave_lobby() -> void: - if steam == null or lobby_id == 0: - return - _log("leaving lobby %s" % lobby_id) - if steam.has_method("leaveLobby"): - steam.call("leaveLobby", lobby_id) - lobby_id = 0 - lobby_members.clear() - lobby_ready_states.clear() - fake_peer_enabled = false - is_host = false - _show_browser_view() - _update_debug_panel() - - -func send_ping() -> void: - if lobby_id == 0: - _log("not in a lobby yet") - return - _send_p2p_packet(0, { - "type": "ping", - "sequence": packet_sequence, - "from": steam_id, - "time": OS.get_ticks_msec() - }) - packet_sequence += 1 - - -func run_loopback_test() -> void: - var fake_remote_id := 48000000000000001 - _log("starting local loopback test") - _handle_packet(fake_remote_id, { - "type": "handshake", - "from": fake_remote_id - }, true) - _handle_packet(fake_remote_id, { - "type": "ping", - "sequence": packet_sequence, - "from": fake_remote_id, - "time": OS.get_ticks_msec() - }, true) - packet_sequence += 1 - _log("local loopback test finished") - - -func on_run_scene_ready(main_node: Node) -> void: - current_run_active = true - current_run_id = "%s-%s-%s" % [steam_id, OS.get_unix_time(), RunData.current_wave] - var run_state := _build_run_state() - _log("run scene ready: %s" % run_state) - _send_p2p_packet(0, { - "type": "run_started", - "run_id": current_run_id, - "from": steam_id, - "state": run_state - }) - - -func on_run_players_spawned(players: Array) -> void: - current_players = players - _log("players spawned: %s" % _build_players_snapshot(players)) - _send_p2p_packet(0, { - "type": "players_spawned", - "run_id": current_run_id, - "from": steam_id, - "players": _build_players_snapshot(players) - }) - - -func on_run_snapshot_tick(players: Array) -> void: - if not current_run_active: - return - var snapshot := _build_players_snapshot(players) - if snapshot.empty(): - return - var packet := { - "type": "host_snapshot" if is_host else "client_snapshot", - "run_id": current_run_id, - "sequence": snapshot_sequence, - "from": steam_id, - "wave": RunData.current_wave, - "players": snapshot - } - snapshot_sequence += 1 - _send_p2p_packet(0, packet) - - -func on_run_scene_exiting() -> void: - if current_run_active: - _log("run scene exiting: %s" % current_run_id) - current_run_active = false - current_run_id = "" - current_players.clear() - snapshot_sequence = 0 - - -func prepare_online_coop_slots() -> void: - _log("preparing experimental 2-player online coop slots; this intentionally enters Brotato local coop UI") - RunData.play_mode = RunData.PlayMode.COOP - RunData.set_coop_run(true) - RunData.set_player_count(2) - CoopService.clear_coop_players() - CoopService._add_player(CoopService.KEYBOARD_REMAPPED_DEVICE_ID, CoopService.PlayerType.KEYBOARD_AND_MOUSE) - CoopService._add_player(CoopService.FIRST_DEBUG_DEVICE_ID, CoopService.PlayerType.KEYBOARD_AND_MOUSE) - _log("coop slots ready: RunData players=%s, CoopService players=%s" % [RunData.get_player_count(), CoopService.connected_players]) - - -func _init_steam() -> void: - if not Engine.has_singleton("Steam"): - _log("Steam singleton not available") - return - steam = Engine.get_singleton("Steam") - if steam.has_method("getSteamID"): - steam_id = int(steam.call("getSteamID")) - _connect_steam_signal("lobby_created", "_on_lobby_created") - _connect_steam_signal("lobby_joined", "_on_lobby_joined") - _connect_steam_signal("lobby_match_list", "_on_lobby_match_list") - _connect_steam_signal("lobby_chat_update", "_on_lobby_chat_update") - _connect_steam_signal("p2p_session_request", "_on_p2p_session_request") - _connect_steam_signal("p2p_session_connect_fail", "_on_p2p_session_connect_fail") - if steam.has_method("allowP2PPacketRelay"): - steam.call("allowP2PPacketRelay", true) - _log("Steam ready as %s" % steam_id) - - -func _connect_steam_signal(signal_name: String, method_name: String) -> void: - if steam == null: - return - if steam.has_signal(signal_name) and not steam.is_connected(signal_name, self, method_name): - steam.connect(signal_name, self, method_name) - - -func _disconnect_steam_signals() -> void: - _disconnect_steam_signal("lobby_created", "_on_lobby_created") - _disconnect_steam_signal("lobby_joined", "_on_lobby_joined") - _disconnect_steam_signal("lobby_match_list", "_on_lobby_match_list") - _disconnect_steam_signal("lobby_chat_update", "_on_lobby_chat_update") - _disconnect_steam_signal("p2p_session_request", "_on_p2p_session_request") - _disconnect_steam_signal("p2p_session_connect_fail", "_on_p2p_session_connect_fail") - - -func _disconnect_steam_signal(signal_name: String, method_name: String) -> void: - if steam == null: - return - if steam.has_signal(signal_name) and steam.is_connected(signal_name, self, method_name): - steam.disconnect(signal_name, self, method_name) - - -func _on_lobby_created(connect: int, created_lobby_id: int) -> void: - if connect != 1: - _log("lobby creation failed: %s" % connect) - return - lobby_id = created_lobby_id - is_host = true - _log("created lobby %s" % lobby_id) - steam.call("setLobbyJoinable", lobby_id, true) - steam.call("setLobbyData", lobby_id, "name", pending_lobby_name) - steam.call("setLobbyData", lobby_id, "brogether", "1") - steam.call("setLobbyData", lobby_id, "version", BROGETHER_VERSION) - steam.call("setLobbyData", lobby_id, "max_players", str(MAX_LOBBY_MEMBERS)) - steam.call("setLobbyData", lobby_id, "has_password", "1" if not pending_lobby_password_hash.empty() else "0") - steam.call("setLobbyData", lobby_id, "password_hash", pending_lobby_password_hash) - lobby_ready_states.clear() - _set_member_ready(steam_id, false) - _refresh_lobby_members() - _show_lobby_view() - _update_debug_panel() - - -func _on_lobby_joined(joined_lobby_id: int, _permissions: int, _locked: bool, response: int) -> void: - var success := response == 1 - if not success: - _log("join failed for lobby %s: %s" % [joined_lobby_id, response]) - return - lobby_id = joined_lobby_id - lobby_ready_states.clear() - _set_member_ready(steam_id, false) - _refresh_lobby_members() - is_host = steam.has_method("getLobbyOwner") and int(steam.call("getLobbyOwner", lobby_id)) == steam_id - _log("joined lobby %s as %s with %s member(s)" % [lobby_id, "host" if is_host else "client", lobby_members.size()]) - _send_p2p_packet(0, {"type": "handshake", "from": steam_id}) - _show_lobby_view() - _update_debug_panel() - - -func _on_lobby_match_list(lobbies: Array) -> void: - available_lobbies.clear() - for candidate in lobbies: - var candidate_id := int(candidate) - if steam.has_method("getLobbyData") and steam.call("getLobbyData", candidate_id, "brogether") == "1": - available_lobbies.append(_get_lobby_summary(candidate_id)) - _log("found %s Brogether lobby candidate(s)" % available_lobbies.size()) - _update_lobby_browser() - - -func _get_lobby_summary(target_lobby_id: int) -> Dictionary: - var member_count := 0 - var max_members := MAX_LOBBY_MEMBERS - if steam != null and steam.has_method("getNumLobbyMembers"): - member_count = int(steam.call("getNumLobbyMembers", target_lobby_id)) - if steam != null and steam.has_method("getLobbyMemberLimit"): - max_members = int(steam.call("getLobbyMemberLimit", target_lobby_id)) - var owner_id := 0 - if steam != null and steam.has_method("getLobbyOwner"): - owner_id = int(steam.call("getLobbyOwner", target_lobby_id)) - return { - "id": target_lobby_id, - "name": _get_lobby_data(target_lobby_id, "name", "Brogether Lobby"), - "owner_id": owner_id, - "owner_name": _get_persona_name(owner_id), - "member_count": member_count, - "max_members": max_members, - "has_password": _get_lobby_data(target_lobby_id, "has_password", "0") == "1", - "password_hash": _get_lobby_data(target_lobby_id, "password_hash", ""), - "version": _get_lobby_data(target_lobby_id, "version", "") - } - - -func _get_lobby_data(target_lobby_id: int, key: String, fallback: String = "") -> String: - if steam == null or not steam.has_method("getLobbyData"): - return fallback - var value = steam.call("getLobbyData", target_lobby_id, key) - if value == null: - return fallback - var text := str(value) - return fallback if text.empty() else text - - -func _get_persona_name(target_steam_id: int) -> String: - if target_steam_id == 0: - return "Unknown" - if steam != null and steam.has_method("getFriendPersonaName"): - var persona := str(steam.call("getFriendPersonaName", target_steam_id)) - if not persona.empty(): - return persona - return str(target_steam_id) - - -func _hash_password(password: String) -> String: - var clean := password.strip_edges() - return "" if clean.empty() else str(clean.hash()) - - -func _on_lobby_chat_update(this_lobby_id: int, _changed_id: int, _making_change_id: int, _chat_state: int) -> void: - if this_lobby_id != lobby_id: - return - _refresh_lobby_members() - _send_p2p_packet(0, {"type": "handshake", "from": steam_id}) - _update_debug_panel() - - -func _on_p2p_session_request(remote_id: int) -> void: - _log("accepting P2P session from %s" % remote_id) - steam.call("acceptP2PSessionWithUser", remote_id) - _send_p2p_packet(remote_id, {"type": "handshake_ack", "from": steam_id}) - - -func _on_p2p_session_connect_fail(remote_id: int, session_error: int) -> void: - _log("P2P session failed with %s: error %s" % [remote_id, session_error]) - - -func _refresh_lobby_members() -> void: - lobby_members.clear() - if lobby_id == 0 or steam == null or not steam.has_method("getNumLobbyMembers"): - return - var member_count := int(steam.call("getNumLobbyMembers", lobby_id)) - for index in range(member_count): - var member_id := int(steam.call("getLobbyMemberByIndex", lobby_id, index)) - lobby_members.append({"steam_id": member_id, "name": _get_persona_name(member_id)}) - if not lobby_ready_states.has(member_id): - _set_member_ready(member_id, false) - _append_fake_peer_if_needed() - _log("members: %s" % lobby_members) - _update_lobby_room() - _update_debug_panel() - - -func _read_all_p2p_packets(read_count: int = 0) -> void: - if read_count >= 32 or steam == null or not steam.has_method("getAvailableP2PPacketSize"): - return - var packet_size := int(steam.call("getAvailableP2PPacketSize", 0)) - if packet_size <= 0: - return - _read_p2p_packet(packet_size) - _read_all_p2p_packets(read_count + 1) - - -func _read_p2p_packet(packet_size: int) -> void: - var packet: Dictionary = steam.call("readP2PPacket", packet_size, 0) - if packet.empty() or not packet.has("data"): - _log("read empty packet") - return - var sender := int(packet.get("remote_steam_id", 0)) - var data = bytes2var(packet["data"]) - _handle_packet(sender, data) - - -func _handle_packet(sender: int, data, is_loopback: bool = false) -> void: - _log("packet from %s: %s" % [sender, data]) - if typeof(data) != TYPE_DICTIONARY: - return - match data.get("type", ""): - "handshake": - _log("handshake received from %s" % sender) - "handshake_ack": - _log("handshake acknowledged by %s" % sender) - "ping": - _log("ping received from %s, sending pong" % sender) - if is_loopback: - _handle_packet(sender, { - "type": "pong", - "sequence": data.get("sequence", -1), - "from": steam_id, - "time": OS.get_ticks_msec() - }, true) - return - _send_p2p_packet(sender, { - "type": "pong", - "sequence": data.get("sequence", -1), - "from": steam_id, - "time": OS.get_ticks_msec() - }) - "pong": - _log("pong received from %s for sequence %s" % [sender, data.get("sequence", -1)]) - "ready_state": - var ready_sender := int(data.get("from", sender)) - _set_member_ready(ready_sender, bool(data.get("ready", false))) - _log("ready state from %s: %s" % [ready_sender, _ready_text(ready_sender)]) - _update_lobby_room() - "run_started": - _log("remote run started from %s: %s" % [sender, data.get("state", {})]) - "players_spawned": - _log("remote players spawned from %s: %s" % [sender, data.get("players", [])]) - "host_snapshot": - _log("host snapshot %s from %s: %s player(s)" % [data.get("sequence", -1), sender, data.get("players", []).size()]) - _apply_remote_snapshot(sender, data) - "client_snapshot": - _log("client snapshot %s from %s: %s player(s)" % [data.get("sequence", -1), sender, data.get("players", []).size()]) - _apply_remote_snapshot(sender, data) - _: - _log("unknown packet type from %s" % sender) - - -func _send_p2p_packet(target: int, packet_data: Dictionary) -> void: - if steam == null or lobby_id == 0: - return - var send_type := 2 - var payload := PoolByteArray() - payload.append_array(var2bytes(packet_data)) - if target == 0: - for member in lobby_members: - var member_id := int(member["steam_id"]) - if member_id != steam_id and not _is_fake_peer(member_id): - steam.call("sendP2PPacket", member_id, payload, send_type, 0) - else: - if _is_fake_peer(target): - _handle_packet(target, packet_data, true) - return - steam.call("sendP2PPacket", target, payload, send_type, 0) - - -func _log(message: String) -> void: - var line := "[Brogether] %s" % message - print(line) - _show_status(message) - _update_debug_panel(message) - var file := File.new() - var error := file.open(LOG_PATH, File.READ_WRITE) - if error != OK: - error = file.open(LOG_PATH, File.WRITE) - if error == OK: - file.seek_end() - file.store_line(line) - file.close() - - -func _clear_log() -> void: - var file := File.new() - if file.open(LOG_PATH, File.WRITE) == OK: - file.store_line("[Brogether] log started") - file.close() - - -func _create_status_overlay() -> void: - status_layer = CanvasLayer.new() - status_layer.name = "BrogetherStatusLayer" - status_layer.layer = 100 - status_layer.pause_mode = Node.PAUSE_MODE_PROCESS - add_child(status_layer) - - status_label = Label.new() - status_label.name = "BrogetherStatusLabel" - status_label.anchor_left = 0.0 - status_label.anchor_top = 0.0 - status_label.margin_left = 16 - status_label.margin_top = 16 - status_label.rect_min_size = Vector2(620, 32) - status_label.add_color_override("font_color", Color(0.25, 1.0, 0.65, 1.0)) - status_label.add_color_override("font_color_shadow", Color(0, 0, 0, 0.85)) - status_label.add_constant_override("shadow_offset_x", 2) - status_label.add_constant_override("shadow_offset_y", 2) - status_label.visible = false - status_layer.add_child(status_label) - - -func _create_debug_panel() -> void: - dim_background = ColorRect.new() - dim_background.name = "BrogetherDimBackground" - dim_background.anchor_right = 1.0 - dim_background.anchor_bottom = 1.0 - dim_background.color = Color(0, 0, 0, 0.62) - dim_background.mouse_filter = Control.MOUSE_FILTER_STOP - dim_background.visible = false - status_layer.add_child(dim_background) - - panel = PanelContainer.new() - panel.name = "BrogetherPanel" - panel.anchor_left = 0.5 - panel.anchor_right = 0.5 - panel.anchor_top = 0.5 - panel.anchor_bottom = 0.5 - panel.margin_left = -430 - panel.margin_right = 430 - panel.margin_top = -300 - panel.margin_bottom = 300 - panel.pause_mode = Node.PAUSE_MODE_PROCESS - panel.visible = false - - panel.add_stylebox_override("panel", _make_panel_style(Color(0.075, 0.08, 0.09, 0.98), Color(0.55, 0.58, 0.62, 1.0), 18)) - - var box: VBoxContainer = VBoxContainer.new() - box.name = "Content" - box.add_constant_override("separation", 12) - panel.add_child(box) - - var header: HBoxContainer = HBoxContainer.new() - header.add_constant_override("separation", 10) - box.add_child(header) - - var title: Label = Label.new() - title.text = "BROGETHER" - title.rect_min_size = Vector2(220, 28) - title.add_color_override("font_color", Color(1, 1, 1, 1)) - header.add_child(title) - - panel_status_label = Label.new() - panel_status_label.text = "Steam: ..." - panel_status_label.align = Label.ALIGN_RIGHT - panel_status_label.autowrap = true - panel_status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - header.add_child(panel_status_label) - - var close_button: Button = Button.new() - close_button.text = "X" - close_button.rect_min_size = Vector2(42, 34) - close_button.connect("pressed", self, "close_debug_panel") - header.add_child(close_button) - - var separator: HSeparator = HSeparator.new() - box.add_child(separator) - - browser_view = HBoxContainer.new() - browser_view.name = "LobbyBrowser" - browser_view.size_flags_vertical = Control.SIZE_EXPAND_FILL - browser_view.add_constant_override("separation", 14) - box.add_child(browser_view) - - var browser_column: VBoxContainer = VBoxContainer.new() - browser_column.rect_min_size = Vector2(390, 410) - browser_column.size_flags_vertical = Control.SIZE_EXPAND_FILL - browser_column.add_constant_override("separation", 8) - browser_view.add_child(browser_column) - - var browser_title: Label = Label.new() - browser_title.text = "LOBBY BROWSER" - browser_title.add_color_override("font_color", Color(0.92, 0.96, 1.0, 1.0)) - browser_column.add_child(browser_title) - - var browser_hint: Label = Label.new() - browser_hint.text = "Open Brogether lobbies appear here." - browser_hint.autowrap = true - browser_column.add_child(browser_hint) - - var scroll: ScrollContainer = ScrollContainer.new() - scroll.rect_min_size = Vector2(390, 300) - scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - browser_column.add_child(scroll) - - lobby_list_container = VBoxContainer.new() - lobby_list_container.add_constant_override("separation", 6) - scroll.add_child(lobby_list_container) - - lobby_empty_label = Label.new() - lobby_empty_label.text = "No lobbies loaded yet." - lobby_empty_label.autowrap = true - lobby_list_container.add_child(lobby_empty_label) - - var refresh_button: Button = Button.new() - refresh_button.text = "Refresh" - refresh_button.rect_min_size = Vector2(390, 36) - refresh_button.connect("pressed", self, "find_lobbies") - browser_column.add_child(refresh_button) - - var action_column: VBoxContainer = VBoxContainer.new() - action_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL - action_column.add_constant_override("separation", 10) - browser_view.add_child(action_column) - - var create_box: PanelContainer = PanelContainer.new() - create_box.add_stylebox_override("panel", _make_panel_style(Color(0.105, 0.11, 0.12, 0.95), Color(0.28, 0.31, 0.34, 1.0), 12)) - action_column.add_child(create_box) - - var create_content: VBoxContainer = VBoxContainer.new() - create_content.add_constant_override("separation", 8) - create_box.add_child(create_content) - - var create_title: Label = Label.new() - create_title.text = "CREATE LOBBY" - create_content.add_child(create_title) - - lobby_name_input = LineEdit.new() - lobby_name_input.placeholder_text = "Lobby name" - lobby_name_input.rect_min_size = Vector2(360, 34) - create_content.add_child(lobby_name_input) - - lobby_password_check = CheckBox.new() - lobby_password_check.text = "Use password" - create_content.add_child(lobby_password_check) - - lobby_password_input = LineEdit.new() - lobby_password_input.placeholder_text = "Password" - lobby_password_input.secret = true - lobby_password_input.rect_min_size = Vector2(360, 34) - create_content.add_child(lobby_password_input) - - var host_button: Button = Button.new() - host_button.text = "Host Lobby" - host_button.rect_min_size = Vector2(360, 38) - host_button.connect("pressed", self, "_create_lobby_from_ui") - create_content.add_child(host_button) - panel_default_focus = host_button - - var join_box: PanelContainer = PanelContainer.new() - join_box.add_stylebox_override("panel", _make_panel_style(Color(0.105, 0.11, 0.12, 0.95), Color(0.28, 0.31, 0.34, 1.0), 12)) - action_column.add_child(join_box) - - var join_content: VBoxContainer = VBoxContainer.new() - join_content.add_constant_override("separation", 8) - join_box.add_child(join_content) - - var join_title: Label = Label.new() - join_title.text = "SELECTED LOBBY" - join_content.add_child(join_title) - - selected_lobby_label = Label.new() - selected_lobby_label.text = "No lobby selected." - selected_lobby_label.autowrap = true - selected_lobby_label.rect_min_size = Vector2(360, 58) - join_content.add_child(selected_lobby_label) - - join_password_input = LineEdit.new() - join_password_input.placeholder_text = "Password if required" - join_password_input.secret = true - join_password_input.rect_min_size = Vector2(360, 34) - join_content.add_child(join_password_input) - - var join_button: Button = Button.new() - join_button.text = "Join Lobby" - join_button.rect_min_size = Vector2(360, 38) - join_button.connect("pressed", self, "_join_selected_lobby_from_ui") - join_content.add_child(join_button) - - lobby_view = VBoxContainer.new() - lobby_view.name = "LobbyRoom" - lobby_view.size_flags_vertical = Control.SIZE_EXPAND_FILL - lobby_view.add_constant_override("separation", 12) - lobby_view.visible = false - box.add_child(lobby_view) - - lobby_title_label = Label.new() - lobby_title_label.text = "WAITING ROOM" - lobby_title_label.add_color_override("font_color", Color(0.92, 0.96, 1.0, 1.0)) - lobby_view.add_child(lobby_title_label) - - lobby_detail_label = Label.new() - lobby_detail_label.text = "Not in a lobby yet." - lobby_detail_label.autowrap = true - lobby_view.add_child(lobby_detail_label) - - lobby_members_container = GridContainer.new() - lobby_members_container.columns = 2 - lobby_members_container.add_constant_override("hseparation", 10) - lobby_members_container.add_constant_override("vseparation", 10) - lobby_members_container.size_flags_vertical = Control.SIZE_EXPAND_FILL - lobby_view.add_child(lobby_members_container) - - var lobby_actions: HBoxContainer = HBoxContainer.new() - lobby_actions.add_constant_override("separation", 8) - lobby_view.add_child(lobby_actions) - - ready_button = _add_panel_button(lobby_actions, "Ready", "toggle_local_ready") - fake_peer_button = _add_panel_button(lobby_actions, "Fake Peer", "toggle_fake_peer") - fake_ready_button = _add_panel_button(lobby_actions, "Fake Ready", "toggle_fake_peer_ready") - _add_panel_button(lobby_actions, "Ping", "send_ping") - _add_panel_button(lobby_actions, "Leave", "leave_lobby") - _add_panel_button(lobby_actions, "Browser", "_show_browser_view") - - panel_log_label = Label.new() - panel_log_label.text = "Ready" - panel_log_label.autowrap = true - panel_log_label.rect_min_size = Vector2(820, 44) - box.add_child(panel_log_label) - - status_layer.add_child(panel) - _show_browser_view() - _update_debug_panel() - - -func _add_panel_button(parent: Control, label: String, method_name: String) -> Button: - var button: Button = Button.new() - button.text = label - button.rect_min_size = Vector2(130, 36) - button.connect("pressed", self, method_name) - parent.add_child(button) - return button - - -func _make_panel_style(bg: Color, border: Color, margin: int) -> StyleBoxFlat: - var style: StyleBoxFlat = StyleBoxFlat.new() - style.bg_color = bg - style.border_color = border - style.set_border_width_all(1) - style.set_corner_radius_all(3) - style.content_margin_left = margin - style.content_margin_right = margin - style.content_margin_top = margin - style.content_margin_bottom = margin - return style - - -func _create_lobby_from_ui() -> void: - var password := "" - if lobby_password_check != null and lobby_password_check.pressed and lobby_password_input != null: - password = lobby_password_input.text - var display_name := "" - if lobby_name_input != null: - display_name = lobby_name_input.text - create_lobby(password, display_name) - - -func _join_selected_lobby_from_ui() -> void: - if selected_lobby_id == 0: - _log("select a lobby before joining") - return - var summary: Dictionary = _get_selected_lobby_summary() - if summary.has("has_password") and bool(summary["has_password"]): - var entered := "" - if join_password_input != null: - entered = join_password_input.text - if _hash_password(entered) != str(summary.get("password_hash", "")): - _log("wrong or missing lobby password") - return - join_lobby(selected_lobby_id) - - -func toggle_local_ready() -> void: - if lobby_id == 0: - _log("join or create a lobby before readying up") - return - var next_ready := not _is_member_ready(steam_id) - _set_member_ready(steam_id, next_ready) - _send_p2p_packet(0, { - "type": "ready_state", - "from": steam_id, - "ready": next_ready - }) - _log("local ready state: %s" % _ready_text(steam_id)) - _update_lobby_room() - - -func toggle_fake_peer() -> void: - if lobby_id == 0: - _log("create a lobby before adding a fake peer") - return - fake_peer_enabled = not fake_peer_enabled - if fake_peer_enabled: - _set_member_ready(FAKE_PEER_ID, false) - _append_fake_peer_if_needed() - _handle_packet(FAKE_PEER_ID, {"type": "handshake", "from": FAKE_PEER_ID}, true) - _log("fake peer joined the lobby") - else: - _remove_fake_peer() - _log("fake peer left the lobby") - _update_lobby_room() - - -func toggle_fake_peer_ready() -> void: - if not fake_peer_enabled: - _log("add a fake peer before toggling fake ready") - return - var next_ready := not _is_member_ready(FAKE_PEER_ID) - _handle_packet(FAKE_PEER_ID, { - "type": "ready_state", - "from": FAKE_PEER_ID, - "ready": next_ready - }, true) - - -func _show_browser_view() -> void: - if browser_view != null: - browser_view.visible = true - if lobby_view != null: - lobby_view.visible = false - _update_lobby_browser() - - -func _show_lobby_view() -> void: - if browser_view != null: - browser_view.visible = false - if lobby_view != null: - lobby_view.visible = true - _update_lobby_room() - - -func _update_lobby_browser() -> void: - if lobby_list_container == null: - return - _clear_children(lobby_list_container) - if available_lobbies.empty(): - lobby_empty_label = Label.new() - lobby_empty_label.text = "No Brogether lobbies found.\nCreate a lobby or press Refresh." - lobby_empty_label.autowrap = true - lobby_empty_label.rect_min_size = Vector2(360, 80) - lobby_list_container.add_child(lobby_empty_label) - else: - for summary_data in available_lobbies: - var summary: Dictionary = summary_data - var button: Button = Button.new() - button.text = _format_lobby_summary(summary) - button.rect_min_size = Vector2(360, 74) - button.connect("pressed", self, "_on_lobby_row_pressed", [int(summary["id"])]) - lobby_list_container.add_child(button) - _update_selected_lobby_label() - - -func _format_lobby_summary(summary: Dictionary) -> String: - var lock_text := "PASSWORT" if bool(summary.get("has_password", false)) else "OFFEN" - return "%s\n%s/%s players | %s | Host: %s" % [ - str(summary.get("name", "Brogether Lobby")), - str(summary.get("member_count", 0)), - str(summary.get("max_members", MAX_LOBBY_MEMBERS)), - lock_text, - str(summary.get("owner_name", "Unknown")) - ] - - -func _on_lobby_row_pressed(target_lobby_id: int) -> void: - selected_lobby_id = target_lobby_id - _update_selected_lobby_label() - - -func _update_selected_lobby_label() -> void: - if selected_lobby_label == null: - return - var summary: Dictionary = _get_selected_lobby_summary() - if summary.empty(): - selected_lobby_label.text = "No lobby selected." - return - var password_text := "Password required" if bool(summary.get("has_password", false)) else "No password" - selected_lobby_label.text = "%s\n%s/%s players\n%s" % [ - str(summary.get("name", "Brogether Lobby")), - str(summary.get("member_count", 0)), - str(summary.get("max_members", MAX_LOBBY_MEMBERS)), - password_text - ] - - -func _get_selected_lobby_summary() -> Dictionary: - for summary_data in available_lobbies: - var summary: Dictionary = summary_data - if int(summary.get("id", 0)) == selected_lobby_id: - return summary - return {} - - -func _update_lobby_room() -> void: - if lobby_members_container == null: - return - _clear_children(lobby_members_container) - if lobby_id == 0: - if lobby_title_label != null: - lobby_title_label.text = "WAITING ROOM" - if lobby_detail_label != null: - lobby_detail_label.text = "Not in a lobby yet." - return - var lobby_name: String = _get_lobby_data(lobby_id, "name", "Brogether Lobby") - var password_text := "password protected" if _get_lobby_data(lobby_id, "has_password", "0") == "1" else "no password" - if lobby_title_label != null: - lobby_title_label.text = lobby_name.to_upper() - if lobby_detail_label != null: - lobby_detail_label.text = "Waiting room %s | %s/%s players | %s | %s" % [str(lobby_id), lobby_members.size(), MAX_LOBBY_MEMBERS, password_text, _lobby_ready_summary()] - for member in lobby_members: - _add_member_card(member) - var empty_slot_count: int = max(0, MAX_LOBBY_MEMBERS - lobby_members.size()) - for empty_slot in range(empty_slot_count): - _add_empty_member_slot(empty_slot) - _update_lobby_controls() - - -func _add_member_card(member: Dictionary) -> void: - var card: PanelContainer = PanelContainer.new() - card.rect_min_size = Vector2(390, 128) - card.add_stylebox_override("panel", _make_panel_style(Color(0.12, 0.13, 0.14, 0.96), Color(0.32, 0.36, 0.39, 1.0), 10)) - lobby_members_container.add_child(card) - - var row: HBoxContainer = HBoxContainer.new() - row.add_constant_override("separation", 12) - card.add_child(row) - - var avatar: PanelContainer = PanelContainer.new() - avatar.rect_min_size = Vector2(74, 74) - avatar.add_stylebox_override("panel", _make_panel_style(_avatar_color(int(member.get("steam_id", 0))), Color(0.72, 0.76, 0.8, 1.0), 4)) - row.add_child(avatar) - - var initials: Label = Label.new() - initials.text = _initials(str(member.get("name", "?"))) - initials.align = Label.ALIGN_CENTER - initials.valign = Label.VALIGN_CENTER - initials.rect_min_size = Vector2(66, 66) - initials.add_color_override("font_color", Color(1, 1, 1, 1)) - avatar.add_child(initials) - - var text_box: VBoxContainer = VBoxContainer.new() - text_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL - row.add_child(text_box) - - var name_label: Label = Label.new() - name_label.text = str(member.get("name", "Unknown")) - name_label.add_color_override("font_color", Color(0.95, 0.97, 1.0, 1.0)) - text_box.add_child(name_label) - - var id_label: Label = Label.new() - id_label.text = "Steam %s" % str(member.get("steam_id", 0)) - text_box.add_child(id_label) - - var role_label: Label = Label.new() - role_label.text = "Host" if int(member.get("steam_id", 0)) == _current_lobby_owner() else "Player" - text_box.add_child(role_label) - - var ready_label: Label = Label.new() - ready_label.text = _ready_text(int(member.get("steam_id", 0))) - ready_label.add_color_override("font_color", Color(0.45, 0.9, 0.56, 1.0) if _is_member_ready(int(member.get("steam_id", 0))) else Color(0.9, 0.62, 0.4, 1.0)) - text_box.add_child(ready_label) - - -func _add_empty_member_slot(_index: int) -> void: - var card: PanelContainer = PanelContainer.new() - card.rect_min_size = Vector2(390, 128) - card.add_stylebox_override("panel", _make_panel_style(Color(0.09, 0.095, 0.105, 0.72), Color(0.2, 0.22, 0.24, 1.0), 10)) - lobby_members_container.add_child(card) - - var label: Label = Label.new() - label.text = "Open slot" - label.align = Label.ALIGN_CENTER - label.valign = Label.VALIGN_CENTER - label.rect_min_size = Vector2(360, 96) - card.add_child(label) - - -func _current_lobby_owner() -> int: - if steam != null and lobby_id != 0 and steam.has_method("getLobbyOwner"): - return int(steam.call("getLobbyOwner", lobby_id)) - return steam_id if is_host else 0 - - -func _append_fake_peer_if_needed() -> void: - if not fake_peer_enabled: - return - for member in lobby_members: - if int(member.get("steam_id", 0)) == FAKE_PEER_ID: - return - lobby_members.append({"steam_id": FAKE_PEER_ID, "name": "Fake Test Peer", "fake": true}) - if not lobby_ready_states.has(FAKE_PEER_ID): - _set_member_ready(FAKE_PEER_ID, false) - - -func _remove_fake_peer() -> void: - for index in range(lobby_members.size() - 1, -1, -1): - var member: Dictionary = lobby_members[index] - if int(member.get("steam_id", 0)) == FAKE_PEER_ID: - lobby_members.remove(index) - lobby_ready_states.erase(FAKE_PEER_ID) - - -func _is_fake_peer(member_id: int) -> bool: - return member_id == FAKE_PEER_ID - - -func _set_member_ready(member_id: int, ready: bool) -> void: - lobby_ready_states[member_id] = ready - - -func _is_member_ready(member_id: int) -> bool: - return bool(lobby_ready_states.get(member_id, false)) - - -func _ready_text(member_id: int) -> String: - return "Ready" if _is_member_ready(member_id) else "Waiting" - - -func _all_members_ready() -> bool: - if lobby_members.empty(): - return false - for member in lobby_members: - if not _is_member_ready(int(member.get("steam_id", 0))): - return false - return true - - -func _lobby_ready_summary() -> String: - var ready_count := 0 - for member in lobby_members: - if _is_member_ready(int(member.get("steam_id", 0))): - ready_count += 1 - return "%s/%s ready" % [ready_count, lobby_members.size()] - - -func _update_lobby_controls() -> void: - if ready_button != null: - ready_button.text = "Unready" if _is_member_ready(steam_id) else "Ready" - if fake_peer_button != null: - fake_peer_button.text = "Remove Fake" if fake_peer_enabled else "Add Fake" - if fake_ready_button != null: - fake_ready_button.disabled = not fake_peer_enabled - fake_ready_button.text = "Fake Unready" if fake_peer_enabled and _is_member_ready(FAKE_PEER_ID) else "Fake Ready" - - -func _initials(display_name: String) -> String: - var clean := display_name.strip_edges() - if clean.empty(): - return "?" - var result := "" - var parts := clean.split(" ", false) - for part in parts: - if result.length() >= 2: - break - if str(part).length() > 0: - result += str(part).substr(0, 1).to_upper() - if result.empty(): - result = clean.substr(0, 1).to_upper() - return result - - -func _avatar_color(value: int) -> Color: - var colors: Array = [ - Color(0.25, 0.45, 0.82, 1.0), - Color(0.68, 0.35, 0.72, 1.0), - Color(0.25, 0.58, 0.48, 1.0), - Color(0.74, 0.44, 0.28, 1.0) - ] - var color_index: int = int(abs(value)) % colors.size() - return colors[color_index] - - -func _clear_children(parent: Node) -> void: - for child in parent.get_children(): - parent.remove_child(child) - child.free() - - -func _free_ui() -> void: - if panel != null and panel.visible: - get_tree().paused = restore_paused_value - restore_focus_control = null - if status_layer != null and is_instance_valid(status_layer): - if status_layer.get_parent() != null: - status_layer.get_parent().remove_child(status_layer) - status_layer.free() - status_layer = null - status_label = null - dim_background = null - panel = null - panel_status_label = null - panel_log_label = null - panel_default_focus = null - browser_view = null - lobby_view = null - lobby_list_container = null - lobby_empty_label = null - lobby_name_input = null - lobby_password_input = null - lobby_password_check = null - join_password_input = null - selected_lobby_label = null - lobby_members_container = null - lobby_title_label = null - lobby_detail_label = null - ready_button = null - fake_peer_button = null - fake_ready_button = null - - -func toggle_debug_panel() -> void: - if panel != null and panel.visible: - close_debug_panel() - else: - open_debug_panel() - - -func open_debug_panel() -> void: - if panel == null: - return - restore_paused_value = get_tree().paused - restore_focus_control = _find_focus_owner() - get_tree().paused = true - Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) - dim_background.visible = true - panel.visible = true - set_process_input(true) - if lobby_id != 0: - _show_lobby_view() - else: - _show_browser_view() - if panel_default_focus != null and is_instance_valid(panel_default_focus): - panel_default_focus.call_deferred("grab_focus") - _update_debug_panel("Panel opened") - - -func close_debug_panel() -> void: - if panel == null: - return - panel.visible = false - if dim_background != null: - dim_background.visible = false - get_tree().paused = restore_paused_value - if restore_focus_control != null and is_instance_valid(restore_focus_control): - restore_focus_control.call_deferred("grab_focus") - - -func _update_debug_panel(last_message: String = "") -> void: - if panel_status_label == null: - return - var lobby_text: String = "none" if lobby_id == 0 else str(lobby_id) - var role: String = "host" if is_host else "client" - panel_status_label.text = "Steam %s\nLobby %s\nRole %s | Members %s" % [steam_id, lobby_text, role, lobby_members.size()] - if panel_log_label != null and not last_message.empty(): - panel_log_label.text = last_message - - -func _show_status(message: String) -> void: - if status_label == null: - return - if panel == null or not panel.visible: - return - status_label.text = "Brogether: " + message - status_label.visible = true - status_time_left = 5.0 - - -func _find_focus_owner(): - var scene: Node = get_tree().current_scene - if scene != null and scene.has_method("get_focus_owner"): - return scene.get_focus_owner() - return null - - -func _build_run_state() -> Dictionary: - return { - "run_id": current_run_id, - "wave": RunData.current_wave, - "zone": RunData.current_zone, - "player_count": RunData.get_player_count(), - "is_coop_run": RunData.is_coop_run, - "is_host": is_host, - "lobby_id": str(lobby_id) - } - - -func _build_players_snapshot(players: Array) -> Array: - var snapshot := [] - for player in players: - if not is_instance_valid(player): - continue - var health := -1 - var max_health := -1 - if "current_stats" in player and player.current_stats != null: - health = int(player.current_stats.health) - if "max_stats" in player and player.max_stats != null: - max_health = int(player.max_stats.health) - snapshot.append({ - "player_index": int(player.player_index) if "player_index" in player else snapshot.size(), - "x": float(player.global_position.x), - "y": float(player.global_position.y), - "health": health, - "max_health": max_health, - "dead": bool(player.dead) if "dead" in player else false - }) - return snapshot - - -func _apply_remote_snapshot(sender: int, data: Dictionary) -> void: - if current_players.size() < 2: - _log("remote snapshot from %s ignored: no second local player exists" % sender) - return - var players: Array = data.get("players", []) - if players.empty(): - return - var remote_player: Dictionary = players[0] - var target_index: int = 1 - if target_index >= current_players.size() or not is_instance_valid(current_players[target_index]): - return - var target = current_players[target_index] - target.global_position = Vector2(float(remote_player.get("x", target.global_position.x)), float(remote_player.get("y", target.global_position.y))) - if "current_stats" in target and target.current_stats != null and remote_player.has("health"): - target.current_stats.health = int(remote_player.health) - _log("applied remote snapshot from %s to local player %s" % [sender, target_index]) + var options_node = BrotogetherOptions.new() + options_node.set_name("BrotogetherOptions") + $"/root".call_deferred("add_child",options_node) diff --git a/src/mods-unpacked/Brogether-Brogether/networking/connection.gd b/src/mods-unpacked/Brogether-Brogether/networking/connection.gd new file mode 100644 index 0000000..bdbe666 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/networking/connection.gd @@ -0,0 +1,27 @@ +extends Node + +# Connection interface + +func send_start_game(_game_info:Dictionary) -> void: + pass + +func send_display_floating_text(_text_info:Dictionary) -> void: + pass + +func send_display_hit_effect(_effect_info:Dictionary) -> void: + pass + +func send_enemy_death(_enemy_id:int) -> void: + pass + +func send_end_wave() -> void: + pass + +func send_flash_enemy(_enemy_id:int) -> void: + pass + +func send_flash_neutral(_neutral_id:int) -> void: + pass + +func send_client_position(_client_position: Dictionary) -> void: + pass diff --git a/src/mods-unpacked/Brogether-Brogether/networking/data_node.gd b/src/mods-unpacked/Brogether-Brogether/networking/data_node.gd new file mode 100644 index 0000000..1986b31 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/networking/data_node.gd @@ -0,0 +1,3 @@ +extends Node + +var network_id diff --git a/src/mods-unpacked/Brogether-Brogether/networking/direct_connection.gd b/src/mods-unpacked/Brogether-Brogether/networking/direct_connection.gd new file mode 100644 index 0000000..4721946 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/networking/direct_connection.gd @@ -0,0 +1,117 @@ +extends "res://mods-unpacked/Brogether-Brogether/networking/connection.gd" + +var parent + +func _ready(): + var _error = get_tree().connect("network_peer_connected", self, "_player_connected") + _error = get_tree().connect("connected_to_server", self, "_connected_ok") + _error = get_tree().connect("connection_failed", self, "_connected_fail") + _error = get_tree().connect("server_disconnected", self, "_server_disconnected") + +func _player_connected(_id): + rpc("register_player") + +func _connected_ok(): + var current_scene_name = get_tree().get_current_scene().get_name() + if current_scene_name == "MultiplayerMenu": + $"/root/MultiplayerMenu/HBoxContainer/InfoBox/Label".text = "connected" + parent.self_peer_id = get_tree().get_network_unique_id() + +func _server_disconnected(): + pass # Server kicked us; show error and abort. + +func _connected_fail(): + pass # Could not even connect to server; abort. + +remotesync func register_player(): + var id = get_tree().get_rpc_sender_id() + if parent.is_host and not parent.tracked_players.has(id): + parent.tracked_players[id] = {} + +func send_state(game_state:Dictionary) -> void: + rpc("update_game_state", game_state) + +remote func update_game_state(game_state:Dictionary) -> void: + parent.update_game_state(game_state) + +func send_start_game(game_info:Dictionary) -> void: + rpc("start_game", game_info) + +remote func start_game(game_info: Dictionary) -> void: + parent.start_game(game_info) + +func send_display_floating_text(text_info:Dictionary) -> void: + rpc("display_floating_text", text_info) + +remote func display_floating_text(text_info:Dictionary) -> void: + parent.display_floating_text(text_info) + +func send_display_hit_effect(effect_info: Dictionary) -> void: + rpc("display_hit_effect", effect_info) + +remote func display_hit_effect(effect_info: Dictionary) -> void: + parent.display_hit_effect(effect_info) + +func send_enemy_death(enemy_id:int) -> void: + rpc("enemy_death", enemy_id) + +remote func enemy_death(enemy_id:int) -> void: + parent.enemy_death(enemy_id) + +func send_end_wave() -> void: + rpc("end_wave") + +remote func end_wave() -> void: + parent.end_wave() + +func send_more_enemies() -> void: + rpc("receive_more_enemies") + +remote func receive_more_enemies() -> void: + parent.received_more_enemies(get_tree().get_rpc_sender_id()) + +func send_bought_item(shop_item:Resource) -> void: + rpc("receive_bought_item", shop_item.get_path()) + +remote func receive_bought_item(resource_path: String) -> void: + var sender_id = get_tree().get_rpc_sender_id() + var resource = load(resource_path) + parent.receive_bought_item(resource, sender_id) + +func send_flash_enemy(enemy_id:int) -> void: + rpc("flash_enemy", enemy_id) + +remote func flash_enemy(enemy_id:int) -> void: + parent.flash_enemy(enemy_id) + +func send_flash_neutral(neutral_id:int) -> void: + rpc("flash_neutral", neutral_id) + +remote func flash_neutral(neutral_id:int) -> void: + parent.flash_neutral(neutral_id) + +func send_client_position(client_position:Dictionary) -> void: + rpc("update_client_position", client_position) + +remote func update_client_position(client_position:Dictionary) -> void: + parent.update_client_position(client_position) + +func send_ready(is_ready:bool) -> void: + rpc("ready", is_ready) + +remote func ready(is_ready:bool) -> void: + var sender_id = get_tree().get_rpc_sender_id() + parent.update_ready_state(sender_id, is_ready) + +func send_tracked_players(tracked_players:Dictionary) -> void: + rpc("update_tracked_players", tracked_players) + +remote func update_tracked_players(tracked_players:Dictionary) -> void: + parent.update_tracked_players(tracked_players) + +func send_health_update(current_health:int, max_health:int) -> void: + rpc("health_update", current_health, max_health) + +remote func health_update(current_health:int, max_health:int) -> void: + var sender_id = get_tree().get_rpc_sender_id() + parent.receive_health_update(current_health, max_health, sender_id) diff --git a/src/mods-unpacked/Brogether-Brogether/networking/game_controller.gd b/src/mods-unpacked/Brogether-Brogether/networking/game_controller.gd new file mode 100644 index 0000000..a25d6dd --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/networking/game_controller.gd @@ -0,0 +1,946 @@ +extends Node + +# here they'll be keyed by steam user ids +var tracked_players = {} +var connection + +# tracked player info to be used in the multiplayer lobby +var lobby_data = {} + +# The id of this player, in direct ip connections this will be 1 for the host. +# in steam connections this will be a steam id against which a username can +# be queried +var self_peer_id + +# True iff the user hosted the lobby +var is_host = false + +var is_source_of_truth = true +var game_mode = GameMode.VERSUS + +enum GameMode { + VERSUS, + COOP, +} + +# A counter user to assign ids for game components +var id_count = 0 + +var GameStateController = load("res://mods-unpacked/Brogether-Brogether/networking/game_state_controller.gd") + +const toggle_scene = preload("res://mods-unpacked/Brogether-Brogether/ui/toggle.tscn") +const button_scene = preload("res://mods-unpacked/Brogether-Brogether/ui/button.tscn") +const explosion_scene = preload("res://projectiles/explosion.tscn") + +const MOD_NAME = "Brogether-Brogether" +const CONFIG_FILENAME = "user://Brogether-Brogether-options.cfg" +const CONFIG_SECTION = "latest-mp-lobby" + +var current_scene_name = "" +var run_updates = false +var disable_pause = false +var back_to_lobby = false +var all_players_ready = true + +var batched_deaths = [] +var batched_enemy_damage = [] +var batched_flash_enemy = [] +var batched_floating_text = [] +var batched_hit_effects = [] + +var ready_toggle + +var extra_enemies_next_wave = {} +var effects_next_wave = {} +var game_state_controller + +var waiting_for_client_starts = false + +signal complete_player_update +signal lobby_info_updated +signal danger_selected(danger_level) +signal character_selected(character_item_data) +signal weapon_selected(weapon_data) + + +func _init(): + game_state_controller = GameStateController.new() + game_state_controller.parent = self + add_child(game_state_controller) + + +func _ready(): + init_lobby_info() + + +func _process(_delta): + var scene_name = get_tree().get_current_scene().get_name() + # TODO: NOT THIS + if scene_name != current_scene_name: + if scene_name == "Shop": + enter_async_shop() + + current_scene_name = scene_name + +func init_client_starts_wait() -> void: + waiting_for_client_starts = true + for player in tracked_players: + if not player != self_peer_id: + tracked_players[player].in_game = false + receive_client_start(self_peer_id) + + +func init_lobby_info() -> void: + lobby_data = {} + lobby_data["players"] = {} + + lobby_data["game_mode"] = 0 + lobby_data["copy_host"] = false + lobby_data["first_death_loss"] = false + lobby_data["shared_gold"] = false + lobby_data["material_count"] = 1 + lobby_data["enemy_count"] = 1 + lobby_data["enemy_hp"] = 1 + lobby_data["enemy_damage"] = 1 + lobby_data["enemy_speed"] = 1 + load_config() + + +func save_config() -> void: + var config := ConfigFile.new() + + for key in lobby_data: + if key == "players": + continue + config.set_value(CONFIG_SECTION, key, lobby_data[key]) + + var _unused_save_result = config.save(CONFIG_FILENAME) + + +func load_config() -> void: + var config := ConfigFile.new() + var err = config.load(CONFIG_FILENAME) + + if err != OK: + return + + for key in config.get_section_keys(CONFIG_SECTION): + lobby_data[key] = config.get_value(CONFIG_SECTION, key) + + +func send_client_started() -> void: + connection.send_client_started() + + +func receive_client_start(player_id) -> void: + if not is_host: + return + + if not waiting_for_client_starts: + return + + tracked_players[player_id].in_game = true + + var done_waiting = true + for player in tracked_players: + if not player != self_peer_id: + if not tracked_players[player].in_game: + done_waiting = false + break + + if done_waiting: + waiting_for_client_starts = false + $"/root/Main"._wave_timer.set_paused(false) + $"/root/Main".send_updates = true + +func enter_async_shop() -> void: + if $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer": + $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer".update_bought_items(tracked_players) + + if is_host: + init_shop_go_button() + + else: + $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/GoButton".hide() + $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2".add_child(create_ready_toggle()) + +func create_extra_creatures_map() -> Dictionary: + var extra_creatures_map = {} + + for player_id in tracked_players: + extra_creatures_map[player_id] = tracked_players[player_id]["extra_enemies_next_wave"] + return extra_creatures_map + +func create_effects_map() -> Dictionary: + var effects_map = {} + + for player_id in tracked_players: + effects_map[player_id] = tracked_players[player_id]["effects"] + + return effects_map + +func init_shop_go_button() -> void: + var shop = get_tree().get_current_scene() + var button = $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/GoButton" + + button.disconnect("pressed", shop, "_on_GoButton_pressed") + button.connect("pressed", self, "_on_GoButton_pressed") + + update_go_button() + +func _on_GoButton_pressed()-> void: + var shop = get_tree().get_current_scene() + if shop._go_button_pressed: + return + + shop._go_button_pressed = true + + extra_enemies_next_wave = create_extra_creatures_map() + effects_next_wave = create_effects_map() + + RunData.current_wave += 1 + MusicManager.tween(0) + + var wave_data = {"current_wave":RunData.current_wave, "mode":game_mode} + var extra_creatures_map = extra_enemies_next_wave + + wave_data["extra_enemies_next_wave"] = extra_creatures_map + wave_data["effects"] = effects_next_wave + + send_start_game(wave_data) + reset_extra_creatures() + reset_ready_map() + +# RunData.effects["extra_enemies_next_wave"] = tracked_players[self_peer_id]["extra_enemies_next_wave"] + + var _error = get_tree().change_scene(MenuData.game_scene) + +func init_all_player_data(): + for player_id in tracked_players: + init_player_data(tracked_players[player_id], player_id) + +func init_player_data(player:Dictionary, player_id) -> void: + var run_data = {} + + # Randomly useful in places + run_data["player_id"] = player_id + + run_data["effects"] = RunData.init_effects() + + run_data["items"] = [] + run_data["weapons"] = [] + run_data["additional_weapon_effects"] = [] + run_data["active_set_effects"] = [] + run_data["gold"] = DebugService.starting_gold + run_data["current_xp"] = 0 + run_data["current_level"] = 0 + + run_data["tier_i_weapon_effects"] = [] + run_data["tier_iv_weapon_effects"] = [] + + run_data["unique_effects"] = [] + run_data["appearances_displayed"] = [] + run_data["tracked_item_effects"] = RunData.init_tracked_effects() + + player["linked_stats"] = {} + player["consumables_to_process"] = [] + player["linked_stats"]["stats"] = RunData.init_stats(true) + player["linked_stats"]["update_on_gold_chance"] = false + + player["temp_stats"] = {} + player["temp_stats"]["stats"] = RunData.init_stats(true) + player["run_data"] = run_data + +func start_game(_game_info: Dictionary): + print_debug("game controller game would have started here but we had to remove all the logic") + + +func send_death() -> void: + disable_pause = false + run_updates = false + if is_host: + receive_death(self_peer_id) + + connection.send_death() + +func receive_death(source_player_id:int) -> void: + tracked_players[source_player_id]["dead"] = true + if is_host: + connection.send_tracked_players(tracked_players) + if source_player_id != self_peer_id: + check_win() + +func check_win() -> void: + var all_others_dead = true + var anyone_dead = false + var all_dead = true + for tracked_player_id in tracked_players: + if tracked_player_id == self_peer_id: + continue + var tracked_player = tracked_players[tracked_player_id] + if not tracked_player.has("dead") or not tracked_player.dead: + all_others_dead = false + all_dead = false + else: + anyone_dead = true + + # TODO this doesn't actually work yet + var should_end_coop = false + if lobby_data["first_death_loss"]: + should_end_coop = anyone_dead + else: + should_end_coop = all_dead + + if all_others_dead or (should_end_coop): + disable_pause = false + var main = get_tree().get_current_scene() + var did_win = not is_coop() + main._is_run_won = did_win + main._is_run_lost = not did_win + RunData.run_won = did_win + + main.clean_up_room(false, main._is_run_lost, main._is_run_won) + + if not is_host: + send_complete_player_request() + yield(self, "complete_player_update") + var _error = get_tree().change_scene("res://ui/menus/run/end_run.tscn") + +func on_consumable_to_process_added(player_id, consumable_data) -> void: + if player_id == self_peer_id: + receive_add_consumable_to_process(player_id, consumable_data.get_path()) + else: + connection.send_add_consumable_to_process(player_id, consumable_data.get_path()) + +func receive_add_consumable_to_process(player_id, consumable_data_path) -> void: + if player_id == self_peer_id: + var consumable_data = load(consumable_data_path) + tracked_players[self_peer_id].consumables_to_process.push_back(consumable_data) + get_tree().get_current_scene().emit_signal("consumable_to_process_added", consumable_data) + +func on_upgrade_selected(upgrade_data:UpgradeData)->void : + if is_host: + receive_uprade_selected(upgrade_data.my_id, self_peer_id) + else: + connection.send_upgrade_selection(upgrade_data.my_id) + +func send_complete_player_request() -> void: + connection.send_complete_player_request() + +func send_complete_player(player_id:int) -> void: + if not is_host: + return + + var player_dict = tracked_players[player_id].duplicate() + if not player_dict.has("run_data"): + player_dict.run_data = {} + player_dict.run_data = player_dict.run_data.duplicate() + player_dict.run_data.effects = player_dict.run_data.effects.duplicate() + + var new_items = [] + + if player_dict.run_data.has("items"): + for item in player_dict.run_data.items: + var to_add = {} + to_add["my_id"] = item.my_id + new_items.push_back(to_add) + player_dict.run_data["items"] = new_items + + if player_dict.run_data.has("current_character"): + player_dict.run_data.current_character = player_dict.run_data.current_character.my_id + + var new_weapons = [] + if player_dict.run_data.has("weapons"): + for weapon in player_dict.run_data.weapons: + var to_add = {} + to_add["my_id"] = weapon.my_id + new_weapons.push_back(to_add) + player_dict.run_data["weapons"] = new_weapons + + var burn_chance = {} + + if player_dict.run_data.has("effects"): + burn_chance.chance = player_dict.run_data.effects.burn_chance.chance + burn_chance.damage = player_dict.run_data.effects.burn_chance.damage + burn_chance.duration = player_dict.run_data.effects.burn_chance.duration + player_dict.run_data.effects.burn_chance = burn_chance + + var elites = [] + for elite in RunData.elites_spawn: + elites.push_back([elite[0], elite[1]]) + player_dict["elites"] = elites + + connection.send_complete_player(player_id, player_dict) + +func receive_complete_player(player_id:int, player_dict: Dictionary) -> void: + if player_id == self_peer_id: + tracked_players[player_id]["run_data"] = player_dict.run_data + + var new_items = [] + + for item in player_dict.run_data.items: + var found : bool = false + + for query_item in ItemService.items: + if query_item.my_id == item.my_id: + new_items.push_back(query_item.duplicate()) + found = true + break + if not found: + for query_item in ItemService.characters: + if query_item.my_id == item.my_id: + new_items.push_back(query_item.duplicate()) + found = true + break + tracked_players[player_id]["run_data"]["items"] = new_items + + for query_character in ItemService.characters: + if player_dict.run_data.has("current_character"): + if query_character.my_id == player_dict.run_data.current_character: + player_dict.run_data.current_character = query_character + break + + var new_weapons = [] + + for weapon in player_dict.run_data.weapons: + for query_weapon in ItemService.weapons: + if query_weapon.my_id == weapon.my_id: + new_weapons.push_back(query_weapon.duplicate()) + tracked_players[player_id]["run_data"]["weapons"] = new_weapons + + if tracked_players[player_id]["run_data"].has("gold"): + RunData.emit_signal("gold_changed", tracked_players[player_id]["run_data"].gold) + var elites_spawn = [] + for elite in player_dict["elites"]: + elites_spawn.push_back([elite[0], elite[1], "some_boss_id"]) + RunData.elites_spawn = elites_spawn + emit_signal("complete_player_update") + +func on_item_box_take_button_pressed(item_data:ItemParentData) -> void: + if is_host: + receive_item_box_take(item_data.my_id, self_peer_id) + else: + connection.send_take_item_box(self_peer_id, item_data.my_id) + +func receive_item_box_take(item_id, player_id) -> void: + var run_data_node = $"/root/MultiplayerRunData" + + for item in ItemService.items: + if item.my_id == item_id: + run_data_node.add_item(player_id, item) + return + +func reroll_upgrades() -> void: + var upgrades_ui = get_tree().get_current_scene()._upgrades_ui + var reroll_price = upgrades_ui._reroll_price + if is_host: + receive_reroll_upgrades(self_peer_id, reroll_price) + else: + connection.send_reroll_upgrades(reroll_price) + +func receive_reroll_upgrades(_player_id:int, _reroll_price:int) -> void: + print_debug("receive_reroll_upgrades") + + +func on_item_discard_button_pressed(weapon_data:WeaponData) -> void: + if is_host: + receive_item_discard(weapon_data.my_id, self_peer_id) + else: + connection.send_weapon_discard(weapon_data.my_id) + +func receive_item_discard(weapon_id, player_id) -> void: + var run_data_node = $"/root/MultiplayerRunData" + + var run_data = tracked_players[player_id].run_data + var weapon_data = null + + for weapon in run_data.weapons: + if weapon.my_id == weapon_id: + weapon_data = weapon + + var _weapon = run_data_node.remove_weapon(player_id, weapon_data) + run_data_node.add_gold(player_id, ItemService.get_recycling_value(RunData.current_wave, weapon_data.value, true)) + + if player_id != self_peer_id: + send_complete_player(player_id) + +func receive_player_enter_shop(player_id:int) -> void: + var run_data = tracked_players[player_id].run_data + var run_data_node = $"/root/MultiplayerRunData" + + if run_data.effects["destroy_weapons"]: + run_data_node.remove_all_weapons(player_id) + + if run_data.effects["upgrade_random_weapon"].size() > 0: + for effect in run_data.effects["upgrade_random_weapon"]: + + var possible_upgrades = [] + + for weapon in run_data.weapons: + if weapon.upgrades_into != null and weapon.tier < run_data.effects["max_weapon_tier"]: + possible_upgrades.push_back(weapon) + + if possible_upgrades.size() > 0: + var weapon_to_upgrade = Utils.get_rand_element(possible_upgrades) + receive_item_combine(weapon_to_upgrade.my_id, true, player_id) + else : + run_data_node.add_stat(player_id, effect[0], effect[1]) + + if player_id != self_peer_id: + send_complete_player(player_id) + +func on_item_combine_button_pressed(weapon_data:WeaponData, is_upgrade:bool = false) -> void: + if is_host: + receive_item_combine(weapon_data.my_id, is_upgrade, self_peer_id) + else: + connection.send_combine_item(weapon_data.my_id, is_upgrade, self_peer_id) + +func receive_item_combine(weapon_id, is_upgrade, player_id) -> void: + var run_data_node = $"/root/MultiplayerRunData" + + var nb_to_remove = 2 + var removed_weapons_tracked_value = 0 + + if is_upgrade: + nb_to_remove = 1 + + var run_data = tracked_players[player_id].run_data + var weapon_data = null + + for weapon in run_data.weapons: + if weapon.my_id == weapon_id: + weapon_data = weapon + + removed_weapons_tracked_value += run_data_node.remove_weapon(player_id, weapon_data) + + if not is_upgrade: + removed_weapons_tracked_value += run_data_node.remove_weapon(player_id, weapon_data) + + var new_weapon = run_data_node.add_weapon(player_id, weapon_data.upgrades_into) + + new_weapon.tracked_value = removed_weapons_tracked_value + + if is_upgrade: + new_weapon.dmg_dealt_last_wave = weapon_data.dmg_dealt_last_wave + + if player_id == self_peer_id: + var shop = $"/root/Shop" + shop._weapons_container._elements.remove_element(weapon_data, nb_to_remove) + shop.reset_item_popup_focus() + shop._stats_container.update_stats() + shop._weapons_container._elements.add_element(new_weapon) + SoundManager.play(Utils.get_rand_element(shop.combine_sounds), 0, 0.1, true) + shop._weapons_container.set_label(shop.get_weapons_label_text()) + if Input.get_mouse_mode() == Input.MOUSE_MODE_HIDDEN: + shop._weapons_container._elements.focus_element(new_weapon) + +func receive_uprade_selected(upgrade_data_id, player_id): + for upgrade in ItemService.upgrades: + if upgrade.my_id == upgrade_data_id: + var run_data = tracked_players[player_id].run_data + var run_data_node = $"/root/MultiplayerRunData" + run_data_node.apply_item_effects(player_id, upgrade, run_data) + +# Send normal item +func send_bought_item_by_id(item_id:String, value:int) -> void: + if is_host: + receive_bought_item_by_id(item_id, self_peer_id, value) + else: + connection.send_bought_item_by_id(item_id, value) + +func receive_bought_item_by_id(item_id:String, player_id:int, value:int) -> void: + var run_data_node = $"/root/MultiplayerRunData" + var run_data = tracked_players[player_id].run_data + run_data_node.remove_currency(player_id, value) + var nb_coupons = run_data_node.get_nb_item(player_id, "item_coupon") + + var shop_item_data = null + + for weapon in ItemService.weapons: + if weapon.my_id == item_id: + shop_item_data = weapon + for item in ItemService.items: + if item.my_id == item_id: + shop_item_data = item + + if nb_coupons > 0: +# var coupon_value = get_coupon_value() reimplemented in place + var coupon_value = 0 + for item in run_data.items: + if item.my_id == "item_coupon": + coupon_value = abs(item.effects[0].value) + break + + var coupon_effect = nb_coupons * (coupon_value / 100.0) + var base_value = ItemService.get_value(RunData.current_wave, shop_item_data.value, false, shop_item_data is WeaponData, shop_item_data.my_id) + run_data.tracked_item_effects["item_coupon"] += (base_value * coupon_effect) as int + + if shop_item_data.get_category() == Category.ITEM: + run_data_node.add_item(player_id, shop_item_data) + elif shop_item_data.get_category() == Category.WEAPON: + if not run_data_node.has_weapon_slot_available(player_id, shop_item_data.type): + for weapon in run_data.weapons: + if weapon.my_id == shop_item_data.my_id and weapon.upgrades_into != null: + var _weapon = run_data_node.add_weapon(player_id, shop_item_data) + receive_item_combine(weapon.my_id, false, player_id) + break + else : + var _weapon = run_data_node.add_weapon(player_id, shop_item_data) + + +func send_reroll(price) -> void: + if is_host: + receive_reroll(price, self_peer_id) + else: + connection.send_reroll(price) + + +func receive_reroll(price, player_id): + var run_data = tracked_players[player_id].run_data + run_data.gold -= price + + +func send_bought_item(shop_item:Resource) -> void: + if is_host: + receive_bought_item(shop_item, self_peer_id) + else: + connection.send_bought_item(shop_item) + +func send_client_entered_shop() -> void: + connection.send_client_entered_shop() + +func receive_bought_item(shop_item:Resource, source_player_id:int) -> void: + var effect_path = shop_item.effect.get_path() + for player_id in tracked_players: + if player_id != source_player_id: + var effect = shop_item.effect + if effect is WaveGroupData: + if not tracked_players[player_id]["extra_enemies_next_wave"].has(effect_path): + tracked_players[player_id]["extra_enemies_next_wave"][effect_path] = 0 + tracked_players[player_id]["extra_enemies_next_wave"][effect_path] = tracked_players[player_id]["extra_enemies_next_wave"][effect_path] + 1 + elif effect is Effect: + if not tracked_players[player_id]["effects"].has(effect_path): + tracked_players[player_id]["effects"][effect_path] = 0 + tracked_players[player_id]["effects"][effect_path] = tracked_players[player_id]["effects"][effect_path] + 1 + + if is_host: + connection.send_tracked_players(tracked_players) + if $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer": + $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer".update_bought_items(tracked_players) + +func update_tracked_players(updated_tracked_players: Dictionary) -> void: + # Actual Player state is updated elsewhere + for player_id in updated_tracked_players: + if tracked_players.has(player_id) and tracked_players[player_id].has("player"): + updated_tracked_players[player_id]["player"] = tracked_players[player_id]["player"] + else: + updated_tracked_players[player_id].erase("player") + + var scene_name = get_tree().get_current_scene().get_name() + + tracked_players = updated_tracked_players + if scene_name == "Shop": + $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer".update_bought_items(tracked_players) + elif scene_name == "Main": + check_win() + +func create_ready_toggle() -> Node: + ready_toggle = toggle_scene.instance() + ready_toggle.connect("pressed", self, "_on_ready_toggle") + return ready_toggle + +func _on_ready_toggle() -> void: + connection.send_ready(ready_toggle.pressed) + +func discard_item_box(item_data:ItemParentData) -> void: + if is_host: + receive_discard_item_box(self_peer_id, item_data.my_id) + else: + connection.send_discard_item_box(item_data.my_id) + +func receive_discard_item_box(_player_id:int, _item_id:String) -> void: + print_debug("receive_discard_item_box") + + +func end_wave(): + run_updates = false + game_state_controller.reset_client_items() + + if current_scene_name == "ClientMain": + var wave_timer = $"/root/ClientMain"._wave_timer + wave_timer.wait_time = 0.1 + wave_timer.one_shot = true + wave_timer.start() + + var end_wave_timer = $"/root/ClientMain"._end_wave_timer + end_wave_timer.one_shot = true + end_wave_timer.start() + + for player_id in tracked_players: + tracked_players[player_id].erase("player") + +func send_ready(is_ready:bool) -> void: + connection.send_ready(is_ready) + +func send_game_state() -> void: + if run_updates: + connection.send_state(game_state_controller.get_game_state()) + +func send_start_game(game_info:Dictionary) -> void: + connection.send_start_game(game_info) + +func send_display_floating_text(value:String, text_pos:Vector2, color:Color = Color.white) -> void: + batched_floating_text.push_back([value,text_pos, color]) + +func send_display_hit_effect(effect_pos, direction, effect_scale) -> void: + batched_hit_effects.push_back([effect_pos, direction, effect_scale]) + +func send_enemy_death(enemy_id:int) -> void: + batched_deaths.push_back(enemy_id) +# connection.send_enemy_death(enemy_id) + +func send_end_wave() -> void: + connection.send_end_wave() + +func send_flash_enemy(enemy_id:int) -> void: + batched_flash_enemy.push_back(enemy_id) + +func send_shot(player_id:int, weapon_index:int) -> void: + connection.send_shot(player_id, weapon_index) + +func receive_shot(player_id:int, weapon_index:int) -> void: + if tracked_players.has(player_id): + if tracked_players[player_id].has("player"): + var player = tracked_players[player_id]["player"] + + if is_instance_valid(player): + var weapon = player.current_weapons[weapon_index] + if is_instance_valid(weapon): + SoundManager.play(Utils.get_rand_element(weapon.current_stats.shooting_sounds), weapon.current_stats.sound_db_mod, 0.2) + +func send_explosion(pos: Vector2, scale: float) -> void: + connection.send_explosion(pos, scale) + +func receive_explosion(pos: Vector2, scale: float) -> void: + var main = get_tree().current_scene + var instance = explosion_scene.instance() + instance.set_deferred("global_position", pos) + main.call_deferred("add_child", instance) + instance.call_deferred("set_area", scale) + +func send_enemy_take_damage(enemy_id:int, is_dodge: bool) -> void: + batched_enemy_damage.push_back([enemy_id, is_dodge]) + +func send_flash_neutral(neutral_id:int) -> void: + connection.send_flash_neutral(neutral_id) + +func send_lobby_update(lobby_info:Dictionary) -> void: + connection.send_lobby_update(lobby_info) + +func receive_lobby_update(lobby_info:Dictionary) -> void: + if current_scene_name == "MultiplayerLobby": + lobby_data = lobby_info.duplicate() + $"/root/MultiplayerLobby".remote_update_lobby(lobby_info) + +func update_multiplayer_lobby() -> void: + if current_scene_name == "MultiplayerLobby": + $"/root/MultiplayerLobby".update_selections() + +func send_client_position() -> void: + if not tracked_players.has(self_peer_id) or not tracked_players[self_peer_id].has("player"): + return + var my_player = tracked_players[self_peer_id]["player"] + if not is_instance_valid(my_player): + return + var client_position = {} + client_position["player"] = my_player.position + client_position["id"] = self_peer_id + client_position["movement"] = my_player._current_movement + var weapons = [] + for weapon in my_player.current_weapons: + var weapon_data = {} + weapon_data["weapon_id"] = weapon.weapon_id + weapon_data["position"] = weapon.sprite.position + weapon_data["rotation"] = weapon.sprite.rotation + weapon_data["hitbox_disabled"] = weapon._hitbox._collision.disabled + weapons.push_back(weapon_data) + client_position["weapons"] = weapons + + connection.send_client_position(client_position) + +func update_client_position(client_position:Dictionary) -> void: + if is_source_of_truth: + var id = client_position.id + if tracked_players.has(id): + if tracked_players[id].has("player"): + var player = tracked_players[id]["player"] + if not is_instance_valid(player): + return + player.position = client_position.player + player.maybe_update_animation(client_position.movement, true) + +func update_ready_state(sender_id, is_ready): + if is_host: + tracked_players[sender_id]["is_ready"] = is_ready + if current_scene_name == "Shop": + update_go_button() + +func reset_ready_map(): + for player_id in tracked_players: + tracked_players[player_id]["is_ready"] = false + +func reset_extra_creatures(): + for player_id in tracked_players: + tracked_players[player_id]["extra_enemies_next_wave"] = {} + tracked_players[player_id]["effects"] = {} + +func update_go_button(): + var should_enable = true + + for player_id in tracked_players: + if player_id != self_peer_id and not tracked_players[player_id]["is_ready"]: + should_enable = false + break + + var shop_button = $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/GoButton" + + if not should_enable: + shop_button.disabled = true + else: + shop_button.disabled = false + +func get_items_state() -> Dictionary: + var main = $"/root/Main" + var items = [] + for item in main._items_container.get_children(): + var item_data = {} + + item_data["id"] = item.id + item_data["scale_x"] = item.scale.x + item_data["scale_y"] = item.scale.y + item_data["position"] = item.global_position + item_data["rotation"] = item.rotation + item_data["push_back_destination"] = item.push_back_destination + + # TODO we may want textures propagated + items.push_back(item_data) + return items + +func get_projectiles_state() -> Dictionary: + var main = $"/root/Main" + var projectiles = [] + for child in main.get_children(): + if child is PlayerProjectile: + var projectile_data = {} + projectile_data["id"] = child.id + projectile_data["filename"] = child.filename + projectile_data["position"] = child.position + projectile_data["global_position"] = child.global_position + projectile_data["rotation"] = child.rotation + + projectiles.push_back(projectile_data) + return projectiles + +func send_player_level_up(player_id: int, level:int) -> void: + connection.send_player_level_up(player_id, level) + +func receive_player_level_up(player_id: int, level:int) -> void: + if player_id == self_peer_id: + var main = $"/root/ClientMain" + main._ui_upgrades_to_process.add_element(ItemService.upgrade_to_process_icon, level) + main._upgrades_to_process.push_back(level) + +func receive_health_update(current_health:int, max_health:int, source_player_id:int) -> void: + tracked_players[source_player_id]["max_health"] = max_health + tracked_players[source_player_id]["current_health"] = current_health + +# if is_host: +# connection.send_tracked_players(tracked_players) +# update_health_ui() + +func update_health_ui() -> void: + if current_scene_name == "Main": + if $"/root/Main/UI/HealthTracker": + $"/root/Main/UI/HealthTracker".update_health_bars(tracked_players) + if current_scene_name == "ClientMain": + if $"/root/ClientMain/UI/HealthTracker": + $"/root/ClientMain/UI/HealthTracker".update_health_bars(tracked_players) + +func update_health(current_health:int, max_health:int) -> void: + # If this player owns all the players, all the updates will come through + # here. + if is_coop() and is_host: + for player_id in tracked_players: + var player_dict = tracked_players[player_id] + if player_dict.has("player") and is_instance_valid(player_dict.player): + var player = player_dict.player + + tracked_players[player_id]["max_health"] = player.max_stats.health + tracked_players[player_id]["current_health"] = player.current_stats.health + + receive_health_update(current_health, max_health, self_peer_id) + connection.send_health_update(current_health, max_health) + +func update_game_state(data: PoolByteArray) -> void: + if get_tree().get_current_scene().get_name() != "ClientMain": + return + game_state_controller.update_game_state(data) + update_health_ui() + +func enemy_death(enemy_id): + game_state_controller.enemy_death(enemy_id) + +func flash_neutral(neutral_id): + game_state_controller.flash_neutral(neutral_id) + +func is_coop() -> bool: + return game_mode == GameMode.COOP + + +func on_danger_selected(danger) -> void: + if is_host: + received_danger_selected(self_peer_id, danger) + else: + connection.send_danger_selected(danger) + + +func received_danger_selected(player_id, danger) -> void: + lobby_data["players"][player_id]["danger"] = danger + emit_signal("lobby_info_updated") + + +func on_character_selected(character) -> void: + if is_host: + received_character_selected(self_peer_id, character) + else: + connection.send_character_selected(character) + + +func received_character_selected(player_id, character) -> void: + lobby_data["players"][player_id]["character"] = character + lobby_data["players"][player_id].erase("weapon") + emit_signal("lobby_info_updated") + + +func on_weapon_selected(weapon) -> void: + if is_host: + received_weapon_selected(self_peer_id, weapon) + else: + connection.send_weapon_selected(weapon) + + +func received_weapon_selected(player_id, weapon) -> void: + lobby_data["players"][player_id]["weapon"] = weapon + emit_signal("lobby_info_updated") + + +func on_mp_lobby_ready_changed(is_ready:bool) -> void: + if is_host: + receive_mp_lobby_ready_changed(self_peer_id, is_ready) + else: + connection.send_mp_lobby_readied(is_ready) + + +func receive_mp_lobby_ready_changed(player_id, is_ready:bool) -> void: + lobby_data["players"][player_id]["ready"] = is_ready + emit_signal("lobby_info_updated") diff --git a/src/mods-unpacked/Brogether-Brogether/networking/game_state_controller.gd b/src/mods-unpacked/Brogether-Brogether/networking/game_state_controller.gd new file mode 100644 index 0000000..69b5463 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/networking/game_state_controller.gd @@ -0,0 +1,1268 @@ +extends Node + +var client_enemies = {} +var client_births = {} +var client_players = {} +var client_items = {} +var client_player_projectiles = {} +var client_consumables = {} +var client_neutrals = {} +var client_structures = {} +var client_enemy_projectiles = {} + +var parent +var run_updates = false + +const gold_scene = preload("res://items/materials/gold.tscn") +const entity_birth_scene = preload("res://entities/birth/entity_birth.tscn") +const consumable_scene = preload("res://items/consumables/consumable.tscn") +const consumable_texture = preload("res://items/consumables/fruit/fruit.png") +const player_scene = preload("res://entities/units/player/player.tscn") + +#TODO this is the sussiest of bakas +var weapon_stats_resource = ResourceLoader.load("res://weapons/ranged/pistol/1/pistol_stats.tres") +var turret_stats_resource = ResourceLoader.load("res://entities/structures/turret/turret_stats.tres") + +# TODO all neutrals are going to be trees for now +const tree_scene = preload("res://entities/units/neutral/tree.tscn") + +var ClientMovementBehavior = load("res://mods-unpacked/Brogether-Brogether/client/client_movement_behavior.gd") +var ClientAttackBehavior = load("res://mods-unpacked/Brogether-Brogether/client/client_attack_behavior.gd") + +# TODO sometimes clear these +var sent_detail_ids = {} + +const refresh_time = 1.0 / 30.0 +var update_timer = refresh_time + +func _physics_process(delta): + if not $"/root".has_node("GameController") or not $"/root/GameController".is_coop(): + return + + var game_controller = $"/root/GameController" + update_timer -= delta + var scene_name = get_tree().get_current_scene().get_name() + + if update_timer <= 0: + update_timer = refresh_time + if game_controller.is_host: + if scene_name == "Main": + if get_tree().get_current_scene().send_updates: + game_controller.call_deferred("send_game_state") + + while read_p2p_packet(): + pass + +func read_p2p_packet() -> bool: + var packet_size = Steam.getAvailableP2PPacketSize(1) + + if packet_size > 0: + var packet = Steam.readP2PPacket(packet_size, 1) + + var data = bytes2var(packet["data"].decompress_dynamic(-1, File.COMPRESSION_GZIP)) + var type = data.type + + if type == "game_state": + parent.update_game_state(data.game_state) + return true + return false + +func get_game_state() -> PoolByteArray: + var buffer = StreamPeerBuffer.new() + + if "/root/Main": + var main = $"/root/Main" + + if main: + get_players_state(buffer) + get_enemies_state(buffer) + get_births_state(buffer) + get_items_state(buffer) + get_projectiles_state(buffer) + get_consumables_state(buffer) + get_neutrals_state(buffer) + get_structures_state(buffer) + get_enemy_projectiles(buffer) + get_deaths(buffer) + get_enemy_damages(buffer) + get_enemy_flashes(buffer) + get_batched_floating_text(buffer) + get_hit_effects(buffer) + + buffer.put_float(main._wave_timer.time_left) + buffer.put_32(RunData.bonus_gold) + + return buffer.data_array + +func update_game_state(data: PoolByteArray) -> void: + var main = get_tree().get_current_scene() + if main.get_name() != "ClientMain": + return + + var buffer = StreamPeerBuffer.new() + buffer.data_array = data + + update_players(buffer) + + var enemies_arrays = get_enemies_arrays(buffer) + call_deferred("update_enemies", enemies_arrays) + + update_births(buffer) + update_items(buffer) + + var player_projectiles_array = get_player_projectiles_array(buffer) + call_deferred("update_player_projectiles", player_projectiles_array) + + update_consumables(buffer) + update_neutrals(buffer) + update_structures(buffer) + update_enemy_projectiles(buffer) + + var batched_deaths = get_batched_deaths(buffer) + var batched_damages = get_batched_damages(buffer) + var batched_flashes = get_batched_flashes(buffer) + var batched_floating_text = get_batched_text_array(buffer) + var batched_hit_effects = get_batch_hit_effects_array(buffer) + + call_deferred("do_batched_deaths", batched_deaths) + call_deferred("do_batched_damages", batched_damages) + call_deferred("do_batched_flashes", batched_flashes) + call_deferred("do_batched_floating_text", batched_floating_text) + call_deferred("do_batched_hit_effects", batched_hit_effects) + + var _time = buffer.get_float() + main._wave_timer.start(_time) + + var bonus_gold = buffer.get_32() + if bonus_gold > 0: + main._ui_bonus_gold.show() + main._ui_bonus_gold.update_value(bonus_gold) + else: + main._ui_bonus_gold.hide() + +func get_enemy_projectiles(buffer: StreamPeerBuffer) -> void: + var projectiles_container = $"/root/Main/Projectiles" + var main = $"/root/Main" + var entity_spawner = main._entity_spawner + + var num_projectiles = projectiles_container.get_children().size() + + for enemy in entity_spawner.enemies: + if enemy.has_node("Pivot"): + for projectile in enemy.get_node("Pivot").get_children(): + if projectile is EnemyProjectile: + num_projectiles += 1 + + for boss in entity_spawner.bosses: + if boss.has_node("Pivot"): + for projectile in boss.get_node("Pivot").get_children(): + if projectile is EnemyProjectile: + num_projectiles += 1 + + buffer.put_u16(num_projectiles) + + for projectile in projectiles_container.get_children(): + encode_projectile(buffer, projectile) + + for enemy in entity_spawner.enemies: + if enemy.has_node("Pivot"): + for projectile in enemy.get_node("Pivot").get_children(): + if projectile is EnemyProjectile: + encode_projectile(buffer, projectile) + + for boss in entity_spawner.bosses: + if boss.has_node("Pivot"): + for projectile in boss.get_node("Pivot").get_children(): + if projectile is EnemyProjectile: + encode_projectile(buffer, projectile) + +func encode_projectile(buffer: StreamPeerBuffer, projectile) -> void: + buffer.put_32(projectile.network_id) + + buffer.put_float(projectile.position.x) + buffer.put_float(projectile.position.y) + + buffer.put_float(projectile.global_position.x) + buffer.put_float(projectile.global_position.y) + + buffer.put_float(projectile.rotation) + + if not sent_detail_ids.has(projectile.network_id): + buffer.put_8(1) + + buffer.put_string(projectile.filename) + + sent_detail_ids[projectile.network_id] = true + else: + buffer.put_8(0) + +func update_enemy_projectiles(buffer:StreamPeerBuffer) -> void: + var server_enemy_projectiles = {} + + var num_projectiles = buffer.get_u16() + + for _projectile_index in num_projectiles: + var projectile_id = buffer.get_32() + + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + var position = Vector2(pos_x, pos_y) + + var global_pos_x = buffer.get_float() + var global_pos_y = buffer.get_float() + var global_position = Vector2(global_pos_x, global_pos_y) + + var rotation = buffer.get_float() + + var has_detail = buffer.get_8() == 1 + + var filename = "" + + if has_detail: + filename = buffer.get_string() + + if not client_enemy_projectiles.has(projectile_id): + if filename.empty(): + continue + client_enemy_projectiles[projectile_id] = spawn_enemy_projectile(position, global_position, rotation, filename) + if is_instance_valid(client_enemy_projectiles[projectile_id]): + client_enemy_projectiles[projectile_id].position = position + client_enemy_projectiles[projectile_id].global_position = global_position + server_enemy_projectiles[projectile_id] = true + + for projectile_id in client_enemy_projectiles: + if not server_enemy_projectiles.has(projectile_id): + var projectile = client_enemy_projectiles[projectile_id] + if not client_enemy_projectiles[projectile_id]: + continue + + client_enemy_projectiles.erase(projectile_id) + if not $"/root/ClientMain/Projectiles": + continue + if not projectile: + continue + if not is_instance_valid(projectile): + continue + + # This sometimes throws a C++ error + $"/root/ClientMain/Projectiles".remove_child(projectile) + +func spawn_enemy_projectile(position:Vector2, global_position:Vector2, rotation:float, filename:String): + var projectiles_container = $"/root/ClientMain/Projectiles" + var projectile = load(filename).instance() + projectile.set_meta("pool_id", Keys.generate_hash(filename)) + projectile._hitbox.active = false + + projectile.position = position + projectile.global_position = global_position + projectile.rotation = rotation + + projectile.set_physics_process(false) + + projectiles_container.add_child(projectile, true) + + projectile.call_deferred("set_physics_process", false) + + return projectile + + +func get_items_state(buffer: StreamPeerBuffer) -> void: + var main = $"/root/Main" + + var num_items = main._items_container.get_children().size() + buffer.put_u16(num_items) + + for item in main._items_container.get_children(): + buffer.put_32(item.get_network_id()) + buffer.put_float(item.global_position.x) + buffer.put_float(item.global_position.y) + + if not sent_detail_ids.has(item.get_network_id()): + buffer.put_8(1) + + buffer.put_float(item.scale.x) + buffer.put_float(item.scale.y) + + buffer.put_float(item.rotation) + + buffer.put_float(item.push_back_destination.x) + buffer.put_float(item.push_back_destination.y) + + sent_detail_ids[item.get_network_id()] = true + else: + buffer.put_8(0) + + # TODO we may want textures propagated + +func update_items(buffer:StreamPeerBuffer) -> void: + var server_items = {} + + var num_items = buffer.get_u16() + + for _item_index in num_items: + var item_id = buffer.get_32() + + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + + var has_detail = buffer.get_8() == 1 + + var scale_x = 0.0 + var scale_y = 0.0 + + var rotation = 0.0 + + var push_back_x = 0.0 + var push_back_y = 0.0 + + if has_detail: + scale_x = buffer.get_float() + scale_y = buffer.get_float() + rotation = buffer.get_float() + push_back_x = buffer.get_float() + push_back_y = buffer.get_float() + + if not client_items.has(item_id): + client_items[item_id] = spawn_gold(Vector2(pos_x, pos_y), Vector2(scale_x, scale_y), rotation, Vector2(push_back_x, push_back_y)) + if is_instance_valid(client_items[item_id]): + client_items[item_id].global_position = Vector2(pos_x, pos_y) + client_items[item_id].push_back_destination = Vector2(pos_x, pos_y) + server_items[item_id] = true + + for item_id in client_items: + if not server_items.has(item_id): + var item = client_items[item_id] + if not client_items[item_id]: + continue + + client_items.erase(item_id) + if not $"/root/ClientMain/Items": + continue + if not item: + continue + if not is_instance_valid(item): + continue + + # This sometimes throws a C++ error + $"/root/ClientMain/Items".remove_child(item) + +func spawn_gold(position:Vector2, scale:Vector2, rotation:float, push_back_destination: Vector2): + var gold = gold_scene.instance() + + gold.global_position = position + gold.scale = scale + gold.rotation = rotation + gold.push_back_destination = push_back_destination + + $"/root/ClientMain/Items".add_child(gold) + + return gold + +func get_projectiles_state(buffer: StreamPeerBuffer) -> void: + var main = $"/root/Main" + + var num_projectiles = 0 + for child in main.get_children(): + if child is PlayerProjectile: + num_projectiles += 1 + + buffer.put_u16(num_projectiles) + + for child in main.get_children(): + if child is PlayerProjectile: + buffer.put_32(child.get_network_id()) + + buffer.put_float(child.position.x) + buffer.put_float(child.position.y) + + buffer.put_float(child.global_position.x) + buffer.put_float(child.global_position.y) + + buffer.put_float(child.rotation) + + # TODO send conditionally + buffer.put_string(child.filename) + + +func get_player_projectiles_array(buffer:StreamPeerBuffer) -> Array: + var server_player_projectiles = {} + + var player_projectiles_array = [] + var num_projectiles = buffer.get_u16() + + for _projectile_index in num_projectiles: + var projectile_id = buffer.get_32() + + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + + var global_pos_x = buffer.get_float() + var global_pos_y = buffer.get_float() + + var rotation = buffer.get_float() + var filename = buffer.get_string() + + if filename == "": + continue + + player_projectiles_array.push_back([projectile_id, pos_x, pos_y, global_pos_x, global_pos_y, rotation, filename]) + + return player_projectiles_array + + +func update_player_projectiles(player_projectiles_array:Array) -> void: + var server_player_projectiles = {} + + for player_projectile in player_projectiles_array: + var projectile_id = player_projectile[0] + + var pos_x = player_projectile[1] + var pos_y = player_projectile[2] + var position = Vector2(pos_x, pos_y) + + var global_pos_x = player_projectile[3] + var global_pos_y = player_projectile[4] + var global_position = Vector2(global_pos_x, global_pos_y) + + var rotation = player_projectile[5] + var filename = player_projectile[6] + + if filename == "": + continue + + if not client_player_projectiles.has(projectile_id): + client_player_projectiles[projectile_id] = spawn_player_projectile(position, global_position, rotation, filename) + + var client_player_projectile = client_player_projectiles[projectile_id] + if is_instance_valid(client_player_projectile): + client_player_projectile.position = position + client_player_projectile.rotation = rotation + server_player_projectiles[projectile_id] = true + + for projectile_id in client_player_projectiles.keys(): + if not server_player_projectiles.has(projectile_id): + var player_projectile = client_player_projectiles[projectile_id] + client_player_projectiles.erase(projectile_id) + if is_instance_valid(player_projectile): + player_projectile.queue_free() + + +func spawn_player_projectile(position:Vector2, global_position:Vector2, rotation:float, filename:String): + var main = $"/root/ClientMain" + var projectile = load(filename).instance() + + projectile.position = position + projectile.spawn_position = global_position + projectile.global_position = global_position + projectile.rotation = rotation + # TODO this is probably wrong? + projectile.weapon_stats = weapon_stats_resource.duplicate() + projectile.set_physics_process(false) + + main.add_child(projectile, true) + + projectile.call_deferred("set_physics_process", false) + + return projectile + +func get_deaths(buffer: StreamPeerBuffer) -> void: + var num_deaths = parent.batched_deaths.size() + buffer.put_u16(num_deaths) + + for enemy_id in parent.batched_deaths: + buffer.put_32(enemy_id) + + parent.batched_deaths = [] + +func get_batched_deaths(buffer:StreamPeerBuffer) -> Array: + var result = [] + var num_deaths = buffer.get_u16() + for _i in num_deaths: + result.push_back(buffer.get_32()) + return result + +func do_batched_deaths(death_ids : Array) -> void: + for enemy_id in death_ids: + enemy_death(enemy_id) + +func enemy_death(enemy_id): + if client_enemies.has(enemy_id): + if is_instance_valid(client_enemies[enemy_id]): + client_enemies[enemy_id].die() + +func get_enemy_damages(buffer: StreamPeerBuffer) -> void: + var num_damages = parent.batched_enemy_damage.size() + buffer.put_u16(num_damages) + + for damage_array in parent.batched_enemy_damage: + buffer.put_32(damage_array[0]) + var is_dodge = 0 + if damage_array[1]: + is_dodge = 1 + buffer.put_8(is_dodge) + + parent.batched_enemy_damage = [] + +func get_batched_damages(buffer:StreamPeerBuffer) -> Array: + var result = [] + var num_damages = buffer.get_u16() + for _i in num_damages: + var enemy_id = buffer.get_32() + var is_dodge = buffer.get_8() == 1 + result.push_back([enemy_id, is_dodge]) + return result + +func do_batched_damages(batched_damages: Array) -> void: + for damage in batched_damages: + var enemy_id = damage[0] + var is_dodge = damage[1] + do_enemy_damage(enemy_id, is_dodge) + +func do_enemy_damage(enemy_id:int, is_dodge:bool) -> void: + if not is_inside_tree(): + return + if client_enemies.has(enemy_id): + var enemy = client_enemies[enemy_id] + if is_instance_valid(enemy): + var sound + if is_dodge: + sound = Utils.get_rand_element(enemy.dodge_sounds) + else: + sound = Utils.get_rand_element(enemy.hurt_sounds) + SoundManager2D.play(sound, enemy.global_position, 0, 0.2, enemy.always_play_hurt_sound) + +func get_enemy_flashes(buffer: StreamPeerBuffer) -> void: + var num_flashes = parent.batched_flash_enemy.size() + buffer.put_u16(num_flashes) + + for enemy_id in parent.batched_flash_enemy: + buffer.put_32(enemy_id) + + parent.batched_flash_enemy = [] + +func get_batched_flashes(buffer:StreamPeerBuffer) -> Array: + var result = [] + var num_flashes = buffer.get_u16() + + for _i in num_flashes: + var enemy_id = buffer.get_32() + result.push_back(enemy_id) + return result + +func do_batched_flashes(batched_flashes: Array) -> void: + for enemy_id in batched_flashes: + flash_enemy(enemy_id) + +func flash_enemy(enemy_id): + if not is_inside_tree(): + return + if client_enemies.has(enemy_id): + if is_instance_valid(client_enemies[enemy_id]): + client_enemies[enemy_id].flash() + +func get_batched_floating_text(buffer: StreamPeerBuffer) -> void: + var num_floating_text = parent.batched_floating_text.size() + buffer.put_u16(num_floating_text) + + for floating_text_array in parent.batched_floating_text: + var value = floating_text_array[0] + var text_pos = floating_text_array[1] + var color = floating_text_array[2] + + buffer.put_string(value) + buffer.put_float(text_pos.x) + buffer.put_float(text_pos.y) + buffer.put_32(color.to_rgba32()) + + parent.batched_floating_text = [] + +func get_batched_text_array(buffer: StreamPeerBuffer) -> Array: + var result = [] + var num_floating_text = buffer.get_u16() + + for _i in num_floating_text: + var value = buffer.get_string() + + var x = buffer.get_float() + var y = buffer.get_float() + var text_pos = Vector2(x,y) + + var color_rgba32 = buffer.get_32() + var color = Color(color_rgba32) + result.push_back([value, text_pos, color]) + return result + +func do_batched_floating_text(text_array: Array) -> void: + for text in text_array: + display_floating_text(text[0], text[1], text[2]) + +func display_floating_text(value:String, text_pos:Vector2, color:Color = Color.white): + if $"/root/ClientMain": + $"/root/ClientMain/FloatingTextManager".display(value, text_pos, color) + +func get_hit_effects(buffer: StreamPeerBuffer) -> void: + var num_hit_effects = parent.batched_hit_effects.size() + buffer.put_u16(num_hit_effects) + + for hit_effect in parent.batched_hit_effects: + buffer.put_float(hit_effect[0].x) + buffer.put_float(hit_effect[0].y) + + buffer.put_float(hit_effect[1].x) + buffer.put_float(hit_effect[1].y) + + buffer.put_float(hit_effect[2]) + parent.batched_hit_effects = [] + +func get_batch_hit_effects_array(buffer: StreamPeerBuffer) -> Array: + var result = [] + var num_hit_effects = buffer.get_u16() + + for _i in num_hit_effects: + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + + var dir_x = buffer.get_float() + var dir_y = buffer.get_float() + + var scale = buffer.get_float() + var pos = Vector2(pos_x, pos_y) + var dir = Vector2(dir_x, dir_y) + result.push_back([pos, dir, scale]) + return result + +func do_batched_hit_effects(hit_effects_array: Array) -> void: + for hit_effect in hit_effects_array: + display_hit_effect(hit_effect[0], hit_effect[1], hit_effect[2]) + +func display_hit_effect(position, direction, scale): + if $"/root/ClientMain/EffectsManager": + var effects_manager = $"/root/ClientMain/EffectsManager" + effects_manager.play_hit_particles(position, direction, scale) + effects_manager.play_hit_effect(position, direction, scale) + +func flash_neutral(neutral_id): + if client_neutrals.has(neutral_id): + if is_instance_valid(client_neutrals[neutral_id]): + client_neutrals[neutral_id].flash() + +func get_enemies_state(buffer: StreamPeerBuffer) -> void: + var main = $"/root/Main" + var entity_spawner = main._entity_spawner + + var num_enemies = entity_spawner.enemies.size() + buffer.put_u16(num_enemies) + + for enemy in entity_spawner.enemies: + if is_instance_valid(enemy): + var network_id = enemy.get_network_id() + buffer.put_32(network_id) + + if not sent_detail_ids.has(network_id): + buffer.put_8(1) + + buffer.put_string(enemy.stats.resource_path) + buffer.put_string(enemy.filename) + + sent_detail_ids[network_id] = true + else: + buffer.put_8(0) + + buffer.put_float(enemy.position.x) + buffer.put_float(enemy.position.y) + buffer.put_float(enemy._current_movement.x) + buffer.put_float(enemy._current_movement.y) + + var num_bosses = entity_spawner.bosses.size() + buffer.put_u16(num_bosses) + + for enemy in entity_spawner.bosses: + if is_instance_valid(enemy): + var network_id = enemy.get_network_id() + buffer.put_32(network_id) + + if not sent_detail_ids.has(network_id): + buffer.put_8(1) + + buffer.put_string(enemy.stats.resource_path) + buffer.put_string(enemy.filename) + + sent_detail_ids[network_id] = true + else: + buffer.put_8(0) + + buffer.put_float(enemy.position.x) + buffer.put_float(enemy.position.y) + buffer.put_float(enemy._current_movement.x) + buffer.put_float(enemy._current_movement.y) + + buffer.put_32(enemy.current_stats.health) + buffer.put_32(enemy.max_stats.health) + +func get_enemies_arrays(buffer:StreamPeerBuffer) -> Array: + var enemies_array = [] + var num_enemies = buffer.get_u16() + for _enemy_index in num_enemies: + var enemy_id = buffer.get_32() + var has_filenames = buffer.get_8() == 1 + var filename = "" + var resource_path = "" + if has_filenames: + resource_path = buffer.get_string() + filename = buffer.get_string() + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + var mov_x = buffer.get_float() + var mov_y = buffer.get_float() + enemies_array.push_back([enemy_id, filename, resource_path, pos_x, pos_y, mov_x, mov_y]) + + var bosses_array = [] + var num_bosses = buffer.get_u16() + for _enemy_index in num_bosses: + var enemy_id = buffer.get_32() + var has_filenames = buffer.get_8() == 1 + var filename = "" + var resource_path = "" + if has_filenames: + resource_path = buffer.get_string() + filename = buffer.get_string() + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + var mov_x = buffer.get_float() + var mov_y = buffer.get_float() + var current_hp = buffer.get_32() + var max_hp = buffer.get_32() + bosses_array.push_back([enemy_id, filename, resource_path, pos_x, pos_y, mov_x, mov_y, current_hp, max_hp]) + + return [enemies_array, bosses_array] + +func update_enemies(enemies_arrays: Array) -> void: + var server_enemies = {} + + for enemy in enemies_arrays[0]: + var enemy_id = enemy[0] + var filename = enemy[1] + var resource_path = enemy[2] + + var pos_x = enemy[3] + var pos_y = enemy[4] + var mov_x = enemy[5] + var mov_y = enemy[6] + + var position = Vector2(pos_x, pos_y) + var movement = Vector2(mov_x, mov_y) + + if not client_enemies.has(enemy_id): + if filename == "": + continue + call_deferred("spawn_enemy", enemy_id, position, filename, resource_path) + + if client_enemies.has(enemy_id): + var stored_enemy = client_enemies[enemy_id] + if is_instance_valid(stored_enemy): + server_enemies[enemy_id] = true + stored_enemy.position = position + stored_enemy.call_deferred("update_animation", movement) + + for boss in enemies_arrays[1]: + var enemy_id = boss[0] + var filename = boss[1] + var resource_path = boss[2] + + var pos_x = boss[3] + var pos_y = boss[4] + var mov_x = boss[5] + var mov_y = boss[6] + + var current_hp = boss[7] + var max_hp = boss[8] + + var position = Vector2(pos_x, pos_y) + var movement = Vector2(mov_x, mov_y) + + if not client_enemies.has(enemy_id): + if filename == "": + continue + call_deferred("spawn_enemy", enemy_id, position, filename, resource_path) + + if client_enemies.has(enemy_id): + var stored_enemy = client_enemies[enemy_id] + if is_instance_valid(stored_enemy): + server_enemies[enemy_id] = true + stored_enemy.position = position + stored_enemy.on_health_updated(current_hp, max_hp) + stored_enemy.call_deferred("update_animation", movement) + + +func spawn_enemy(enemy_id, position:Vector2, filename:String, resource_path:String): + var entity = load(filename).instance() + + entity.position = position + entity.stats = load(resource_path) + + _clear_movement_behavior(entity) + + $"/root/ClientMain/Entities".add_child(entity) + + client_enemies[enemy_id] = entity + + +# TODO: DEDUPE +func _clear_movement_behavior(entity:Entity, is_player:bool = false) -> void: + # Players will only move via client calls, locally make them do + # nothing + # Since the player is added before it's children can be manipulatd, + # manually set the current movement behavior to set it correctly + var movement_behavior = entity.get_node("MovementBehavior") + entity.remove_child(movement_behavior) + var client_movement_behavior = ClientMovementBehavior.new() + client_movement_behavior.set_name("MovementBehavior") + entity.add_child(client_movement_behavior, true) + entity._current_movement_behavior = client_movement_behavior + + if not is_player: + var attack_behavior = entity.get_node("AttackBehavior") + entity.remove_child(attack_behavior) + var client_attack_behavior = ClientAttackBehavior.new() + client_attack_behavior.set_name("AttackBehavior") + entity.add_child(client_attack_behavior, true) + entity._current_attack_behavior = client_attack_behavior + + if is_player: + entity.call_deferred("remove_weapon_behaviors") + + + +func spawn_entity_birth(color:Color, position:Vector2): + var entity_birth = entity_birth_scene.instance() + + entity_birth.color = color + entity_birth.global_position = position + + $"/root/ClientMain/Entities".add_child(entity_birth) + + return entity_birth + +func reset_client_items(): + client_enemies = {} + client_births = {} + client_players = {} + client_items = {} + client_player_projectiles = {} + client_consumables = {} + client_neutrals = {} + +func get_structures_state(buffer: StreamPeerBuffer) -> void: + var main = $"/root/Main" + var entity_spawner = main._entity_spawner + + var num_structures = entity_spawner.structures.size() + buffer.put_u16(num_structures) + + for structure in entity_spawner.structures: + if is_instance_valid(structure): + var structure_id = structure.network_id + buffer.put_32(structure_id) + + # TODO only send spawn info once + if not sent_detail_ids.has(structure_id): + buffer.put_8(1) + buffer.put_string(structure.filename) + else: + buffer.put_8(0) + + buffer.put_float(structure.position.x) + buffer.put_float(structure.position.y) + +func update_structures(buffer:StreamPeerBuffer) -> void: + var server_structures = {} + + var num_structures = buffer.get_u16() + + for _structure_index in num_structures: + var structure_id = buffer.get_32() + var has_filename = buffer.get_8() == 1 + var filename = "" + + if has_filename: + filename = buffer.get_string() + + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + + var position = Vector2(pos_x, pos_y) + + if not client_structures.has(structure_id): + if not has_filename: + continue + var enemy = spawn_stucture(position, filename) + client_structures[structure_id] = enemy + + var stored_structure = client_structures[structure_id] + if is_instance_valid(stored_structure): + server_structures[structure_id] = true + stored_structure.position = position + + for structure_id in client_structures: + if not server_structures.has(structure_id): + var structure = client_structures[structure_id] + client_structures.erase(structure_id) + if is_instance_valid(structure) and $"/root/ClientMain/Entities".is_a_parent_of(structure): + $"/root/ClientMain/Entities".remove_child(structure) + +func spawn_stucture(position:Vector2, filename:String): + var structure = load(filename).instance() + + structure.position = position + structure.stats = turret_stats_resource + + $"/root/ClientMain/Entities".add_child(structure) + + return structure + +func get_births_state(buffer: StreamPeerBuffer) -> void: + var main = $"/root/Main" + + var num_births = main._entity_spawner.births.size() + buffer.put_u16(num_births) + + for birth in main._entity_spawner.births: + if is_instance_valid(birth) and birth.is_inside_tree() and birth.get_node("data_node") != null: + buffer.put_32(birth.get_network_id()) + + buffer.put_float(birth.global_position.x) + buffer.put_float(birth.global_position.y) + + buffer.put_float(birth.color.r) + buffer.put_float(birth.color.g) + buffer.put_float(birth.color.b) + buffer.put_float(birth.color.a) + else: + buffer.put_32(1) + + buffer.put_float(0.0) + buffer.put_float(0.0) + + buffer.put_float(0.0) + buffer.put_float(0.0) + buffer.put_float(0.0) + buffer.put_float(0.0) + +func update_births(buffer:StreamPeerBuffer) -> void: + var server_births = {} + var num_births = buffer.get_u16() + + for _birth_index in num_births: + var birth_id = buffer.get_32() + + var x = buffer.get_float() + var y = buffer.get_float() + var r = buffer.get_float() + var g = buffer.get_float() + var b = buffer.get_float() + var a = buffer.get_float() + + if not client_births.has(birth_id): + client_births[birth_id] = spawn_entity_birth(Color(r,g,b,a), Vector2(x,y)) + server_births[birth_id] = true + + for birth_id in client_births: + if not server_births.has(birth_id): + var birth_to_delete = client_births[birth_id] + if birth_to_delete: +# Children go away on their own when they time out? +# $"/root/ClientMain/Births".remove_child(birth_to_delete) + client_births.erase(birth_id) + + +func get_players_state(buffer: StreamPeerBuffer) -> void: + var tracked_players = parent.tracked_players + var num_players = tracked_players.size() + buffer.put_u16(num_players) + + for player_id in tracked_players: + var tracked_player = tracked_players[player_id]["player"] + var run_data = tracked_players[player_id]["run_data"] + + buffer.put_64(player_id) + + buffer.put_float(tracked_player.position.x) + buffer.put_float(tracked_player.position.y) + + buffer.put_float(tracked_player.current_stats.speed) + + buffer.put_float(tracked_player._current_movement.x) + buffer.put_float(tracked_player._current_movement.y) + + buffer.put_u16(tracked_player.current_stats.health) + buffer.put_u16(tracked_player.max_stats.health) + + # This would be where individual inventories are sent out instead of + # RunData.gold + buffer.put_u16(run_data.gold) + buffer.put_u16(run_data.current_level) + buffer.put_u16(run_data.current_xp) +# print_debug("sending gold for player ", player_id, " ", run_data.gold) + + var num_appearances = run_data.appearances_displayed.size() + buffer.put_u16(num_appearances) + + for appearance in run_data.appearances_displayed: + buffer.put_string(appearance.resource_path) + + var num_weapons = tracked_player.current_weapons.size() + buffer.put_u16(num_weapons) + + for weapon in tracked_player.current_weapons: + if not is_instance_valid(weapon): + continue + buffer.put_float(weapon.sprite.position.x) + buffer.put_float(weapon.sprite.position.y) + + buffer.put_float(weapon.sprite.rotation) + + buffer.put_8(weapon._is_shooting) + + if not sent_detail_ids.has(player_id): + buffer.put_8(1) + if weapon.has_node("data_node"): + var weapon_data_path = RunData.weapon_paths[weapon.get_node("data_node").weapon_data.my_id] + buffer.put_string(weapon_data_path) +# print_debug("sending data path ", weapon.get_node("data_node").weapon_data.my_id, " ", weapon_data_path) +# TODO: uncomment this to stop writes, it'll cause problems later though +# sent_detail_ids[player_id] = true + else: + buffer.put_8(0) + +func update_players(buffer:StreamPeerBuffer) -> void: + var tracked_players = parent.tracked_players + + var num_players = buffer.get_u16() + + for _player_index in num_players: + var player_id = buffer.get_64() + if not player_id in tracked_players: + tracked_players[player_id] = {} + parent.init_player_data(tracked_players[player_id], player_id) + + var run_data = tracked_players[player_id]["run_data"] + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + + var speed = buffer.get_float() + + var mov_x = buffer.get_float() + var mov_y = buffer.get_float() + + var current_health = buffer.get_u16() + var max_health = buffer.get_u16() + + var gold = buffer.get_u16() + var current_level = buffer.get_16() + var current_xp = buffer.get_16() + + var num_appearances = buffer.get_u16() + var appeareances_filenames = [] + + # TODO This should be part of spawning only + for _appearance_index in num_appearances: + appeareances_filenames.push_back(buffer.get_string()) + + var num_weapons = buffer.get_u16() + var weapons = [] + for _weapon_index in num_weapons: + var weapon = {} + + var weapon_pos_x = buffer.get_float() + var weapon_pos_y = buffer.get_float() + weapon["position"] = Vector2(weapon_pos_x, weapon_pos_y) + + var weapon_rotation = buffer.get_float() + weapon["rotation"] = weapon_rotation + + var weapon_is_shooting = buffer.get_8() + weapon["is_shooting"] = weapon_is_shooting + + var has_detail = buffer.get_8() == 1 + if has_detail: + var weapon_path = buffer.get_string() + weapon["path"] = weapon_path + weapons.push_back(weapon) + + + if not tracked_players[player_id].has("player") or not is_instance_valid(tracked_players[player_id].player): + tracked_players[player_id]["player"] = spawn_player(player_id, Vector2(pos_x, pos_y), speed, weapons, appeareances_filenames) + + var player = tracked_players[player_id]["player"] + if player_id == parent.self_peer_id: + if $"/root/ClientMain": + var main = $"/root/ClientMain" + + main._life_bar.update_value(current_health, max_health) + main.set_life_label(current_health, max_health) + main._damage_vignette.update_from_hp(current_health, max_health) + + if is_instance_valid(player): + player.current_stats.health = current_health + player.max_stats.health = max_health + + tracked_players[player_id]["current_health"] = current_health + tracked_players[player_id]["max_health"] = max_health + + if run_data.gold != gold: + run_data.gold = gold + if player_id == parent.self_peer_id: + $"/root/ClientMain"._ui_gold.on_gold_changed(gold) + + if run_data.current_xp != current_xp: + run_data.current_xp = current_xp + var next_level_xp = RunData.get_xp_needed(current_level + 1) + if player_id == parent.self_peer_id: + $"/root/ClientMain"._xp_bar.update_value(current_xp, next_level_xp) + +# TODO this should only be set once but it seems the run_data is set elsewhere + run_data.current_level = current_level + $"/root/ClientMain"._level_label.text = "LV." + str(current_level) + else: + if is_instance_valid(player): + player.position = Vector2(pos_x, pos_y) + + # This is currently only used by the hp tracker bar + player.current_stats.health = current_health + player.max_stats.health = max_health + + player.call_deferred("maybe_update_animation", Vector2(mov_x, mov_y), true) + if is_instance_valid(player): + for weapon_data_index in player.current_weapons.size(): + var weapon_data = weapons[weapon_data_index] + var weapon = player.current_weapons[weapon_data_index] + if is_instance_valid(weapon): + weapon.sprite.position = weapon_data.position + weapon.sprite.rotation = weapon_data.rotation + weapon._is_shooting = weapon_data.is_shooting + +func spawn_player(player_id:int, position:Vector2, speed:float, weapons:Array, appeareances_filenames: Array): + var spawned_player = player_scene.instance() + spawned_player.position = position + spawned_player.current_stats.speed = speed + spawned_player.call_deferred("init", Vector2(0,0), Vector2(0,0)) + + for weapon in weapons: + spawned_player.call_deferred("add_weapon", load(weapon["path"]), spawned_player.current_weapons.size()) + + $"/root/ClientMain/Entities".add_child(spawned_player) + + if player_id == parent.self_peer_id: + spawned_player.get_remote_transform().remote_path = $"/root/ClientMain/Camera".get_path() + else: + spawned_player.call_deferred("remove_movement_behavior") + + spawned_player.call_deferred("remove_weapon_behaviors") + + for filename in appeareances_filenames: + var item_sprite = Sprite.new() + item_sprite.texture = load(filename).sprite + spawned_player.get_node("Animation").add_child(item_sprite) + + return spawned_player + +func get_consumables_state(buffer: StreamPeerBuffer) -> void: + var main = $"/root/Main" + + var num_consumables = main._consumables_container.get_children().size() + buffer.put_u16(num_consumables) + + for consumable in main._consumables_container.get_children(): + buffer.put_32(consumable.get_network_id()) + + buffer.put_float(consumable.global_position.x) + buffer.put_float(consumable.global_position.y) + + buffer.put_string(consumable.consumable_data.icon.load_path) + +func update_consumables(buffer:StreamPeerBuffer) -> void: + var num_consumables = buffer.get_u16() + + var server_consumables = {} + + for _consumable_index in num_consumables: + var consumable_id = buffer.get_32() + + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + var position = Vector2(pos_x, pos_y) + + var texture_path = buffer.get_string() + + if not client_consumables.has(consumable_id): + client_consumables[consumable_id] = spawn_consumable(position, texture_path) + + var consumable = client_consumables[consumable_id] + if is_instance_valid(consumable): + consumable.global_position = position + server_consumables[consumable_id] = true + for consumable_id in client_consumables: + if not server_consumables.has(consumable_id): + var consumable = client_consumables[consumable_id] + client_consumables.erase(consumable_id) + if is_instance_valid(consumable): + $"/root/ClientMain/Consumables".remove_child(consumable) + +func spawn_consumable(position:Vector2, filepath:String): + var consumable = consumable_scene.instance() + + consumable.global_position = position + consumable.call_deferred("set_texture", load(filepath)) + consumable.call_deferred("set_physics_process", false) + + $"/root/ClientMain/Consumables".add_child(consumable) + + return consumable + +func get_neutrals_state(buffer: StreamPeerBuffer) -> void: + var main = $"/root/Main" + + var num_neutrals = 0 + for neutral in main._entity_spawner.neutrals: + if is_instance_valid(neutral): + num_neutrals += 1 + + buffer.put_u16(num_neutrals) + + for neutral in main._entity_spawner.neutrals: + if is_instance_valid(neutral): + buffer.put_32(neutral.get_network_id()) + + buffer.put_float(neutral.global_position.x) + buffer.put_float(neutral.global_position.y) + +func update_neutrals(buffer:StreamPeerBuffer) -> void: + var server_neutrals = {} + var num_neutrals = buffer.get_u16() + for _neutral_index in num_neutrals: + var neutral_id = buffer.get_32() + + var pos_x = buffer.get_float() + var pos_y = buffer.get_float() + var position = Vector2(pos_x, pos_y) + + if not client_neutrals.has(neutral_id): + client_neutrals[neutral_id] = spawn_neutral(position) + var neutral = client_neutrals[neutral_id] + if is_instance_valid(neutral): + neutral.global_position = position + server_neutrals[neutral_id] = true + for neutral_id in client_neutrals: + if not server_neutrals.has(neutral_id): + var neutral = client_neutrals[neutral_id] + client_neutrals.erase(neutral_id) + if is_instance_valid(neutral) and $"/root/ClientMain/Entities".is_a_parent_of(neutral): + $"/root/ClientMain/Entities".remove_child(neutral) + +func spawn_neutral(position:Vector2): + var neutral = tree_scene.instance() + neutral.global_position = position + + $"/root/ClientMain/Entities".add_child(neutral) + + return neutral diff --git a/src/mods-unpacked/Brogether-Brogether/networking/steam_connection.gd b/src/mods-unpacked/Brogether-Brogether/networking/steam_connection.gd new file mode 100644 index 0000000..8594303 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/networking/steam_connection.gd @@ -0,0 +1,453 @@ +extends "res://mods-unpacked/Brogether-Brogether/networking/connection.gd" + +var lobby_id = 0 +var parent +var established_p2p_connections = {} + +var rpcs_by_type = {} +var prev_sec = 0 + +func _ready(): + print_debug("steam connection ready?") + lobby_id = 0 + var _connect_error = Steam.connect("lobby_created", self, "_on_Lobby_Created") + print_debug("connect error? ", _connect_error) + _connect_error = Steam.connect("lobby_match_list", self, "_on_Lobby_Match_List") + _connect_error = Steam.connect("lobby_joined", self, "_on_Lobby_Joined") + _connect_error = Steam.connect("lobby_chat_update", self, "_on_Lobby_Chat_Update") + _connect_error = Steam.connect("p2p_session_request", self, "_on_P2P_Session_Request") + +func _process(_delta): + if lobby_id > 0: + while read_p2p_packet(): + pass + +func read_p2p_packet() -> bool: + var packet_size = Steam.getAvailableP2PPacketSize(0) + + var sec = Time.get_time_dict_from_system()["second"] + if sec != prev_sec: + prev_sec = sec + + if packet_size > 0: + var packet = Steam.readP2PPacket(packet_size, 0) + + var sender = packet["steam_id_remote"] + var data = bytes2var(packet["data"].decompress_dynamic(-1, File.COMPRESSION_GZIP)) + var type = data.type + + if not rpcs_by_type.has(type): + rpcs_by_type[type] = 0 + rpcs_by_type[type] += 1 + + if type == "start_game": + parent.start_game(data.game_info) + elif type == "reroll_upgrades": + parent.receive_reroll_upgrades(sender, data.reroll_price) + elif type == "weapon_discard": + parent.receive_item_discard(data.weapon_id, sender) + elif type == "send_combine_item": + parent.receive_item_combine(data.weapon_data_id, data.is_upgrade, data.player_id) + elif type == "discard_item_box": + parent.receive_discard_item_box(sender, data.item_id) + elif type == "send_take_item_box": + parent.receive_item_box_take(data.item_id, data.player_id) + elif type == "client_entered_shop": + parent.receive_player_enter_shop(data.player_id) + elif type == "client_started": + parent.receive_client_start(data.player_id) + elif type == "add_consumable_to_process": + parent.receive_add_consumable_to_process(data.player_id, data.consumable_data_path) + elif type == "end_wave": + parent.end_wave() + elif type == "level_up": + parent.receive_player_level_up(data.player_id, data.level) + elif type == "flash_neutral": + parent.flash_neutral(data.neutral_id) + elif type == "client_position_update": + parent.update_client_position(data.client_position) + elif type == "send_bought_item": + parent.receive_bought_item(load(data.shop_item), sender) + elif type == "send_bought_item_by_id": + parent.receive_bought_item_by_id(data.item_id, sender, data.value) + elif type == "send_ready": + parent.update_ready_state(sender, data.is_ready) + elif type == "request_complete_player": + parent.send_complete_player(sender) + elif type == "complete_player": + parent.receive_complete_player(data.player_id, data.player_dict) + elif type == "handshake": + # completes the handshake? + established_p2p_connections[sender] = true + update_tracked_players() + send_welcomes() + elif type == "update_tracked_players": + parent.update_tracked_players(data.tracked_players) + elif type == "lobby_update": + print_debug("received lobby update") + parent.receive_lobby_update(data.lobby_info) + elif type == "reroll": + parent.receive_reroll(data.price, sender) + elif type == "health_update": + parent.receive_health_update(data.current_health, data.max_health, sender) + elif type == "death": + parent.receive_death(sender) + elif type == "select_upgrade": + parent.receive_uprade_selected(data.upgrade_data_id, sender) + elif type == "shot": + parent.receive_shot(data.player_id, data.weapon_index) + elif type == "explosion": + parent.receive_explosion(Vector2(data.pos_x, data.pos_y), data.scale) + elif type == "character_selected": + parent.received_character_selected(data.player_id, data.character) + elif type == "weapon_selected": + parent.received_weapon_selected(data.player_id, data.weapon) + elif type == "danger_selected": + parent.received_danger_selected(data.player_id, data.danger) + elif type == "mp_lobby_readied": + parent.receive_mp_lobby_ready_changed(data.player_id, data.is_ready) + elif type == "request_lobby_update": + print_debug("received updated request") + if get_tree().get_current_scene().get_name() == "MultiplayerLobby": + send_lobby_update(parent.lobby_data) + else: + print_debug("unhandled type " , type) + return true + return false + +func _on_Lobby_Match_List(lobbies: Array): + var scene_name = get_tree().get_current_scene().get_name() + if scene_name == "MultiplayerMenu": + $"/root/MultiplayerMenu".update_lobbies(lobbies) + pass + + +# if lobbies.size() == 1: +# Steam.joinLobby(lobbies[0]) +# pass + +func _on_Lobby_Created(connect: int, connected_lobby_id: int) -> void: + print_debug("lobby created") + if connect == 1: + lobby_id = connected_lobby_id + + var _set_error = Steam.setLobbyData(lobby_id, "game", "Brotatogether") + _set_error = Steam.setLobbyData(lobby_id, "host", Steam.getPersonaName()) + + var _error = Steam.allowP2PPacketRelay(false) + +func close_lobby() -> void: + # TODO this probably isn't the correct way to close a lobby but at least no one will find it + # for now. + var _set_error = Steam.setLobbyData(lobby_id, "game", "Not At All Brotatogether, Go Away") + +func _on_Lobby_Joined(joined_lobby_id: int, _permissions: int, _locked: bool, response: int) -> void: + if response == 1: + print_debug("on lobby joined") + lobby_id = joined_lobby_id + parent.self_peer_id = Steam.getSteamID() + var _scene_error = get_tree().change_scene("res://mods-unpacked/Brogether-Brogether/ui/multiplayer_lobby.tscn") + update_tracked_players() + send_handshakes() + + if not parent.is_host: + request_lobby_update() + +func _on_Lobby_Chat_Update(_update_lobby_id: int, change_id: int, _making_change_id: int, chat_state: int) -> void: + var username = Steam.getFriendPersonaName(change_id) + update_tracked_players() + + if chat_state == 1: + print_debug(username, " joined the lobby ", change_id) + pass + + # Else if a player has left the lobby + elif chat_state == 2: + print_debug(username, " has left the lobby.") + + # Else if a player has been kicked + elif chat_state == 8: + print_debug(username, " has been kicked from the lobby.") + + # Else if a player has been banned + elif chat_state == 16: + print_debug(username, " has been banned from the lobby.") + +# clears tracked_players and resets tracked players based +func update_tracked_players() -> void: + # Clear your previous lobby list + parent.tracked_players.clear() + + # Get the number of members from this lobby from Steam + var num_members = Steam.getNumLobbyMembers(lobby_id) + var all_players_ready = true + + # Get the data of these players from Steam + for member_index in range(num_members): + var member_steam_id = Steam.getLobbyMemberByIndex(lobby_id, member_index) + var member_username: String = Steam.getFriendPersonaName(member_steam_id) + + if not established_p2p_connections.has(member_steam_id) or not established_p2p_connections[member_steam_id]: + if member_steam_id != parent.self_peer_id: + all_players_ready = false + if parent.is_host: + member_username = member_username + + parent.tracked_players[member_steam_id] = {"username": member_username} + + parent.all_players_ready = all_players_ready + parent.update_multiplayer_lobby() + +func send_state(game_state:PoolByteArray) -> void: + var send_data = {} + send_data["game_state"] = game_state + send_data["type"] = "game_state" + + for player_id in parent.tracked_players: + if player_id == parent.self_peer_id: + continue + var compressed_data = var2bytes(send_data).compress(File.COMPRESSION_GZIP) + + # State updates get their own channel + var _error = Steam.sendP2PPacket(player_id, compressed_data, Steam.P2P_SEND_RELIABLE, 1) + +func send_start_game(game_info:Dictionary) -> void: + var send_data = {} + send_data["type"] = "start_game" + send_data["game_info"] = game_info + send_data_to_all(send_data) + +func send_end_wave(): + var send_data = {} + send_data["type"] = "end_wave" + send_data_to_all(send_data) + +func send_shot(player_id:int, weapon_index:int) -> void: + var send_data = {} + send_data["type"] = "shot" + send_data["player_id"] = player_id + send_data["weapon_index"] = weapon_index + send_data_to_all(send_data) + +func send_flash_neutral(neutral_id): + var send_data = {} + send_data["type"] = "flash_neutral" + send_data["neutral_id"] = neutral_id + send_data_to_all(send_data) + +func send_data_to_all(packet_data: Dictionary): + for player_id in parent.tracked_players: + if player_id == parent.self_peer_id: + continue + send_data(packet_data, player_id) + +func send_data(packet_data: Dictionary, target: int): + # TODO actually compress + var compressed_data = var2bytes(packet_data).compress(File.COMPRESSION_GZIP) + + # Just use channel 0 for everything for now + var _error = Steam.sendP2PPacket(target, compressed_data, Steam.P2P_SEND_RELIABLE, 0) + +# Done to trigger p2p session requests +func send_handshakes() -> void: + var send_data = {} + send_data["type"] = "handshake" + send_data_to_all(send_data) + +func send_welcomes() -> void: + var send_data = {} + send_data["type"] = "welcome" + send_data_to_all(send_data) + +func _on_P2P_Session_Request(remote_id: int) -> void: + var _error = Steam.acceptP2PSessionWithUser(remote_id) + + # Make the initial handshake + send_handshakes() + +func send_client_position(client_position:Dictionary) -> void: + var send_data = {} + send_data["type"] = "client_position_update" + send_data["client_position"] = client_position + send_data_to_all(send_data) + +func send_bought_item(shop_item:Resource) -> void: + var send_data = {} + send_data["type"] = "send_bought_item" + send_data["shop_item"] = shop_item.get_path() + send_data_to_all(send_data) + +func send_bought_item_by_id(item_id:String, value:int) -> void: + var send_data = {} + send_data["type"] = "send_bought_item_by_id" + send_data["item_id"] = item_id + send_data["value"] = value + send_data_to_all(send_data) + +func send_ready(is_ready:bool) -> void: + print_debug("sending ready") + var send_data = {} + send_data["type"] = "send_ready" + send_data["is_ready"] = is_ready + send_data_to_all(send_data) + +func send_tracked_players(tracked_players:Dictionary) -> void: + var send_data = {} + send_data["type"] = "update_tracked_players" + send_data["tracked_players"] = tracked_players + send_data_to_all(send_data) + +func send_lobby_update(lobby_info:Dictionary) -> void: + var send_data = {} + send_data["type"] = "lobby_update" + send_data["lobby_info"] = lobby_info + send_data_to_all(send_data) + +func request_lobby_update() -> void: + var send_data = {} + send_data["type"] = "request_lobby_update" + send_data_to_all(send_data) + +func send_health_update(current_health:int, max_health:int) -> void: + var send_data = {} + send_data["type"] = "health_update" + send_data["current_health"] = current_health + send_data["max_health"] = max_health + send_data_to_all(send_data) + +func send_death() -> void: + var send_data = {} + send_data["type"] = "death" + send_data_to_all(send_data) + +func send_explosion(pos: Vector2, scale: float) -> void: + var send_data = {} + send_data["type"] = "explosion" + send_data["pos_x"] = pos.x + send_data["pos_y"] = pos.y + send_data["scale"] = scale + send_data_to_all(send_data) + +func get_lobby_host() -> String: + return Steam.getLobbyData(lobby_id,"host") + +func send_upgrade_selection(upgrade_data_id) -> void: + var send_data = {} + send_data["type"] = "select_upgrade" + send_data["upgrade_data_id"] = upgrade_data_id + send_data_to_all(send_data) + +func send_player_level_up(player_id:int, level:int) -> void: + var send_data = {} + send_data["type"] = "level_up" + send_data["player_id"] = player_id + send_data["level"] = level + send_data_to_all(send_data) + +func send_complete_player_request() -> void: + var send_data = {} + send_data["type"] = "request_complete_player" + send_data_to_all(send_data) + +func send_complete_player(player_id:int, player_dict:Dictionary) -> void: + var send_data = {} + send_data["type"] = "complete_player" + send_data["player_id"] = player_id + send_data["player_dict"] = player_dict + send_data_to_all(send_data) + +func send_add_consumable_to_process(player_id:int, consumable_data_path) -> void: + var send_data = {} + send_data["type"] = "add_consumable_to_process" + send_data["player_id"] = player_id + send_data["consumable_data_path"] = consumable_data_path + send_data_to_all(send_data) + +func send_take_item_box(player_id:int, item_id) -> void: + var send_data = {} + send_data["type"] = "send_take_item_box" + send_data["player_id"] = player_id + send_data["item_id"] = item_id + send_data_to_all(send_data) + +func send_combine_item(weapon_data_id, is_upgrade, player_id) -> void: + var send_data = {} + send_data["type"] = "send_combine_item" + send_data["player_id"] = player_id + send_data["is_upgrade"] = is_upgrade + send_data["weapon_data_id"] = weapon_data_id + send_data_to_all(send_data) + +func send_discard_item_box(item_id) -> void: + var send_data = {} + send_data["type"] = "discard_item_box" + send_data["item_id"] = item_id + send_data_to_all(send_data) + +func send_reroll_upgrades(reroll_price) -> void: + var send_data = {} + send_data["type"] = "reroll_upgrades" + send_data["reroll_price"] = reroll_price + send_data_to_all(send_data) + +func send_weapon_discard(weapon_id) -> void: + var send_data = {} + send_data["type"] = "weapon_discard" + send_data["weapon_id"] = weapon_id + send_data_to_all(send_data) + +func send_client_entered_shop() -> void: + var send_data = {} + send_data["type"] = "client_entered_shop" + send_data["player_id"] = parent.self_peer_id + send_data_to_all(send_data) + +func send_client_started() -> void: + var send_data = {} + send_data["type"] = "client_started" + send_data["player_id"] = parent.self_peer_id + send_data_to_all(send_data) + + +func send_danger_selected(danger) -> void: + var send_data = {} + send_data["type"] = "danger_selected" + send_data["player_id"] = parent.self_peer_id + send_data["danger"] = danger + send_data_to_all(send_data) + + +func send_character_selected(character) -> void: + var send_data = {} + send_data["type"] = "character_selected" + send_data["player_id"] = parent.self_peer_id + send_data["character"] = character + send_data_to_all(send_data) + + +func send_weapon_selected(weapon) -> void: + var send_data = {} + send_data["type"] = "weapon_selected" + send_data["player_id"] = parent.self_peer_id + send_data["weapon"] = weapon + send_data_to_all(send_data) + + +func send_mp_lobby_readied(is_ready) -> void: + var send_data = {} + send_data["type"] = "mp_lobby_readied" + send_data["player_id"] = parent.self_peer_id + send_data["is_ready"] = is_ready + send_data_to_all(send_data) + + +func send_reroll(price) -> void: + var send_data = {} + send_data["type"] = "reroll" + send_data["player_id"] = parent.self_peer_id + send_data["price"] = price + send_data_to_all(send_data) + + +func send_game_state() -> void: + pass diff --git a/src/mods-unpacked/Brogether-Brogether/steam_connection.gd b/src/mods-unpacked/Brogether-Brogether/steam_connection.gd new file mode 100644 index 0000000..4f529e8 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/steam_connection.gd @@ -0,0 +1,1197 @@ +extends Node + +const GLOBAL_CHAT_TYPE := "BROTATOGETHER_GLOBAL_CHAT" +const GAME_LOBBY_TYPE := "BROTATOGETHER_GAME_LOBBY" + +enum PlayerStatus {CONNECTING, LOBBYING, PLAYING, SELF} + +# Channels will be mapped 1:1 with channels +enum MessageType { + # Start a latency ping. client -> host + MESSAGE_TYPE_PING, + + # Respond to a latency ping. host -> client + MESSAGE_TYPE_PONG, + + # Report the results of a latency ping. client -> host + MESSAGE_TYPE_LATENCY_REPORT, + + # Report the status of all players to clients. host -> client + MESSAGE_TYPE_PLAYER_STATUS, + + # Report a change in focus of the currently selected character. client -> host + MESSAGE_TYPE_CHARACTER_FOCUS, + + # Report that a character has been selected (clicked). client -> host + MESSAGE_TYPE_CHARACTER_SELECTED, + + MESSAGE_TYPE_CHARACTER_SELECTION_COMPLETED, + + # Report the state of all player selections so far + MESSAGE_TYPE_CHARACTER_LOBBY_UPDATE, + + # Report a change in focus of the currently selected weapon. client -> host + MESSAGE_TYPE_WEAPON_FOCUS, + + # Report that a weapon has been selected (clicked). client -> host + MESSAGE_TYPE_WEAPON_SELECTED, + + # Report the state of all player selections for weapons. + MESSAGE_TYPE_WEAPON_LOBBY_UPDATE, + + MESSAGE_TYPE_WEAPON_SELECTION_COMPLETED, + + # Report that the host has focused on a difficulty + MESSAGE_TYPE_DIFFICULTY_FOCUSED, + + # Report that the host has selected a difficulty + MESSAGE_TYPE_DIFFICULTY_PRESSED, + + MESSAGE_TYPE_SHOP_ITEM_FOCUS, + + MESSAGE_TYPE_SHOP_INVENTORY_ITEM_FOCUS, + + MESSAGE_TYPE_SHOP_GO_BUTTON_UPDATED, + + MESSAGE_TYPE_SHOP_WEAPON_DISCARD, + + MESSAGE_TYPE_SHOP_CLOSE_POPUP, + + MESSAGE_TYPE_SHOP_BUY_ITEM, + + MESSAGE_TYPE_SHOP_REROLL, + + MESSAGE_TYPE_SHOP_LOCK_ITEM, + + MESSAGE_TYPE_SHOP_UNLOCK_ITEM, + + MESSAGE_TYPE_SHOP_COMBINE_WEAPON, + + MESSAGE_TYPE_SHOP_LOBBY_UPDATE, + + MESSAGE_TYPE_LEAVE_SHOP, + + MESSAGE_TYPE_HOST_ROUND_START, + + MESSAGE_TYPE_MAIN_STATE, + + MESSAGE_TYPE_CLIENT_POSITION, + + MESSAGE_TYPE_CLIENT_FOCUS_MAIN_SCENE, + + MESSAGE_TYPE_MAIN_SCENE_REROLL_BUTTON_PRESSED, + + MESSAGE_TYPE_MAIN_SCENE_CHOOSE_UPGRADE_PRESSED, + + MESSAGE_TYPE_MAIN_SCENE_TAKE_BUTTON_PRESSED, + + MESSAGE_TYPE_MAIN_SCENE_DISCARD_BUTTON_PRESSED, + + MESSAGE_TYPE_HOST_ENTERED_SHOP, +} + +var global_chat_lobby_id : int = -1 +var steam_id : int + +var game_lobby_id : int = -1 +var lobby_members : Array = [] +var lobby_member_names : Array = [] + +# Messages to be displayed in the global lobby chat next time its entered +var pending_system_messages : Array = [] + +# Player latencies will be populated as people join and start sending statuses. +var player_latencies : Dictionary = {} +var game_lobby_owner_id : int = -1 + +var is_querying_global_chat := false + +var is_creating_global_chat_lobby : bool = false + +# Regularly ensure that we're connected to the lowest id global chat channel. +var global_chat_check_timer : Timer + +# Regularly all players to make sure they're still connected and to update +# latency readings. +# +# Clients will use this timer to initiate ping exchanges. +# +# The Host will use this timer to send latency status to all clients. +var ping_timer : Timer + +# Random string to map ping requests +var ping_key +var ping_start_time_msec = -1 + +# Received a global chat, should be connected to chat panels to display the new chat message. +signal global_chat_received (username, message) + +# Received a lobby chat, should be connected to lobby chat panels ot display the new chat message. +signal game_lobby_chat_received (username, message) + +# A new game lobby was found, display the game lobby, probably with a join button +signal game_lobby_found (lobby_id, lobby_name) + +# A player has entered or left the lobby +signal lobby_players_updated() + +# A player changed the focus in the character selection screen. Connect to update the ui +signal player_focused_character(player_index, character) + +# A player has confrimed their character selection. Connect to update the ui +signal player_selected_character(player_index, character) + +signal request_character_lobby_update() + +# Clients should update to this state of the character selection screen. Clients should only update +# others' character selections to maintain responsive control over their own UI. +signal character_lobby_update(player_characters, has_player_selected) + +signal character_selection_complete(some_player_has_weapon_slots, selected_characters) + +# A player changed the focus in the weapon selection screen. Connect to update the ui +signal player_focused_weapon(player_index, weapon) + +# A player confirmed their selected weapon. Connect to update the ui +signal player_selected_weapon(player_index, weapon) + +# Client should update to this state of the weapons selection screen. Clients should ignore updates +# To their own state +signal weapon_lobby_update(player_weapons, has_player_selected) + +signal weapon_selection_completed(selected_weapons) + +# Clients shoulda update to this state of the difficulty selection screen. There is only one +# difficulty item to select and it controlled by the host. +signal difficulty_focused(lobby_difficulty) + +# The difficulty was selected, proceed out of the difficulty selection screen +signal difficulty_selected(lobby_difficulty) + +# For shop item focus +signal client_shop_focus_updated(shop_item_string, player_index) + +# For inventory (item or weapon) focus +signal client_shop_focus_inventory_element(element_dict, player_index) + +signal client_shop_buy_item(item_string, player_index) + +signal client_shop_reroll(player_index) + +signal client_shop_go_button_pressed(current_state, player_index) + +signal client_shop_lock_item(item_string, wave_value, player_index) + +signal client_shop_unlock_item(item_string, player_index) + +signal client_shop_combine_weapon(weapon_string, is_upgrade, player_index) + +signal client_shop_discard_weapon(weapon_string, player_index) + +signal client_shop_requested() + +signal shop_lobby_update(shop_dict) + +signal client_status_received(status_dict, player_index) + +signal host_starts_round() + +signal close_popup() + +signal receive_leave_shop() + +signal state_update(state_dict) + +signal client_position(client_position_dict, player_index) + +signal client_menu_focus(client_menu_focus_dict, player_index) + +signal client_main_scene_reroll_button_pressed(player_index) + +signal client_main_scene_choose_upgrade_pressed(upgrade_data_dict, player_index) + +signal client_main_scene_take_button_pressed(player_index) + +signal client_main_scene_discard_button_pressed(player_index) + +signal host_entered_shop() + +func _ready(): + if not Steam.loggedOn(): + return + + var _err + + _err = Steam.connect("lobby_match_list", self, "_on_lobby_match_list") + _err = Steam.connect("lobby_created", self, "_on_lobby_created") + _err = Steam.connect("lobby_joined", self, "_on_lobby_joined") + _err = Steam.connect("lobby_message", self, "_on_lobby_message") + _err = Steam.connect("lobby_chat_update", self, "_on_lobby_chat_update") + _err = Steam.connect("p2p_session_request", self, "_on_p2p_session_request") + _err = Steam.connect("p2p_session_connect_fail", self, "_on_p2p_session_connect_fail") + + global_chat_check_timer = Timer.new() + _err = global_chat_check_timer.connect("timeout", self, "_request_global_chat_search") + add_child(global_chat_check_timer) + global_chat_check_timer.start(3.0) + + ping_timer = Timer.new() + _err = ping_timer.connect("timeout", self, "_ping_timer_timeout") + add_child(ping_timer) + ping_timer.start(2.0) + + _request_global_chat_search() + steam_id = Steam.getSteamID() + + +func _physics_process(_delta : float) -> void: + Steam.run_callbacks() + + if game_lobby_id > 0: + read_p2p_packet() + + +func send_global_chat_message(message : String) -> void: + var _err = Steam.sendLobbyChatMsg(global_chat_lobby_id, message) + + +func send_lobby_chat_message(message : String) -> void: + var _err = Steam.sendLobbyChatMsg(game_lobby_id, message) + + +func _on_lobby_match_list(lobbies: Array) -> void: + var found_global_chat_lobby : bool = false + var min_chat_lobby_id : int = -1 + + for lobby_id in lobbies: + var lobby_data : Dictionary = Steam.getAllLobbyData(lobby_id) + + var is_game_lobby : bool = false + var game_lobby_name : String + + for kvpair_index in lobby_data: + var key = lobby_data[kvpair_index]["key"] + var value = lobby_data[kvpair_index]["value"] + if key.to_lower() == "lobby_type": + if value == GLOBAL_CHAT_TYPE: + if not found_global_chat_lobby: + min_chat_lobby_id = lobby_id + + if lobby_id < min_chat_lobby_id: + min_chat_lobby_id = lobby_id + + found_global_chat_lobby = true + elif value == GAME_LOBBY_TYPE: + is_game_lobby = true + elif key == "lobby_name": + game_lobby_name = value + + if is_game_lobby: + emit_signal("game_lobby_found", lobby_id, game_lobby_name) + + if found_global_chat_lobby: + if min_chat_lobby_id != global_chat_lobby_id: + Steam.joinLobby(min_chat_lobby_id) + else: + if global_chat_lobby_id == -1 and not is_creating_global_chat_lobby: + is_creating_global_chat_lobby = true + Steam.createLobby(Steam.LOBBY_TYPE_INVISIBLE, 250) + + +func leave_game_lobby() -> void: + if game_lobby_id > 0: + lobby_members.clear() + lobby_member_names.clear() + + Steam.leaveLobby(game_lobby_id) + game_lobby_id = 0 + + +func _on_lobby_created(connect: int, created_lobby_id: int) -> void: + print("Created Lobby") + if connect == 1: + if is_creating_global_chat_lobby: + print("Lobby created for chat: ", created_lobby_id) + is_creating_global_chat_lobby = false + global_chat_lobby_id = created_lobby_id + var _err = Steam.setLobbyData(created_lobby_id, "lobby_type", GLOBAL_CHAT_TYPE) + else: + print("Lobby created for game: ", created_lobby_id) + game_lobby_id = created_lobby_id + game_lobby_owner_id = Steam.getLobbyOwner(game_lobby_id) # This should be me but query to make sure + var _err = Steam.setLobbyData(created_lobby_id, "lobby_type", GAME_LOBBY_TYPE) + _err = Steam.setLobbyData(created_lobby_id,"lobby_name", Steam.getFriendPersonaName(Steam.getSteamID())) + _err = Steam.setLobbyData(created_lobby_id,"lobby_status", "OPEN") + + +func _request_global_chat_search() -> void: + Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE) + Steam.addRequestLobbyListStringFilter("lobby_type", GLOBAL_CHAT_TYPE, Steam.LOBBY_COMPARISON_EQUAL) + Steam.requestLobbyList() + + +func request_lobby_search() -> void: + Steam.addRequestLobbyListDistanceFilter(Steam.LOBBY_DISTANCE_FILTER_WORLDWIDE) + Steam.addRequestLobbyListStringFilter("lobby_type", GAME_LOBBY_TYPE, Steam.LOBBY_COMPARISON_EQUAL) + Steam.addRequestLobbyListStringFilter("lobby_status", "OPEN", Steam.LOBBY_COMPARISON_EQUAL) + Steam.requestLobbyList() + + +func _on_lobby_joined(lobby_id: int, _permissions: int, _locked: bool, response: int) -> void: + print("Joined Lobby ", lobby_id) + if response == Steam.CHAT_ROOM_ENTER_RESPONSE_SUCCESS: + var lobby_data : Dictionary = Steam.getAllLobbyData(lobby_id) + for kvpair_index in lobby_data: + var key = lobby_data[kvpair_index]["key"] + var value = lobby_data[kvpair_index]["value"] + print("Lobby Data: ", lobby_id, " ----- ", key, ":", value) + if key.to_lower() == "lobby_type": + if value == GLOBAL_CHAT_TYPE: + global_chat_lobby_id = lobby_id + elif value == GAME_LOBBY_TYPE: + $"/root/BrotogetherOptions".joining_multiplayer_lobby = true + game_lobby_id = lobby_id + game_lobby_owner_id = Steam.getLobbyOwner(lobby_id) + + for member_index in Steam.getNumLobbyMembers(lobby_id): + var member_id = Steam.getLobbyMemberByIndex(lobby_id, member_index) + lobby_members.push_back(member_id) + lobby_member_names.push_back(Steam.getFriendPersonaName(member_id)) + + print("Changing to Character Selection Screen...") + var _error = get_tree().change_scene(MenuData.character_selection_scene) + print("change scene error : ", _error) + + _initiate_ping() + + +func _on_lobby_chat_update(lobby_id: int, change_id: int, _making_change_id: int, chat_state: int) -> void: + # Get the user who has made the lobby change + var changer_name: String = Steam.getFriendPersonaName(change_id) + + # If a player has joined the lobby + if chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_ENTERED: + if lobby_id == game_lobby_id and game_lobby_id != -1: + if not lobby_members.has(change_id): + lobby_members.push_back(change_id) + lobby_member_names.push_back(changer_name) + emit_signal("lobby_players_updated") + print("%s has joined the lobby." % changer_name) + + # Else if a player has left the lobby + elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_LEFT: + if change_id == game_lobby_owner_id: + pending_system_messages.push_back("Host left the lobby") + leave_game_lobby() + var _error = get_tree().change_scene("res://mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.tscn") + print("%s has left the lobby." % changer_name) + + # Else if a player has been kicked + elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_KICKED: + print("%s has been kicked from the lobby." % changer_name) + + # Else if a player has been banned + elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_BANNED: + print("%s has been banned from the lobby." % changer_name) + + # Else there was some unknown change + else: + print("%s did... something." % changer_name) + + +func _on_p2p_session_connect_fail(_steam_id: int, session_error: int) -> void: + if session_error != 0: + print_debug("error connecting to p2p session") + + +func _on_lobby_message(lobby_id : int, user_id : int, buffer : String, _chat_type : int) -> void: + if lobby_id == global_chat_lobby_id: + emit_signal("global_chat_received", Steam.getFriendPersonaName(user_id), buffer) + elif lobby_id == game_lobby_id: + emit_signal("game_lobby_chat_received", Steam.getFriendPersonaName(user_id), buffer) + + +func create_new_game_lobby() -> void: + Steam.createLobby(Steam.LOBBY_TYPE_PUBLIC, 4) + + +# Sends data to a receipient or to all others if target_id is -1 +func send_p2p_packet(data : Dictionary, message_type : int, target_id = -1) -> void: + # Set the send_type and channel + var send_type: int = Steam.P2P_SEND_RELIABLE + var channel: int = message_type + + if game_lobby_id == -1: + + return + + var packet_data: PoolByteArray = PoolByteArray() + # Compress the PoolByteArray we create from our dictionary using the GZIP compression method + + var compressed_data: PoolByteArray = var2bytes(data).compress(File.COMPRESSION_GZIP) + packet_data.append_array(compressed_data) + +# print_debug("[", MessageType.keys()[message_type], "] sending packet size: ", compressed_data.size()) + + if target_id == -1: + for lobby_member_id in lobby_members: + if lobby_member_id != steam_id: + var _err = Steam.sendP2PPacket(lobby_member_id, packet_data, send_type, channel) + else: + if target_id == steam_id: + print("WARNING - Attempting to send data to myself:\n", data) + return + + var _err = Steam.sendP2PPacket(target_id, packet_data, send_type, channel) + + +func read_p2p_packet() -> void: + for channel in MessageType.values(): + var packet_size: int = Steam.getAvailableP2PPacketSize(channel) + + while packet_size > 0: + var packet : Dictionary = Steam.readP2PPacket(packet_size, channel) + + if packet == null or packet.empty(): + packet_size = 0 + continue + + var sender_id : int = packet["remote_steam_id"] + var data : Dictionary = bytes2var(packet["data"].decompress_dynamic(-1, File.COMPRESSION_GZIP)) + + if channel == MessageType.MESSAGE_TYPE_PING: + _respond_to_ping(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_PONG: + _respond_to_pong(data) + elif channel == MessageType.MESSAGE_TYPE_LATENCY_REPORT: + _accept_latency_report(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_PLAYER_STATUS: + _receive_player_statuses(data) + elif channel == MessageType.MESSAGE_TYPE_CHARACTER_FOCUS: + _receive_character_focus(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_CHARACTER_SELECTED: + _receive_character_select(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_CHARACTER_LOBBY_UPDATE: + _receive_character_lobby_update(data) + elif channel == MessageType.MESSAGE_TYPE_WEAPON_FOCUS: + _receive_weapon_focus(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_WEAPON_SELECTED: + _receive_weapon_select(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_WEAPON_LOBBY_UPDATE: + _receive_weapon_lobby_update(data) + elif channel == MessageType.MESSAGE_TYPE_DIFFICULTY_FOCUSED: + _receive_difficutly_focus_update(data) + elif channel == MessageType.MESSAGE_TYPE_DIFFICULTY_PRESSED: + _receive_difficutly_pressed(data) + + # Shop channels + elif channel == MessageType.MESSAGE_TYPE_SHOP_LOBBY_UPDATE: + _receive_shop_update(data) + elif channel == MessageType.MESSAGE_TYPE_SHOP_WEAPON_DISCARD: + _receive_shop_weapon_discard(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_BUY_ITEM: + _receive_shop_buy_item(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_REROLL: + _receive_shop_reroll(sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_COMBINE_WEAPON: + _receive_shop_combine_weapon(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_ITEM_FOCUS: + _receive_shop_item_focus(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_GO_BUTTON_UPDATED: + _receive_shop_go_button_pressed(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_LOCK_ITEM: + _receive_shop_lock_item(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_UNLOCK_ITEM: + _receive_shop_unlock_item(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_CHARACTER_SELECTION_COMPLETED: + _receive_character_selection_completed(data) + elif channel == MessageType.MESSAGE_TYPE_WEAPON_SELECTION_COMPLETED: + _receive_weapon_selection_completed(data) + elif channel == MessageType.MESSAGE_TYPE_HOST_ROUND_START: + _receive_host_round_start() + elif channel == MessageType.MESSAGE_TYPE_SHOP_INVENTORY_ITEM_FOCUS: + _receive_shop_focus_inventory_element(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_SHOP_CLOSE_POPUP: + _receive_shop_close_popup() + elif channel == MessageType.MESSAGE_TYPE_LEAVE_SHOP: + _receive_leave_shop() + + # Main scene channels + elif channel == MessageType.MESSAGE_TYPE_MAIN_STATE: + _receive_game_state(data) + elif channel == MessageType.MESSAGE_TYPE_CLIENT_POSITION: + _receive_client_position(data, sender_id) + + ## Post-wave menu channels + elif channel == MessageType.MESSAGE_TYPE_CLIENT_FOCUS_MAIN_SCENE: + _receive_client_menu_focus(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_MAIN_SCENE_REROLL_BUTTON_PRESSED: + _receive_main_scene_client_reroll_button_pressed(sender_id) + elif channel == MessageType.MESSAGE_TYPE_MAIN_SCENE_CHOOSE_UPGRADE_PRESSED: + _receive_main_scene_client_choose_upgrade_pressed(data, sender_id) + elif channel == MessageType.MESSAGE_TYPE_MAIN_SCENE_TAKE_BUTTON_PRESSED: + _receive_main_scene_client_take_button_pressed(sender_id) + elif channel == MessageType.MESSAGE_TYPE_MAIN_SCENE_DISCARD_BUTTON_PRESSED: + _receive_main_scene_client_discard_button_pressed(sender_id) + elif channel == MessageType.MESSAGE_TYPE_HOST_ENTERED_SHOP: + _receive_host_entered_shop() + + packet_size = Steam.getAvailableP2PPacketSize(channel) + + +func _generate_ping_key() -> String: + var characters : String = "abcdefghijklmnopqrstuvwxyz" + var word : String = "" + for _i in 5: + word += characters[randi() % len(characters)] + return word + + +func _ping_timer_timeout() -> void: + if game_lobby_id == -1 or game_lobby_owner_id == -1: + return + + if steam_id == game_lobby_owner_id: + _send_player_statuses() + else: + _initiate_ping() + + +func _initiate_ping() -> void: + if game_lobby_owner_id == -1: + print("WARNING - Attempting to send ping to unknown lobby host") + return + + ping_key = _generate_ping_key() + ping_start_time_msec = Time.get_ticks_msec() + + var data = { + "PING_KEY": ping_key, + "CURRENT_SCENE": get_tree().current_scene.name, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_PING, game_lobby_owner_id) + + +func _respond_to_ping(data : Dictionary, sender_id : int) -> void: + if not data.has("PING_KEY"): + print("WARNING - Ping sent without key") + return + + emit_signal("client_status_received", data, get_lobby_index_for_player(sender_id)) + send_p2p_packet({"PING_KEY": data["PING_KEY"]}, MessageType.MESSAGE_TYPE_PONG, sender_id) + + +func _respond_to_pong(data : Dictionary) -> void: + if not data.has("PING_KEY"): + print("WARNING - Pong sent without key") + return + + if data["PING_KEY"] != ping_key: + print("WARNING - Ping response key doesn't match") + return + + if ping_start_time_msec == -1: + print("WARNING - Ping request send without starting timer") + return + + var current_time_msec = Time.get_ticks_msec() + var latency_msec = current_time_msec - ping_start_time_msec + + send_p2p_packet({"LATENCY": str(latency_msec)}, MessageType.MESSAGE_TYPE_LATENCY_REPORT, game_lobby_owner_id) + + +func _accept_latency_report(data : Dictionary, sender_id : int) -> void: + if not data.has("LATENCY"): + print("WARNING - Received latency report without result") + return + + player_latencies[sender_id] = int(data["LATENCY"]) + + +func _send_player_statuses() -> void: + if game_lobby_id == -1 or game_lobby_owner_id == -1: + print("WARNING - Attempting to send player latencies to unknown lobby or unknown lobby owner") + return + + if game_lobby_owner_id != steam_id: + print("WARNING - Attempting to send player latencies when not the lobby owner") + return + +# print_debug("sending player latencies : ", player_latencies) + + for player_id in lobby_members: + if player_id == steam_id: + continue + + send_p2p_packet({"PLAYER_LATENCIES": player_latencies}, MessageType.MESSAGE_TYPE_PLAYER_STATUS) + + +func _receive_player_statuses(data : Dictionary) -> void: + if not data.has("PLAYER_LATENCIES"): + print("WARNING - player statuses returned without latencies") + return + + player_latencies = data["PLAYER_LATENCIES"] +# print_debug("received player latencies: ", player_latencies) + + +func _on_p2p_session_request(remote_id: int) -> void: + # Get the requester's name + var this_requester: String = Steam.getFriendPersonaName(remote_id) + print("%s is requesting a P2P session" % this_requester) + + # Accept the P2P session; can apply logic to deny this request if needed + var _err = Steam.acceptP2PSessionWithUser(remote_id) + + # Make the initial handshake + if not game_lobby_owner_id == steam_id: + _initiate_ping() + + +func is_host() -> bool: + return game_lobby_owner_id == steam_id + + +# Character select screen functions +func character_focused(character_key : String) -> void: + if is_host(): + emit_signal("request_character_lobby_update") + else: + send_p2p_packet({"CHARACTER": character_key}, MessageType.MESSAGE_TYPE_CHARACTER_FOCUS, game_lobby_owner_id) + + +func _receive_character_focus(data : Dictionary, sender_id : int) -> void: + if not data.has("CHARACTER"): + print("WARNING - received character focus for player ", sender_id) + return + + if sender_id == -1 or sender_id == steam_id or sender_id == game_lobby_owner_id: + print("WARNING - received character focus for player ", sender_id) + return + + var player_index = get_lobby_index_for_player(sender_id) + if player_index == -1: + return + + emit_signal("player_focused_character", player_index, data["CHARACTER"]) + emit_signal("request_character_lobby_update") + + +func character_selected() -> void: + if is_host(): + emit_signal("request_character_lobby_update") + else: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_CHARACTER_SELECTED, game_lobby_owner_id) + + +func send_character_selection_completed(some_player_has_weapon_slots : bool, currently_focused_characters : Array) -> void: + if not is_host(): + return + + var _err = Steam.setLobbyData(game_lobby_id, "lobby_status", "CLOSED") + var data = { + "HAS_WEAPON_SLOTS" : some_player_has_weapon_slots, + "SELECTED_CHARACTERS": currently_focused_characters, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_CHARACTER_SELECTION_COMPLETED) + + +func _receive_character_selection_completed(data : Dictionary) -> void: + emit_signal("character_selection_complete", data["HAS_WEAPON_SLOTS"], data["SELECTED_CHARACTERS"]) + + +func _receive_character_select(_data : Dictionary, sender_id : int) -> void: + if sender_id == -1 or sender_id == steam_id or sender_id == game_lobby_owner_id: + print("WARNING - received character select for player ", sender_id) + return + + var player_index = get_lobby_index_for_player(sender_id) + if player_index == -1: + return + + emit_signal("player_selected_character", player_index) + emit_signal("request_character_lobby_update") + + +func send_character_lobby_update(currently_focused_characters : Array, has_player_selected: Array) -> void: + var data = { + "SELECTED_CHARACTERS": currently_focused_characters, + "SELECTIONS_CONFIRMED": has_player_selected, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_CHARACTER_LOBBY_UPDATE) + + +func _receive_character_lobby_update(data : Dictionary) -> void: + if not data.has("SELECTED_CHARACTERS") or not data.has("SELECTIONS_CONFIRMED"): + print("WARNING - received lobby player update wihtout player selections", data) + return + + emit_signal("character_lobby_update", data["SELECTED_CHARACTERS"], data["SELECTIONS_CONFIRMED"]) + + +# Weapon select screen functions +func weapon_focused(weapon_key : String) -> void: + if is_host(): + _send_weapon_lobby_update() + else: + send_p2p_packet({"WEAPON": weapon_key}, MessageType.MESSAGE_TYPE_WEAPON_FOCUS, game_lobby_owner_id) + + +func _receive_weapon_focus(data : Dictionary, sender_id : int) -> void: + if not data.has("WEAPON"): + print("WARNING - received weapon focus for player without weapon id ", sender_id) + return + + if sender_id == -1 or sender_id == steam_id or sender_id == game_lobby_owner_id: + print("WARNING - received weapon focus for player ", sender_id) + return + + var player_index = get_lobby_index_for_player(sender_id) + if player_index == -1: + return + + emit_signal("player_focused_weapon", player_index, data["WEAPON"]) + _send_weapon_lobby_update() + + +func weapon_selected() -> void: + if is_host(): + _send_weapon_lobby_update() + else: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_WEAPON_SELECTED, game_lobby_owner_id) + + +func _receive_weapon_select(_data : Dictionary, sender_id : int) -> void: + if sender_id == -1 or sender_id == steam_id or sender_id == game_lobby_owner_id: + print("WARNING - received character select for player ", sender_id) + return + + var player_index = get_lobby_index_for_player(sender_id) + if player_index == -1: + return + + emit_signal("player_selected_weapon", player_index) + + +func _send_weapon_lobby_update() -> void: + if not get_tree().current_scene.name == "WeaponSelection": + print("WARNING - attempting to send weapon selection when no longer in the weapon select; scene actual scene: ", get_tree().current_scene.name) + return + + var weapon_select_scene = get_tree().current_scene + + var currently_focused_weapons = [] + for panel in weapon_select_scene._get_panels(): + var selected_item = panel.item_data + if currently_focused_weapons.size() >= lobby_members.size(): + break + if selected_item == null: + currently_focused_weapons.push_back("RANDOM") + else: + currently_focused_weapons.push_back(weapon_select_scene.weapon_item_to_string(selected_item)) + + var data = { + "SELECTED_WEAPONS": currently_focused_weapons, + "SELECTIONS_CONFIRMED": weapon_select_scene._has_player_selected, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_WEAPON_LOBBY_UPDATE) + + +func _receive_weapon_lobby_update(data : Dictionary) -> void: + if not data.has("SELECTED_WEAPONS") or not data.has("SELECTIONS_CONFIRMED"): + print("WARNING - received lobby player update wihtout player selections", data) + return + + emit_signal("weapon_lobby_update", data["SELECTED_WEAPONS"], data["SELECTIONS_CONFIRMED"]) + + +# Difficulty Selection functions, there are fewer of these since clients don't make choices. +func send_difficulty_lobby_update() -> void: + if not get_tree().current_scene.name == "DifficultySelection": + print("WARNING - attempting to send difficulty selection when no longer in the scene, actual scene: ", get_tree().current_scene.name) + return + + + +func send_weapon_selection_completed(selected_weapons : Array) -> void: + if not is_host(): + return + + var data = { + "SELECTED_WEAPONS" : selected_weapons, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_WEAPON_SELECTION_COMPLETED) + + +func _receive_weapon_selection_completed(data : Dictionary) -> void: + emit_signal("weapon_selection_completed", data["SELECTED_WEAPONS"]) + + +func difficulty_focused() -> void: + var difficulty_selection_scene = get_tree().current_scene + var focused_difficulty : int = difficulty_selection_scene._get_panels()[0].item_data.value + + var data = { + "FOCUSED_DIFFICULTY": focused_difficulty, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_DIFFICULTY_FOCUSED) + + +func _receive_difficutly_focus_update(data : Dictionary) -> void: + if not data.has("FOCUSED_DIFFICULTY"): + print("WARNING - received lobby player update wihtout difficulty focus; data:", data) + return + + if not get_tree().current_scene.name == "DifficultySelection": + print("WARNING - Received difficulty focus update when not in the current scene, current scene:", get_tree().current_scene.name) + return + + emit_signal("difficulty_focused", data["FOCUSED_DIFFICULTY"]) + + +func difficulty_pressed() -> void: + var difficulty_selection_scene = get_tree().current_scene + var selected_difficulty : int = difficulty_selection_scene._get_panels()[0].item_data.value + + var data = { + "SELECTED_DIFFICULTY": selected_difficulty, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_DIFFICULTY_PRESSED) + + +func _receive_difficutly_pressed(data : Dictionary) -> void: + if not data.has("SELECTED_DIFFICULTY"): + print("WARNING - received lobby player update wihtout difficulty focus; data:", data) + return + + if not get_tree().current_scene.name == "DifficultySelection": + print("WARNING - Received difficulty focus update when not in the current scene, current scene:", get_tree().current_scene.name) + return + + emit_signal("difficulty_selected", data["SELECTED_DIFFICULTY"]) + +# returns -1 if the player isn't in the lobby +func get_lobby_index_for_player(player_id : int) -> int: + for index in lobby_members.size(): + if lobby_members[index] == player_id: + return index + + return -1 + + +func request_shop_update(changed_shop_player_indeces : Array = []) -> void: + emit_signal("client_shop_requested", changed_shop_player_indeces) + + +func send_shop_update(shop_data : Dictionary) -> void: + send_p2p_packet(shop_data, MessageType.MESSAGE_TYPE_SHOP_LOBBY_UPDATE) + + +func _receive_shop_update(data : Dictionary) -> void: + emit_signal("shop_lobby_update", data) + + +func shop_item_focused(shop_item_string : String, force_focus_for_players : Array = []) -> void: + if is_host(): + request_shop_update(force_focus_for_players) + else: + var data = { + "FOCUSED_ITEM": shop_item_string, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_SHOP_ITEM_FOCUS, game_lobby_owner_id) + + +func _receive_shop_item_focus(data : Dictionary, sender_id : int) -> void: + if not is_host(): + print("WARNING - received shop item focus as non-host, ignroing; data:", data) + return + + if not data.has("FOCUSED_ITEM"): + print("WARNING - received shop item focus message without focused item; data:", data) + return + + var player_index : int = get_lobby_index_for_player(sender_id) + + emit_signal("client_shop_focus_updated", data["FOCUSED_ITEM"], player_index) + + +func shop_go_button_pressed(current_state : bool) -> void: + if is_host(): + request_shop_update() + else: + var data = { + "CURRENT_STATE": current_state, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_SHOP_GO_BUTTON_UPDATED, game_lobby_owner_id) + + +func _receive_shop_go_button_pressed(data : Dictionary, sender_id : int) -> void: + if not is_host(): + print("WARNING - received shop go button press as non-host, ignroing; data:", data) + return + + if not data.has("CURRENT_STATE"): + print("WARNING - received shop go button press without CURRENT_STATE; data:", data) + return + + var player_index : int = get_lobby_index_for_player(sender_id) + emit_signal("client_shop_go_button_pressed", data["CURRENT_STATE"], player_index) + + +func shop_go_button_exited() -> void: + shop_go_button_pressed(false) + + +func shop_buy_item(item_string : String, player_index : int) -> void: + if is_host(): + request_shop_update([player_index]) + else: + var data = { + "ITEM": item_string, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_SHOP_BUY_ITEM, game_lobby_owner_id) + + +func _receive_shop_buy_item(data : Dictionary, sender_id : int) -> void: + if not is_host(): + print("WARNING - received shop item buy as non-host, ignroing; data:", data) + return + + if not data.has("ITEM"): + print("WARNING - received shop buy item without ITEM; data:", data) + return + + var player_index : int = get_lobby_index_for_player(sender_id) + emit_signal("client_shop_buy_item", data["ITEM"], player_index) + + +func shop_reroll(player_index : int) -> void: + if is_host(): + request_shop_update([player_index]) + else: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_SHOP_REROLL, game_lobby_owner_id) + + +func _receive_shop_reroll(sender_id : int) -> void: + var player_index : int = get_lobby_index_for_player(sender_id) + if not is_host(): + print("WARNING : client received reroll from client signal ", player_index) + return + + emit_signal("client_shop_reroll", player_index) + + +func shop_weapon_discard(weapon_string : String, player_index : int) -> void: + if is_host(): + request_shop_update([player_index]) + else: + var data = { + "WEAPON": weapon_string, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_SHOP_WEAPON_DISCARD, game_lobby_owner_id) + + +func _receive_shop_weapon_discard(data : Dictionary, sender_id : int) -> void: + if not is_host(): + print("WARNING - received shop discard as non-host, ignroing; data:", data) + return + + if not data.has("WEAPON"): + print("WARNING - received shop discard without WEAPON; data:", data) + return + + var player_index : int = get_lobby_index_for_player(sender_id) + emit_signal("client_shop_discard_weapon", data["WEAPON"], player_index) + + +func shop_lock_item(item_string : String, wave_value : int) -> void: + if is_host(): + request_shop_update() + else: + var data = { + "ITEM": item_string, + "WAVE_VALUE": wave_value, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_SHOP_LOCK_ITEM, game_lobby_owner_id) + + +func _receive_shop_lock_item(data : Dictionary, sender_id : int) -> void: + if not is_host(): + print("WARNING - received shop lock as non-host, ignroing; data:", data) + return + + if not data.has("ITEM") or not data.has("WAVE_VALUE"): + print("WARNING - received shop item lock without item or wave_value; data:", data) + return + + var player_index : int = get_lobby_index_for_player(sender_id) + emit_signal("client_shop_lock_item", data["ITEM"], data["WAVE_VALUE"], player_index) + + +func shop_unlock_item(item_string : String) -> void: + if is_host(): + request_shop_update() + else: + var data = { + "ITEM": item_string, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_SHOP_UNLOCK_ITEM, game_lobby_owner_id) + + +func _receive_shop_unlock_item(data : Dictionary, sender_id : int) -> void: + if not is_host(): + print("WARNING - received shop unlock as non-host, ignroing; data:", data) + return + + if not data.has("ITEM"): + print("WARNING - received shop item unlock without item; data:", data) + return + + var player_index : int = get_lobby_index_for_player(sender_id) + emit_signal("client_shop_unlock_item", data["ITEM"], player_index) + + +func shop_combine_weapon(weapon_string : String, is_upgrade : bool, player_index : int) -> void: + if is_host(): + request_shop_update([player_index]) + else: + var data = { + "WEAPON": weapon_string, + "IS_UPGRADE": is_upgrade, + } + + send_p2p_packet(data, MessageType.MESSAGE_TYPE_SHOP_COMBINE_WEAPON, game_lobby_owner_id) + + +func _receive_shop_combine_weapon(data : Dictionary, sender_id : int) -> void: + if not is_host(): + print("WARNING - received shop weapon combine as non-host, ignroing; data:", data) + return + + if not data.has("WEAPON") or not data.has("IS_UPGRADE"): + print("WARNING - received shop weapon combine without weapon or is_upgrade; data:", data) + return + + var player_index : int = get_lobby_index_for_player(sender_id) + emit_signal("client_shop_combine_weapon", data["WEAPON"], data["IS_UPGRADE"], player_index) + + +func send_round_start() -> void: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_HOST_ROUND_START) + + +func _receive_host_round_start() -> void: + emit_signal("host_starts_round") + + +func shop_focus_inventory_element(inventory_item_dict : Dictionary) -> void: + if is_host(): + request_shop_update() + else: + send_p2p_packet(inventory_item_dict, MessageType.MESSAGE_TYPE_SHOP_INVENTORY_ITEM_FOCUS, game_lobby_owner_id) + + +func _receive_shop_focus_inventory_element(data : Dictionary, sender_id : int) -> void: + emit_signal("client_shop_focus_inventory_element", data, get_lobby_index_for_player(sender_id)) + + +func get_my_index() -> int: + return get_lobby_index_for_player(steam_id) + + +func request_close_client_shop_popup(player_index : int) -> void: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_SHOP_CLOSE_POPUP, lobby_members[player_index]) + + +func _receive_shop_close_popup() -> void: + emit_signal("close_popup") + + +func leave_shop() -> void: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_LEAVE_SHOP) + + +func _receive_leave_shop() -> void: + emit_signal("receive_leave_shop") + + +func send_game_state(state_dict : Dictionary) -> void: + if is_host(): + send_p2p_packet(state_dict, MessageType.MESSAGE_TYPE_MAIN_STATE) + + +func _receive_game_state(state_dict : Dictionary) -> void: + if is_host(): + print("ERR - HOST SHOULD NOT BE GETTING STATE UPDATES") + return + + emit_signal("state_update", state_dict) + + +func send_client_position(client_position_dict : Dictionary) -> void: + send_p2p_packet(client_position_dict, MessageType.MESSAGE_TYPE_CLIENT_POSITION) + + +func _receive_client_position(data : Dictionary, sender_id : int) -> void: + emit_signal("client_position", data, get_lobby_index_for_player(sender_id)) + + +func send_client_menu_focus(client_menu_dict : Dictionary) -> void: + send_p2p_packet(client_menu_dict, MessageType.MESSAGE_TYPE_CLIENT_FOCUS_MAIN_SCENE) + + +func _receive_client_menu_focus(data : Dictionary, sender_id: int) -> void: + emit_signal("client_menu_focus", data, get_lobby_index_for_player(sender_id)) + + +func send_main_scene_client_reroll_button_ressed() -> void: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_MAIN_SCENE_REROLL_BUTTON_PRESSED) + + +func _receive_main_scene_client_reroll_button_pressed(sender_id : int) -> void: + emit_signal("client_main_scene_reroll_button_pressed", get_lobby_index_for_player(sender_id)) + + +func send_main_scene_client_choose_upgrade_pressed(upgrade_data_dict : Dictionary) -> void: + send_p2p_packet(upgrade_data_dict, MessageType.MESSAGE_TYPE_MAIN_SCENE_CHOOSE_UPGRADE_PRESSED) + + +func _receive_main_scene_client_choose_upgrade_pressed(upgrade_data_dict : Dictionary, sender_id : int) -> void: + emit_signal("client_main_scene_choose_upgrade_pressed", upgrade_data_dict, get_lobby_index_for_player(sender_id)) + + +func send_main_scene_client_take_button_pressed() -> void: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_MAIN_SCENE_TAKE_BUTTON_PRESSED) + + +func _receive_main_scene_client_take_button_pressed(sender_id : int) -> void: + emit_signal("client_main_scene_take_button_pressed", get_lobby_index_for_player(sender_id)) + + +func send_main_scene_client_discard_button_pressed() -> void: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_MAIN_SCENE_DISCARD_BUTTON_PRESSED) + + +func _receive_main_scene_client_discard_button_pressed(sender_id : int) -> void: + emit_signal("client_main_scene_discard_button_pressed", get_lobby_index_for_player(sender_id)) + + +func send_host_entered_shop() -> void: + send_p2p_packet({}, MessageType.MESSAGE_TYPE_HOST_ENTERED_SHOP) + + +func _receive_host_entered_shop() -> void: + emit_signal("host_entered_shop") diff --git a/src/mods-unpacked/Brogether-Brogether/ui/button.tscn b/src/mods-unpacked/Brogether-Brogether/ui/button.tscn new file mode 100644 index 0000000..4ed55bf --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/button.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://ui/menus/global/my_menu_button.gd" type="Script" id=1] + +[node name="GoButton" type="Button"] +margin_top = 1522.0 +margin_right = 411.0 +margin_bottom = 1587.0 +focus_neighbour_top = NodePath("../../VBoxContainer/HBoxContainer2/ShopItemsContainer/ShopItem4/PanelContainer/MarginContainer/VBoxContainer/BuyButton") +size_flags_horizontal = 3 +size_flags_vertical = 8 +text = "MORE_BADDIES" +script = ExtResource( 1 ) diff --git a/src/mods-unpacked/Brogether-Brogether/ui/chat/chat_message.gd b/src/mods-unpacked/Brogether-Brogether/ui/chat/chat_message.gd new file mode 100644 index 0000000..05cf081 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/chat/chat_message.gd @@ -0,0 +1,12 @@ +extends HBoxContainer + +var username := "Test Username" +var message := "Test Message" + +onready var username_label = $"%Username" +onready var message_label = $"%Message" + + +func _ready(): + username_label.text = username + message_label.text = message diff --git a/src/mods-unpacked/Brogether-Brogether/ui/chat/chat_message.tscn b/src/mods-unpacked/Brogether-Brogether/ui/chat/chat_message.tscn new file mode 100644 index 0000000..2de603b --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/chat/chat_message.tscn @@ -0,0 +1,30 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://mods-unpacked/Brogether-Brogether/ui/chat/chat_message.gd" type="Script" id=1] +[ext_resource path="res://resources/themes/panel/stat_panel.tres" type="StyleBox" id=2] + +[node name="ChatMessage" type="HBoxContainer"] +margin_right = 710.0 +margin_bottom = 45.0 +size_flags_horizontal = 3 +custom_constants/separation = 35 +script = ExtResource( 1 ) + +[node name="Username" type="Label" parent="."] +unique_name_in_owner = true +margin_top = 45.0 +margin_right = 64.0 +margin_bottom = 64.0 +custom_styles/normal = ExtResource( 2 ) +text = "username" + +[node name="Message" type="Label" parent="."] +unique_name_in_owner = true +margin_left = 99.0 +margin_right = 710.0 +margin_bottom = 109.0 +size_flags_horizontal = 3 +custom_styles/normal = ExtResource( 2 ) +text = "Very Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long TextVery Long Text +" +autowrap = true diff --git a/src/mods-unpacked/Brogether-Brogether/ui/chat_panel.tscn b/src/mods-unpacked/Brogether-Brogether/ui/chat_panel.tscn new file mode 100644 index 0000000..2017d91 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/chat_panel.tscn @@ -0,0 +1,73 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/menus/global/arrow_right.png" type="Texture" id=3] +[ext_resource path="res://ui/menus/global/arrow_left.png" type="Texture" id=4] + +[node name="ChatPanel" type="PanelContainer"] +margin_left = 1540.0 +margin_right = 1853.0 +margin_bottom = 500.0 +rect_min_size = Vector2( 0, 500 ) + +[node name="MarginContainer" type="MarginContainer" parent="."] +margin_left = 7.0 +margin_top = 7.0 +margin_right = 343.0 +margin_bottom = 493.0 +custom_constants/margin_right = 25 +custom_constants/margin_top = 25 +custom_constants/margin_left = 25 +custom_constants/margin_bottom = 25 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +margin_left = 25.0 +margin_top = 25.0 +margin_right = 311.0 +margin_bottom = 461.0 +custom_constants/separation = 25 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +margin_right = 286.0 +margin_bottom = 34.0 +custom_constants/separation = 15 +alignment = 1 + +[node name="LeftArrow" type="TextureButton" parent="MarginContainer/VBoxContainer/HBoxContainer"] +margin_right = 33.0 +margin_bottom = 34.0 +texture_normal = ExtResource( 4 ) + +[node name="ChatTitle" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"] +margin_left = 48.0 +margin_top = 1.0 +margin_right = 238.0 +margin_bottom = 32.0 +rect_min_size = Vector2( 190, 0 ) +text = "Global Chat" +clip_text = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="RightArrow" type="TextureButton" parent="MarginContainer/VBoxContainer/HBoxContainer"] +margin_left = 253.0 +margin_right = 286.0 +margin_bottom = 34.0 +texture_normal = ExtResource( 3 ) + +[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer"] +margin_top = 59.0 +margin_right = 286.0 +margin_bottom = 387.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +scroll_horizontal_enabled = false + +[node name="ChatMessages" type="VBoxContainer" parent="MarginContainer/VBoxContainer/ScrollContainer"] +margin_right = 286.0 +size_flags_horizontal = 3 + +[node name="ChatInput" type="LineEdit" parent="MarginContainer/VBoxContainer"] +margin_top = 412.0 +margin_right = 286.0 +margin_bottom = 436.0 diff --git a/src/mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.gd b/src/mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.gd new file mode 100644 index 0000000..12badac --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.gd @@ -0,0 +1,40 @@ +extends VBoxContainer + +var health_bar_map = {} + +const HealthBar = preload("res://ui/hud/ui_progress_bar.tscn") + +func init(tracked_players:Dictionary) -> void: + var life_bar = $TemplateLifeBar + + for tracked_player_id in tracked_players: + var health_bar = life_bar.duplicate() + add_child(health_bar) + health_bar_map[tracked_player_id] = health_bar + + remove_child(life_bar) + update_health_bars(tracked_players) + +func update_health_bars(tracked_players:Dictionary) -> void: + for tracked_player_id in tracked_players: + var player = tracked_players[tracked_player_id] + var health_bar = health_bar_map[tracked_player_id] + + if player.has("username"): + var name_label = health_bar.get_node("NameLabel") + name_label.text = str(player.username) + + var current_health = 15 + var max_health = 15 + + + if player.has("player") and is_instance_valid(player.player): + current_health = player.player.current_stats.health + max_health = player.player.max_stats.health + + + if max_health != 0: + health_bar.update_value(current_health, max_health) + + var life_label = health_bar.get_node("MarginContainer/LifeLabel") + life_label.text = str(max(current_health, 0.0)) + " / " + str(max_health) diff --git a/src/mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.tscn b/src/mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.tscn new file mode 100644 index 0000000..068f584 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.tscn @@ -0,0 +1,53 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/hud/ui_progress_bar.tscn" type="PackedScene" id=1] +[ext_resource path="res://mods-unpacked/Brogether-Brogether/ui/health_tracker/health_tracker.gd" type="Script" id=3] + +[node name="HealthTracker" type="VBoxContainer"] +margin_left = 30.0 +margin_top = 300.0 +margin_right = 350.0 +margin_bottom = 452.0 +script = ExtResource( 3 ) + +[node name="TemplateLifeBar" parent="." instance=ExtResource( 1 )] +margin_bottom = 48.0 +size_flags_horizontal = 0 +tint_progress = Color( 0.721569, 0, 0, 1 ) +progress_color = Color( 0.721569, 0, 0, 1 ) + +[node name="NameLabel" type="Label" parent="TemplateLifeBar"] +anchor_left = 1.0 +anchor_right = 1.0 +margin_left = 12.0 +margin_top = 8.0 +margin_right = 308.0 +margin_bottom = 39.0 +grow_horizontal = 0 +text = "8 / 8" +valign = 1 +uppercase = true + +[node name="MarginContainer" type="MarginContainer" parent="TemplateLifeBar"] +anchor_right = 1.0 +anchor_bottom = 1.0 +custom_constants/margin_right = 12 +custom_constants/margin_top = 7 +custom_constants/margin_left = 12 +custom_constants/margin_bottom = 7 + +[node name="LifeLabel" type="Label" parent="TemplateLifeBar/MarginContainer"] +margin_left = 12.0 +margin_top = 8.0 +margin_right = 308.0 +margin_bottom = 39.0 +grow_horizontal = 0 +text = "8 / 8" +align = 1 +valign = 1 +uppercase = true +__meta__ = { +"_edit_use_anchors_": false +} + +[editable path="TemplateLifeBar"] diff --git a/src/mods-unpacked/Brogether-Brogether/ui/lobby_entry.gd b/src/mods-unpacked/Brogether-Brogether/ui/lobby_entry.gd new file mode 100644 index 0000000..b3baa02 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/lobby_entry.gd @@ -0,0 +1,17 @@ +extends HBoxContainer + +var lobby_id +var lobby_name + +var steam_connection + +onready var lobby_name_label = $"%LobbyName" + + +func _ready(): + steam_connection = $"/root/SteamConnection" + lobby_name_label.text = lobby_name + + +func _on_join_button_pressed(): + Steam.joinLobby(lobby_id) diff --git a/src/mods-unpacked/Brogether-Brogether/ui/lobby_entry.tscn b/src/mods-unpacked/Brogether-Brogether/ui/lobby_entry.tscn new file mode 100644 index 0000000..85752e0 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/lobby_entry.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://mods-unpacked/Brogether-Brogether/ui/lobby_entry.gd" type="Script" id=1] + +[node name="HBoxContainer" type="HBoxContainer"] +margin_right = 789.0 +margin_bottom = 65.0 +custom_constants/separation = 30 +script = ExtResource( 1 ) + +[node name="JoinButton" type="Button" parent="."] +margin_right = 35.0 +margin_bottom = 65.0 +text = "Join" + +[node name="LobbyName" type="Label" parent="."] +unique_name_in_owner = true +margin_left = 65.0 +margin_top = 25.0 +margin_right = 145.0 +margin_bottom = 39.0 +text = "Lobby Name" +align = 1 + +[connection signal="pressed" from="JoinButton" to="." method="_on_join_button_pressed"] diff --git a/src/mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.gd b/src/mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.gd new file mode 100644 index 0000000..f2f4d09 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.gd @@ -0,0 +1,103 @@ +extends Control + +# must be greater than 1024 +var SERVER_PORT = 11111 +var MAX_PLAYERS = 5 + +const ChatMessage = preload("res://mods-unpacked/Brogether-Brogether/ui/chat/chat_message.tscn") +const LobbyEntry = preload("res://mods-unpacked/Brogether-Brogether/ui/lobby_entry.tscn") + +const DirectConnection = preload("res://mods-unpacked/Brogether-Brogether/networking/direct_connection.gd") + +var GameController = load("res://mods-unpacked/Brogether-Brogether/networking/game_controller.gd") + +var direct_connection +var game_controller + +var DEBUG = false + +onready var chat_messages = $"%ChatMessages" +onready var lobbies_list = $"%Lobbies" +onready var chat_input : LineEdit = $"%ChatInput" +onready var create_lobby_button : Button = $"%CreateLobbyButton" + +# Manual on ready vars +var steam_connection +var brotatogether_options + +# Called when the node enters the scene tree for the first time. +func _ready(): + steam_connection = $"/root/SteamConnection" + steam_connection.connect("global_chat_received", self, "_received_global_chat") + steam_connection.connect("game_lobby_found", self, "_game_lobby_found") + + brotatogether_options = $"/root/BrotogetherOptions" + + for message in steam_connection.pending_system_messages: + var new_message_node = ChatMessage.instance() + new_message_node.message = message + new_message_node.username = "SYSTEM" + chat_messages.add_child(new_message_node) + steam_connection.pending_system_messages.clear() + create_lobby_button.grab_focus() + CoopService.clear_coop_players() + + # var solo_test_btn = Button.new() + # solo_test_btn.text = "Solo Test" + # create_lobby_button.get_parent().add_child(solo_test_btn) + # solo_test_btn.connect("pressed", self, "_on_solo_test_button_pressed") + + +func _input(event:InputEvent)->void : + manage_back(event) + + +func manage_back(event:InputEvent)->void : + if event.is_action_pressed("ui_cancel"): + RunData.current_zone = 0 + RunData.reload_music = false + var _error = get_tree().change_scene(MenuData.title_screen_scene) + + +func _on_back_button_pressed(): + RunData.reload_music = false + var _error = get_tree().change_scene(MenuData.title_screen_scene) + + +func _on_chat_input_text_entered(message): + steam_connection.send_global_chat_message(message) + chat_input.clear() + + +func _received_global_chat(user, message) -> void: + var new_message_node = ChatMessage.instance() + new_message_node.message = message + new_message_node.username = user + chat_messages.add_child(new_message_node) + + +func _on_solo_test_button_pressed() -> void: + steam_connection.game_lobby_id = 1 + steam_connection.game_lobby_owner_id = steam_connection.steam_id + steam_connection.lobby_members = [steam_connection.steam_id] + steam_connection.lobby_member_names = [Steam.getFriendPersonaName(steam_connection.steam_id)] + brotatogether_options.is_solo_test = true + brotatogether_options.joining_multiplayer_lobby = true + var _error = get_tree().change_scene(MenuData.character_selection_scene) + + +func _on_create_lobby_button_pressed(): + steam_connection.create_new_game_lobby() + + +func _on_refresh_lobbies_button_pressed(): + for child in lobbies_list.get_children(): + lobbies_list.remove_child(child) + steam_connection.request_lobby_search() + + +func _game_lobby_found(lobby_id, lobby_name) -> void: + var new_lobby_entry = LobbyEntry.instance() + new_lobby_entry.lobby_id = lobby_id + new_lobby_entry.lobby_name = lobby_name + lobbies_list.add_child(new_lobby_entry) diff --git a/src/mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.tscn b/src/mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.tscn new file mode 100644 index 0000000..028b006 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.tscn @@ -0,0 +1,107 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://resources/themes/base_theme.tres" type="Theme" id=2] +[ext_resource path="res://mods-unpacked/Brogether-Brogether/ui/multiplayer_menu.gd" type="Script" id=3] +[ext_resource path="res://ui/menus/global/my_menu_button.gd" type="Script" id=4] + +[node name="MultiplayerMenu" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = -3.0 +margin_bottom = 10.0 +theme = ExtResource( 2 ) +script = ExtResource( 3 ) + +[node name="TitleLabel" type="Label" parent="."] +self_modulate = Color( 1, 0.6, 0.1, 1 ) +margin_left = 300.0 +margin_top = 48.0 +margin_right = 1000.0 +margin_bottom = 148.0 +text = "Brogether" + + +[node name="BackButton" type="Button" parent="."] +unique_name_in_owner = true +self_modulate = Color( 1, 1, 1, 0.862745 ) +margin_left = 48.0 +margin_top = 48.0 +margin_right = 233.0 +margin_bottom = 99.0 +size_flags_horizontal = 4 +size_flags_vertical = 4 +text = "MENU_BACK" +script = ExtResource( 4 ) + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +margin_left = 300.0 +margin_top = 173.0 +margin_right = 1883.0 +margin_bottom = 1066.0 + +[node name="ChatContainer2" type="VBoxContainer" parent="HBoxContainer"] +margin_right = 789.0 +margin_bottom = 893.0 +size_flags_horizontal = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ChatContainer2"] +margin_right = 789.0 +margin_bottom = 893.0 +custom_constants/separation = 28 + +[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/ChatContainer2/VBoxContainer"] +margin_right = 789.0 +margin_bottom = 65.0 + +[node name="CreateLobbyButton" type="Button" parent="HBoxContainer/ChatContainer2/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +margin_right = 303.0 +margin_bottom = 65.0 +text = "Create Lobby" + +[node name="RefreshLobbiesButton" type="Button" parent="HBoxContainer/ChatContainer2/VBoxContainer/HBoxContainer"] +margin_left = 307.0 +margin_right = 666.0 +margin_bottom = 65.0 +text = "Refresh Lobbies" + +[node name="ScrollContainer" type="ScrollContainer" parent="HBoxContainer/ChatContainer2/VBoxContainer"] +margin_top = 93.0 +margin_right = 789.0 +margin_bottom = 893.0 +rect_min_size = Vector2( 0, 800 ) + +[node name="Lobbies" type="VBoxContainer" parent="HBoxContainer/ChatContainer2/VBoxContainer/ScrollContainer"] +unique_name_in_owner = true +margin_right = 789.0 +size_flags_horizontal = 3 +custom_constants/separation = 15 + +[node name="ChatContainer" type="VBoxContainer" parent="HBoxContainer"] +margin_left = 793.0 +margin_right = 1583.0 +margin_bottom = 893.0 +size_flags_horizontal = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="HBoxContainer/ChatContainer"] +margin_right = 790.0 +margin_bottom = 800.0 +rect_min_size = Vector2( 0, 800 ) + +[node name="ChatMessages" type="VBoxContainer" parent="HBoxContainer/ChatContainer/ScrollContainer"] +unique_name_in_owner = true +margin_right = 790.0 +size_flags_horizontal = 3 +custom_constants/separation = 15 + +[node name="ChatInput" type="LineEdit" parent="HBoxContainer/ChatContainer"] +unique_name_in_owner = true +margin_top = 804.0 +margin_right = 790.0 +margin_bottom = 858.0 +placeholder_text = "Type to Chat..." + +[connection signal="pressed" from="BackButton" to="." method="_on_back_button_pressed"] +[connection signal="pressed" from="HBoxContainer/ChatContainer2/VBoxContainer/HBoxContainer/CreateLobbyButton" to="." method="_on_create_lobby_button_pressed"] +[connection signal="pressed" from="HBoxContainer/ChatContainer2/VBoxContainer/HBoxContainer/RefreshLobbiesButton" to="." method="_on_refresh_lobbies_button_pressed"] +[connection signal="text_entered" from="HBoxContainer/ChatContainer/ChatInput" to="." method="_on_chat_input_text_entered"] diff --git a/src/mods-unpacked/Brogether-Brogether/ui/player_selections.gd b/src/mods-unpacked/Brogether-Brogether/ui/player_selections.gd new file mode 100644 index 0000000..67afcff --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/player_selections.gd @@ -0,0 +1,102 @@ +extends HBoxContainer + +signal ready_toggled(is_ready) + +onready var selected_character_element := get_node("%SelectedCharacter") +onready var selected_weapon_element := get_node("%SelectedWeapon") +onready var selected_danger_element := get_node("%SelectedDanger") +onready var username_label := get_node("%Username") +onready var ready_toggle := get_node("%ReadyToggle") + +var unpicked_icon = load("res://items/global/random_icon.png") + + +func set_player_name(name:String) -> void: + username_label.text = name + + +func disable_selections() -> void: + selected_character_element.disabled = true + selected_weapon_element.disabled = true + selected_danger_element.disabled = true + + +func hide_ready_toggle() -> void: + ready_toggle.hide() + + +func disable_ready_toggle() -> void: + ready_toggle.disabled = true + + +func _on_SelectedCharacter_element_pressed(_element): + $"/root/GameController".back_to_lobby = true + var _error = get_tree().change_scene(MenuData.character_selection_scene) + + +func _on_SelectedWeapon_element_pressed(_element): + $"/root/GameController".back_to_lobby = true + var _error = get_tree().change_scene(MenuData.weapon_selection_scene) + + +func set_player_selections(selections_dict : Dictionary, is_me:bool = false, lock_danger: bool = false) -> void: + var weapon_required = true + if selections_dict.has("character"): + var character_id = selections_dict["character"] + for character in ItemService.characters: + if character.my_id == character_id: + selected_character_element.set_element(character) + if character.starting_weapons.empty(): + weapon_required = false + break + if is_me: + if weapon_required: + selected_weapon_element.disabled = false + else: + selected_weapon_element.disabled = true + selected_danger_element.disabled = false + else: + selected_character_element.icon = unpicked_icon + selected_weapon_element.disabled = true + selected_danger_element.disabled = true + + if selections_dict.has("weapon"): + var weapon_id = selections_dict["weapon"] + for weapon in ItemService.weapons: + if weapon.my_id == weapon_id: + selected_weapon_element.set_element(weapon) + break + if is_me: + selected_danger_element.disabled = false + else: + selected_weapon_element.icon = unpicked_icon + selected_danger_element.disabled = weapon_required + + if selections_dict.has("danger"): + var danger_id = selections_dict["danger"] + for difficulty in ItemService.difficulties: + if difficulty.my_id == danger_id: + selected_danger_element.set_element(difficulty) + break + else: + selected_danger_element.icon = unpicked_icon + + if selections_dict.has("ready"): + ready_toggle.pressed = selections_dict["ready"] + + if lock_danger: + selected_danger_element.disabled = true + + if is_me: + selected_character_element.disabled = false + if not is_me: + disable_selections() + + +func _on_select_danger_element_pressed(_element): + $"/root/GameController".back_to_lobby = true + var _error = get_tree().change_scene(MenuData.difficulty_selection_scene) + + +func _on_ReadyToggle_toggled(button_pressed): + emit_signal("ready_toggled", button_pressed) diff --git a/src/mods-unpacked/Brogether-Brogether/ui/player_selections.tscn b/src/mods-unpacked/Brogether-Brogether/ui/player_selections.tscn new file mode 100644 index 0000000..edf69d2 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/player_selections.tscn @@ -0,0 +1,67 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://mods-unpacked/Brogether-Brogether/ui/player_selections.gd" type="Script" id=1] +[ext_resource path="res://items/global/inventory_element.tscn" type="PackedScene" id=2] +[ext_resource path="res://items/global/random_icon.png" type="Texture" id=5] + +[node name="PlayerSelections" type="HBoxContainer"] +margin_right = 1120.0 +margin_bottom = 105.0 +custom_constants/separation = 5 +script = ExtResource( 1 ) + +[node name="SelectedCharacter" parent="." instance=ExtResource( 2 )] +unique_name_in_owner = true +margin_right = 108.0 +margin_bottom = 105.0 +rect_min_size = Vector2( 105, 105 ) +size_flags_horizontal = 0 +size_flags_vertical = 0 +icon = ExtResource( 5 ) +expand_icon = false + +[node name="SelectedWeapon" parent="." instance=ExtResource( 2 )] +unique_name_in_owner = true +margin_left = 113.0 +margin_right = 218.0 +margin_bottom = 105.0 +rect_min_size = Vector2( 105, 105 ) +size_flags_horizontal = 0 +size_flags_vertical = 0 +icon = ExtResource( 5 ) + +[node name="SelectedDanger" parent="." instance=ExtResource( 2 )] +unique_name_in_owner = true +margin_left = 223.0 +margin_right = 328.0 +margin_bottom = 105.0 +rect_min_size = Vector2( 105, 105 ) +size_flags_horizontal = 0 +size_flags_vertical = 0 +icon = ExtResource( 5 ) + +[node name="Username" type="Label" parent="."] +unique_name_in_owner = true +margin_left = 333.0 +margin_top = 30.0 +margin_right = 970.0 +margin_bottom = 75.0 +rect_min_size = Vector2( 500, 0 ) +size_flags_horizontal = 3 +text = "USERNAME" +clip_text = true + +[node name="ReadyToggle" type="CheckButton" parent="."] +unique_name_in_owner = true +margin_left = 975.0 +margin_top = 32.0 +margin_right = 1120.0 +margin_bottom = 72.0 +size_flags_vertical = 4 +text = "Ready" +align = 1 + +[connection signal="element_pressed" from="SelectedCharacter" to="." method="_on_SelectedCharacter_element_pressed"] +[connection signal="element_pressed" from="SelectedWeapon" to="." method="_on_SelectedWeapon_element_pressed"] +[connection signal="element_pressed" from="SelectedDanger" to="." method="_on_select_danger_element_pressed"] +[connection signal="toggled" from="ReadyToggle" to="." method="_on_ReadyToggle_toggled"] diff --git a/src/mods-unpacked/Brogether-Brogether/ui/shop/count_label.tscn b/src/mods-unpacked/Brogether-Brogether/ui/shop/count_label.tscn new file mode 100644 index 0000000..9817859 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/shop/count_label.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=8 format=2] + +[ext_resource path="res://resources/fonts/raw/NotoSansSC-Medium.otf" type="DynamicFontData" id=1] +[ext_resource path="res://resources/themes/base_theme.tres" type="Theme" id=2] +[ext_resource path="res://resources/fonts/raw/Anybody-Medium.ttf" type="DynamicFontData" id=3] +[ext_resource path="res://resources/fonts/raw/NotoSansJP-Medium.otf" type="DynamicFontData" id=4] +[ext_resource path="res://resources/fonts/raw/NotoSansTC-Medium.otf" type="DynamicFontData" id=5] +[ext_resource path="res://resources/fonts/raw/NotoSansKR-Medium.otf" type="DynamicFontData" id=6] + +[sub_resource type="DynamicFont" id=1] +size = 18 +extra_spacing_top = 10 +font_data = ExtResource( 3 ) +fallback/0 = ExtResource( 4 ) +fallback/1 = ExtResource( 6 ) +fallback/2 = ExtResource( 1 ) +fallback/3 = ExtResource( 5 ) + +[node name="Label" type="Label"] +margin_left = 136.0 +margin_right = 191.0 +margin_bottom = 23.0 +theme = ExtResource( 2 ) +custom_fonts/font = SubResource( 1 ) +text = "User 1 - 1" diff --git a/src/mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_stats_container.gd b/src/mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_stats_container.gd new file mode 100644 index 0000000..9c32355 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_stats_container.gd @@ -0,0 +1,63 @@ +extends StatsContainer + +onready var game_controller = $"/root/GameController" + +const shop_options_resource = preload("res://mods-unpacked/Brogether-Brogether/opponents_shop/data/opponents_shop_options.tres") +const shop_monster_container_scene = preload("res://mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.tscn") + +func _ready(): + if not $"/root".has_node("GameController") or $"/root/GameController".is_coop(): + $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer/MarginContainer/VBoxContainer2/OpponentsButton".hide() + + var opponents_shop = $MarginContainer/VBoxContainer2/OpponentsShop + for opponents_shop_option in shop_options_resource.shop_options: + var shop_option = shop_monster_container_scene.instance() + shop_option.init(opponents_shop_option, game_controller) + opponents_shop.add_child(shop_option) + +func update_tab(tab:int)->void : + var has_opponents_tab = $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer/MarginContainer/VBoxContainer2".has_node("OpponentsButton") + + if tab == 3: + if has_opponents_tab: + var opponents_button = $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer/MarginContainer/VBoxContainer2/OpponentsButton" + var opponents_shop = $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer/MarginContainer/VBoxContainer2/OpponentsShop" + opponents_button.flat = true + opponents_shop.show() + _primary_tab.flat = false + _secondary_tab.flat = false + _general_stats.hide() + _primary_stats.hide() + _secondary_stats.hide() + pass + else: + if has_opponents_tab: + var opponents_button = $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer/MarginContainer/VBoxContainer2/OpponentsButton" + var opponents_shop = $"/root/Shop/Content/MarginContainer/HBoxContainer/VBoxContainer2/StatsContainer/MarginContainer/VBoxContainer2/OpponentsShop" + opponents_button.flat = false + opponents_shop.hide() + .update_tab(tab) + + +func _on_OpponentsButton_pressed(): + update_tab(3) + +func update_bought_items(tracked_players:Dictionary) -> void: + var opponents_shop = $MarginContainer/VBoxContainer2/OpponentsShop + for shop_item_container in opponents_shop.get_children(): + shop_item_container.clear_player_counts() + for tracked_player_id in tracked_players: + var all_effects = {} + if tracked_players[tracked_player_id].has("extra_enemies_next_wave"): + all_effects.merge(tracked_players[tracked_player_id]["extra_enemies_next_wave"].duplicate()) + if tracked_players[tracked_player_id].has("effects"): + all_effects.merge(tracked_players[tracked_player_id]["effects"]) + for effect_path in all_effects: + if effect_path == shop_item_container._shop_option.effect.get_path(): + var tracked_player = tracked_players[tracked_player_id] + var count = all_effects[effect_path] + var display_name = str(tracked_player_id) + if tracked_player.has("username"): + display_name = tracked_player["username"] + + shop_item_container.add_player_count(display_name, count) diff --git a/src/mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_stats_container.tscn b/src/mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_stats_container.tscn new file mode 100644 index 0000000..d5cb159 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_stats_container.tscn @@ -0,0 +1,368 @@ +[gd_scene load_steps=8 format=2] + +[ext_resource path="res://ui/menus/shop/secondary_stats_container.gd" type="Script" id=2] +[ext_resource path="res://resources/themes/base_theme.tres" type="Theme" id=3] +[ext_resource path="res://ui/menus/shop/stat_container.tscn" type="PackedScene" id=4] +[ext_resource path="res://ui/menus/shop/general_stats_container.tscn" type="PackedScene" id=5] +[ext_resource path="res://ui/menus/shop/secondary_stat_container.tscn" type="PackedScene" id=6] +[ext_resource path="res://ui/menus/global/my_menu_button.gd" type="Script" id=7] +[ext_resource path="res://mods-unpacked/Brogether-Brogether/ui/shop/multiplayer_stats_container.gd" type="Script" id=8] + +[node name="MultiplayerStats" type="PanelContainer"] +self_modulate = Color( 1, 1, 1, 0.478431 ) +margin_top = 132.0 +margin_right = 384.0 +margin_bottom = 368.0 +rect_min_size = Vector2( 411, 782 ) +theme = ExtResource( 3 ) +script = ExtResource( 8 ) + +[node name="MarginContainer" type="MarginContainer" parent="."] +margin_left = 5.0 +margin_top = 5.0 +margin_right = 406.0 +margin_bottom = 1332.0 +custom_constants/margin_right = 10 +custom_constants/margin_top = 10 +custom_constants/margin_left = 10 +custom_constants/margin_bottom = 10 + +[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer"] +margin_left = 10.0 +margin_top = 10.0 +margin_right = 391.0 +margin_bottom = 1317.0 +custom_constants/separation = 20 + +[node name="OpponentsButton" type="Button" parent="MarginContainer/VBoxContainer2"] +margin_right = 381.0 +margin_bottom = 51.0 +rect_min_size = Vector2( 150, 0 ) +text = "Opponents Shop" +clip_text = true +script = ExtResource( 7 ) + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer2"] +margin_top = 71.0 +margin_right = 381.0 +margin_bottom = 122.0 +custom_constants/separation = 10 +alignment = 1 + +[node name="Primary" type="Button" parent="MarginContainer/VBoxContainer2/HBoxContainer"] +unique_name_in_owner = true +margin_left = 20.0 +margin_right = 170.0 +margin_bottom = 51.0 +rect_min_size = Vector2( 150, 0 ) +text = "PRIMARY" +clip_text = true +script = ExtResource( 7 ) + +[node name="Secondary" type="Button" parent="MarginContainer/VBoxContainer2/HBoxContainer"] +unique_name_in_owner = true +margin_left = 180.0 +margin_right = 360.0 +margin_bottom = 51.0 +rect_min_size = Vector2( 180, 0 ) +text = "SECONDARY" +clip_text = true +script = ExtResource( 7 ) + +[node name="GeneralStats" parent="MarginContainer/VBoxContainer2" instance=ExtResource( 5 )] +margin_top = 142.0 +margin_right = 381.0 +margin_bottom = 172.0 + +[node name="PrimaryStats" type="VBoxContainer" parent="MarginContainer/VBoxContainer2"] +unique_name_in_owner = true +margin_top = 192.0 +margin_right = 381.0 +margin_bottom = 687.0 +custom_constants/separation = 1 + +[node name="MaxHPContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_right = 381.0 +margin_bottom = 30.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_MAX_HP" + +[node name="HPRegenerationContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 31.0 +margin_right = 381.0 +margin_bottom = 61.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_HP_REGENERATION" + +[node name="LifeStealContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 62.0 +margin_right = 381.0 +margin_bottom = 92.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_LIFESTEAL" + +[node name="PercentDamageContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 93.0 +margin_right = 381.0 +margin_bottom = 123.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_PERCENT_DAMAGE" + +[node name="MeleeDamageContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 124.0 +margin_right = 381.0 +margin_bottom = 154.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_MELEE_DAMAGE" + +[node name="RangedDamageContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 155.0 +margin_right = 381.0 +margin_bottom = 185.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_RANGED_DAMAGE" + +[node name="ElementalDamageContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 186.0 +margin_right = 381.0 +margin_bottom = 216.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_ELEMENTAL_DAMAGE" + +[node name="AttackSpeedContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 217.0 +margin_right = 381.0 +margin_bottom = 247.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_ATTACK_SPEED" + +[node name="CritChanceContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 248.0 +margin_right = 381.0 +margin_bottom = 278.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_CRIT_CHANCE" + +[node name="EngineeringContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 279.0 +margin_right = 381.0 +margin_bottom = 309.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_ENGINEERING" + +[node name="RangeContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 310.0 +margin_right = 381.0 +margin_bottom = 340.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_RANGE" + +[node name="ArmorContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 341.0 +margin_right = 381.0 +margin_bottom = 371.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_ARMOR" + +[node name="DodgeContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 372.0 +margin_right = 381.0 +margin_bottom = 402.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_DODGE" + +[node name="SpeedContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 403.0 +margin_right = 381.0 +margin_bottom = 433.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_SPEED" + +[node name="LuckContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 434.0 +margin_right = 381.0 +margin_bottom = 464.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_LUCK" + +[node name="HarvestingContainer" parent="MarginContainer/VBoxContainer2/PrimaryStats" instance=ExtResource( 4 )] +margin_top = 465.0 +margin_right = 381.0 +margin_bottom = 495.0 +__meta__ = { +"_edit_use_anchors_": false +} +key = "STAT_HARVESTING" + +[node name="SecondaryStats" type="VBoxContainer" parent="MarginContainer/VBoxContainer2"] +unique_name_in_owner = true +margin_top = 707.0 +margin_right = 381.0 +margin_bottom = 1307.0 +custom_constants/separation = 0 +script = ExtResource( 2 ) + +[node name="ConsumableHealContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_right = 381.0 +margin_bottom = 30.0 +key = "CONSUMABLE_HEAL" + +[node name="MaterialsHealContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 30.0 +margin_right = 381.0 +margin_bottom = 60.0 +key = "heal_when_pickup_gold" +custom_text_key = "CHANCE_HEAL_ON_GOLD" + +[node name="XPGainContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 60.0 +margin_right = 381.0 +margin_bottom = 90.0 +key = "XP_GAIN" + +[node name="PickupRangeContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 90.0 +margin_right = 381.0 +margin_bottom = 120.0 +key = "PICKUP_RANGE" + +[node name="ItemPriceContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 120.0 +margin_right = 381.0 +margin_bottom = 150.0 +key = "ITEMS_PRICE" +reverse = true + +[node name="ExplosionDamageContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 150.0 +margin_right = 381.0 +margin_bottom = 180.0 +key = "EXPLOSION_DAMAGE" + +[node name="ExplosionSizeContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 180.0 +margin_right = 381.0 +margin_bottom = 210.0 +key = "EXPLOSION_SIZE" + +[node name="BounceContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 210.0 +margin_right = 381.0 +margin_bottom = 240.0 +key = "BOUNCE" + +[node name="PiercingContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 240.0 +margin_right = 381.0 +margin_bottom = 270.0 +key = "PIERCING" + +[node name="PiercingDamageContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 270.0 +margin_right = 381.0 +margin_bottom = 300.0 +key = "PIERCING_DAMAGE" + +[node name="DamageAgainstBossesContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 300.0 +margin_right = 381.0 +margin_bottom = 330.0 +key = "DAMAGE_AGAINST_BOSSES" + +[node name="BurningCooldownReductionContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 330.0 +margin_right = 381.0 +margin_bottom = 360.0 +key = "BURNING_COOLDOWN_REDUCTION" + +[node name="BurningSpreadContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 360.0 +margin_right = 381.0 +margin_bottom = 390.0 +key = "BURNING_SPREAD" + +[node name="KnockbackContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 390.0 +margin_right = 381.0 +margin_bottom = 420.0 +key = "KNOCKBACK" + +[node name="ChanceDoubleGoldContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 420.0 +margin_right = 381.0 +margin_bottom = 450.0 +key = "CHANCE_DOUBLE_GOLD" + +[node name="ItemBoxGoldContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 450.0 +margin_right = 381.0 +margin_bottom = 480.0 +key = "ITEM_BOX_GOLD" + +[node name="FreeRerollsContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 480.0 +margin_right = 381.0 +margin_bottom = 510.0 +key = "FREE_REROLLS" + +[node name="TreesContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 510.0 +margin_right = 381.0 +margin_bottom = 540.0 +key = "TREES" + +[node name="NumberOfEnemiesContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 540.0 +margin_right = 381.0 +margin_bottom = 570.0 +key = "NUMBER_OF_ENEMIES" +custom_text_key = "PCT_NUMBER_OF_ENEMIES" + +[node name="EnemySpeedContainer" parent="MarginContainer/VBoxContainer2/SecondaryStats" instance=ExtResource( 6 )] +margin_top = 570.0 +margin_right = 381.0 +margin_bottom = 600.0 +key = "ENEMY_SPEED" +custom_text_key = "PCT_ENEMY_SPEED" +reverse = true + +[node name="OpponentsShop" type="VBoxContainer" parent="MarginContainer/VBoxContainer2"] +visible = false +margin_top = 1327.0 +margin_right = 354.0 +margin_bottom = 1327.0 +custom_constants/separation = 10 + +[connection signal="pressed" from="MarginContainer/VBoxContainer2/OpponentsButton" to="." method="_on_OpponentsButton_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer2/HBoxContainer/Primary" to="." method="_on_Primary_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer2/HBoxContainer/Secondary" to="." method="_on_Secondary_pressed"] diff --git a/src/mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.gd b/src/mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.gd new file mode 100644 index 0000000..5c0635f --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.gd @@ -0,0 +1,48 @@ +extends VBoxContainer +class_name ShopMonsterContainer + +var _shop_option +var game_controller + +const count_label_scene = preload("res://mods-unpacked/Brogether-Brogether/ui/shop/count_label.tscn") + +# TODO rename to work for all types +func init(shop_option: Resource, parent_game_controller) -> void: + game_controller = parent_game_controller + _shop_option = shop_option + var price: int = shop_option.price + var effect: Resource = shop_option.effect + var display_text: String = shop_option.display_text + + get_node("ShopItem/Title").text = display_text + var _connection_error = get_node("ShopItem/VBoxContainer/BuyButton").connect("pressed", self, "_on_buyButton_pressed", [shop_option]) + + if effect is WaveGroupData: + for wave_unit_data in effect.wave_units_data: + var enemy = wave_unit_data.unit_scene.instance() + get_node("ShopItem/Icon").texture = enemy.get_node("Animation/Sprite").texture + elif effect is Effect: + if effect.key.begins_with("stat_"): + get_node("ShopItem/Icon").texture = ItemService.get_stat_icon(effect.key) + + var adjusted_price = ItemService.get_value(RunData.current_wave, price, true, true) + get_node("ShopItem/VBoxContainer/BuyButton").text = str(adjusted_price) + +func _on_buyButton_pressed(shop_option: Resource) -> void: + var adjusted_price = ItemService.get_value(RunData.current_wave, shop_option.price, true, true) + if RunData.gold >= adjusted_price: + RunData.remove_gold(adjusted_price) + game_controller.send_bought_item(shop_option) + + $"/root/Shop"._shop_items_container.update_buttons_color() + +func add_player_count(username:String, count: int) -> void: + var label = count_label_scene.instance() + label.text = str(username, "-", count) + + get_node("PurchaseTracker").add_child(label) + +func clear_player_counts() -> void: + var purchase_tracker = get_node("PurchaseTracker") + for child in purchase_tracker.get_children(): + purchase_tracker.remove_child(child) diff --git a/src/mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.tscn b/src/mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.tscn new file mode 100644 index 0000000..b6b3fee --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.tscn @@ -0,0 +1,71 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://items/materials/material_ui.png" type="Texture" id=1] +[ext_resource path="res://entities/units/enemies/001/1.png" type="Texture" id=4] +[ext_resource path="res://mods-unpacked/Brogether-Brogether/ui/shop/shop_monster_container.gd" type="Script" id=5] + +[node name="ShopMonsterContainer" type="VBoxContainer"] +margin_left = -212.5 +margin_right = 114.5 +margin_bottom = 116.0 +grow_horizontal = 2 +size_flags_vertical = 0 +alignment = 1 +script = ExtResource( 5 ) + +[node name="ShopItem" type="HBoxContainer" parent="."] +margin_top = 18.0 +margin_right = 327.0 +margin_bottom = 93.0 +custom_constants/separation = 5 +alignment = 2 + +[node name="Icon" type="TextureRect" parent="ShopItem"] +margin_left = 30.0 +margin_right = 105.0 +margin_bottom = 75.0 +rect_min_size = Vector2( 75, 75 ) +texture = ExtResource( 4 ) +expand = true +stretch_mode = 6 + +[node name="Title" type="Label" parent="ShopItem"] +margin_left = 110.0 +margin_top = 25.0 +margin_right = 222.0 +margin_bottom = 49.0 +text = "Baby Alien" +align = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="ShopItem"] +margin_left = 227.0 +margin_right = 327.0 +margin_bottom = 75.0 +alignment = 1 + +[node name="BuyButton" type="Button" parent="ShopItem/VBoxContainer"] +margin_top = 19.0 +margin_right = 100.0 +margin_bottom = 56.0 +grow_horizontal = 0 +grow_vertical = 0 +rect_min_size = Vector2( 100, 0 ) +focus_neighbour_left = NodePath("../../../ShopItem/HBoxContainer/LockButton") +focus_neighbour_right = NodePath("../../../ShopItem3/HBoxContainer/LockButton") +size_flags_horizontal = 12 +size_flags_vertical = 0 +toggle_mode = true +text = "1500" +icon = ExtResource( 1 ) +icon_align = 2 +expand_icon = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="PurchaseTracker" type="HBoxContainer" parent="."] +margin_top = 97.0 +margin_right = 327.0 +margin_bottom = 97.0 +custom_constants/separation = 10 +alignment = 1 diff --git a/src/mods-unpacked/Brogether-Brogether/ui/toggle.tscn b/src/mods-unpacked/Brogether-Brogether/ui/toggle.tscn new file mode 100644 index 0000000..317b6ed --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/toggle.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=1 format=2] + + +[node name="DamageDisplayButton" type="CheckButton"] +unique_name_in_owner = true +margin_top = 365.0 +margin_right = 780.0 +margin_bottom = 435.0 +focus_neighbour_right = NodePath("../../../../Column2/SoundContainer/PauseOnFocusLostButton") +text = "Ready" diff --git a/src/mods-unpacked/Brogether-Brogether/ui/username_label.tscn b/src/mods-unpacked/Brogether-Brogether/ui/username_label.tscn new file mode 100644 index 0000000..72eb3b6 --- /dev/null +++ b/src/mods-unpacked/Brogether-Brogether/ui/username_label.tscn @@ -0,0 +1,6 @@ +[gd_scene format=2] + +[node name="username_label" type="Label"] +size_flags_vertical = 1 +text = "Username" +align = 1