b0c83af092
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
293 lines
7.8 KiB
GDScript
293 lines
7.8 KiB
GDScript
@tool
|
|
extends Panel
|
|
|
|
var visibility_popup: VestUI.VisibilityPopup
|
|
|
|
@onready var _tree := %Tree as Tree
|
|
@onready var _spinner := %Spinner as Control
|
|
@onready var _spinner_icon := %"Spinner Icon" as TextureRect
|
|
@onready var _spinner_label := %"Spinner Label" as Label
|
|
@onready var _animation_player := $PanelContainer/Spinner/AnimationPlayer as AnimationPlayer
|
|
|
|
var _results: VestResult.Suite = null
|
|
var _render_results: VestResult.Suite = null
|
|
var _search_string: String = ""
|
|
|
|
signal on_collapse_changed()
|
|
|
|
func get_results() -> VestResult.Suite:
|
|
return _results
|
|
|
|
func set_results(results: VestResult.Suite) -> void:
|
|
if _results != results:
|
|
_results = results
|
|
_render()
|
|
|
|
func get_search_string() -> String:
|
|
return _search_string
|
|
|
|
func set_search_string(search_string: String) -> void:
|
|
if _search_string != search_string:
|
|
_search_string = search_string
|
|
_render()
|
|
|
|
func clear() -> void:
|
|
set_results(null)
|
|
|
|
func set_spinner(text: String, icon: Texture2D = null, animated: bool = true) -> void:
|
|
clear()
|
|
_spinner_icon.texture = icon
|
|
_spinner_label.text = text
|
|
_spinner.show()
|
|
if animated:
|
|
_animation_player.play("spin")
|
|
else:
|
|
_animation_player.stop()
|
|
|
|
func collapse() -> void:
|
|
var root := _tree.get_root()
|
|
if not root: return
|
|
|
|
for item in root.get_children():
|
|
item.set_collapsed_recursive(true)
|
|
|
|
func expand() -> void:
|
|
var root := _tree.get_root()
|
|
if not root: return
|
|
|
|
for item in root.get_children():
|
|
item.set_collapsed_recursive(false)
|
|
|
|
func toggle_collapsed() -> void:
|
|
if is_any_collapsed():
|
|
expand()
|
|
else:
|
|
collapse()
|
|
|
|
func is_any_collapsed() -> bool:
|
|
var at := _tree.get_root()
|
|
var queue := [] as Array[TreeItem]
|
|
|
|
while at:
|
|
queue.append_array(at.get_children())
|
|
|
|
if at.collapsed:
|
|
return true
|
|
at = queue.pop_back()
|
|
|
|
return false
|
|
|
|
func _ready():
|
|
_tree.item_collapsed.connect(func(_item):
|
|
on_collapse_changed.emit()
|
|
)
|
|
|
|
func _clear():
|
|
_tree.clear()
|
|
for connection in _tree.item_activated.get_connections():
|
|
connection["signal"].disconnect(connection["callable"])
|
|
_spinner.hide()
|
|
|
|
func _render():
|
|
_clear()
|
|
if _results != null:
|
|
_render_results = VestResult.Suite._from_wire(_results._to_wire()) # HACK: Duplicate
|
|
_filter_visibility(_render_results)
|
|
_filter_search(_render_results, _search_string)
|
|
_render_result(_render_results, _tree)
|
|
|
|
func _filter_visibility(results: VestResult.Suite) -> void:
|
|
var cases_to_remove := []
|
|
var suites_to_remove := []
|
|
|
|
for case in results.cases:
|
|
if not _check_visibility(case):
|
|
cases_to_remove.append(case)
|
|
|
|
for subsuite in results.subsuites:
|
|
if not _check_visibility(subsuite):
|
|
suites_to_remove.append(subsuite)
|
|
else:
|
|
_filter_visibility(subsuite)
|
|
|
|
for case in cases_to_remove:
|
|
results.cases.erase(case)
|
|
for subsuite in suites_to_remove:
|
|
results.subsuites.erase(subsuite)
|
|
|
|
func _filter_search(results: VestResult.Suite, needle: String) -> void:
|
|
if not needle:
|
|
# Search string empty, do nothing
|
|
return
|
|
|
|
var scores := {}
|
|
var parents := {}
|
|
var at := results
|
|
var queue: Array[VestResult.Suite] = []
|
|
var leaves: Array[VestResult.Suite] = []
|
|
|
|
# Calculate scores
|
|
while at:
|
|
for subsuite in at.subsuites:
|
|
queue.append(subsuite)
|
|
parents[subsuite] = at
|
|
|
|
if at.subsuites.is_empty():
|
|
leaves.append(at)
|
|
|
|
scores[at] = VestUI.fuzzy_score(needle, at.suite.name)
|
|
for case in at.cases:
|
|
scores[case] = maxf(
|
|
VestUI.fuzzy_score(needle, case.case.description),
|
|
VestUI.fuzzy_score(needle, case.case.method_name + "()")
|
|
)
|
|
|
|
at = queue.pop_back() as VestResult.Suite
|
|
|
|
# Propagate best scores from leaves
|
|
for leaf in leaves:
|
|
at = leaf
|
|
|
|
# Calculate best score for leaf
|
|
var best_score := scores[at] as float
|
|
for case in at.cases:
|
|
best_score = maxf(best_score, scores[case])
|
|
|
|
# Propagate upwards in tree
|
|
while at:
|
|
scores[at] = maxf(scores[at], best_score)
|
|
best_score = maxf(best_score, scores[at])
|
|
at = parents.get(at, null)
|
|
|
|
# Remove results that don't match the search string
|
|
at = results
|
|
queue.clear()
|
|
while at:
|
|
at.cases.sort_custom(func(a, b): return scores[a] > scores[b])
|
|
at.subsuites.sort_custom(func(a, b): return scores[a] > scores[b])
|
|
|
|
queue.append_array(at.subsuites)
|
|
at = queue.pop_back()
|
|
|
|
func _check_visibility(what: Variant) -> bool:
|
|
if what is VestResult.Case:
|
|
var case := what as VestResult.Case
|
|
return visibility_popup.get_visibility_for(case.status)
|
|
elif what is VestResult.Suite:
|
|
var suite := what as VestResult.Suite
|
|
for status in suite.get_unique_statuses():
|
|
if visibility_popup.get_visibility_for(status):
|
|
return true
|
|
return false
|
|
else:
|
|
push_warning("Checking visibility for unknown item: %s" % [what])
|
|
return true
|
|
|
|
func _render_result(what: Object, tree: Tree, parent: TreeItem = null):
|
|
if what is VestResult.Suite:
|
|
var item := tree.create_item(parent)
|
|
item.set_text(0, what.suite.name)
|
|
item.set_text(1, what.get_aggregate_status_string().capitalize())
|
|
|
|
item.set_icon(0, VestUI.get_status_icon(what))
|
|
item.set_icon_max_width(0, VestUI.get_icon_size())
|
|
|
|
tree.item_activated.connect(func():
|
|
if tree.get_selected() == item:
|
|
_navigate(what.suite.definition_file, what.suite.definition_line)
|
|
)
|
|
|
|
for subsuite in what.subsuites:
|
|
_render_result(subsuite, tree, item)
|
|
for case in what.cases:
|
|
_render_result(case, tree, item)
|
|
elif what is VestResult.Case:
|
|
var item := tree.create_item(parent)
|
|
item.set_text(0, what.case.description)
|
|
item.set_text(1, what.get_status_string().capitalize())
|
|
item.collapsed = what.status == VestResult.TEST_PASS
|
|
|
|
item.set_icon(0, VestUI.get_status_icon(what))
|
|
item.set_icon_max_width(0, VestUI.get_icon_size())
|
|
|
|
_render_data(what, tree, item)
|
|
|
|
tree.item_activated.connect(func():
|
|
if tree.get_selected() == item:
|
|
_navigate(what.case.definition_file, what.case.definition_line)
|
|
)
|
|
else:
|
|
push_error("Rendering unknown object: %s" % [what])
|
|
|
|
func _render_data(case: VestResult.Case, tree: Tree, parent: TreeItem):
|
|
var data := case.data.duplicate()
|
|
|
|
for message in case.messages:
|
|
var item := tree.create_item(parent)
|
|
item.set_text(0, message)
|
|
|
|
tree.item_activated.connect(func():
|
|
if tree.get_selected() == item:
|
|
# TODO: popup_dialog()
|
|
add_child(VestMessagePopup.of(message).window)
|
|
)
|
|
|
|
if data == null or data.is_empty():
|
|
return
|
|
|
|
if data.has("messages"):
|
|
var header_item := tree.create_item(parent)
|
|
header_item.set_text(0, "Messages")
|
|
|
|
for message in data["messages"]:
|
|
tree.create_item(header_item).set_text(0, message)
|
|
|
|
data.erase("messages")
|
|
|
|
if data.has("benchmarks"):
|
|
var header_item := tree.create_item(parent)
|
|
header_item.set_text(0, "Benchmarks")
|
|
|
|
for benchmark in data["benchmarks"]:
|
|
var benchmark_item = tree.create_item(header_item)
|
|
benchmark_item.set_text(0, benchmark["name"])
|
|
if benchmark.has("duration"): benchmark_item.set_text(1, benchmark["duration"])
|
|
|
|
for measurement in benchmark.keys():
|
|
if measurement == "name": continue
|
|
|
|
var measurement_item := tree.create_item(benchmark_item)
|
|
measurement_item.set_text(0, str(measurement).capitalize())
|
|
measurement_item.set_text(1, str(benchmark[measurement]))
|
|
|
|
data.erase("benchmarks")
|
|
|
|
if data.has("expect") and data.has("got"):
|
|
var header_item := tree.create_item(parent)
|
|
header_item.set_text(0, "Got:")
|
|
header_item.set_text(1, "Expected:")
|
|
|
|
var got_string := JSON.stringify(data["got"], " ")
|
|
var expect_string := JSON.stringify(data["expect"], " ")
|
|
|
|
var comparison_item := tree.create_item(header_item)
|
|
comparison_item.set_text(0, got_string)
|
|
comparison_item.set_text(1, expect_string)
|
|
|
|
tree.item_activated.connect(func():
|
|
# TODO: popup_dialog()
|
|
if tree.get_selected() in [header_item, comparison_item]:
|
|
add_child(VestComparisonPopup.of(expect_string, got_string).window)
|
|
)
|
|
|
|
data.erase("got")
|
|
data.erase("expect")
|
|
|
|
for key in data:
|
|
var item := tree.create_item(parent)
|
|
item.set_text(0, var_to_str(key))
|
|
item.set_text(1, var_to_str(data[key]))
|
|
|
|
func _navigate(file: String, line: int):
|
|
Vest._get_editor_interface().edit_script(load(file), line)
|