NEO-153: fix gdlint pre-push for contract HUD wiring.

Extract contract HUD main wiring, dedupe deny copy, trim main.gd, and add wiring tests.
pull/195/head
VinPropane 2026-06-28 17:44:59 -04:00
parent 6a48315ba5
commit b9ffd7553b
7 changed files with 132 additions and 90 deletions

View File

@ -10,6 +10,13 @@ const PROTOTYPE_CONTRACT_SEED_BUCKET := "prototype_contract_dev_seed"
const ISSUE_IDLE_HINT := "Contract issue: — (Shift+C prototype)" const ISSUE_IDLE_HINT := "Contract issue: — (Shift+C prototype)"
const ISSUE_SENDING_HINT := "Contract issue: sending…" const ISSUE_SENDING_HINT := "Contract issue: sending…"
const REWARD_HEADER := "Contract rewards:" const REWARD_HEADER := "Contract rewards:"
const ISSUE_DENY_COPY := {
"active_contract_exists": "Contract issue: denied — active contract already issued",
"no_eligible_template": "Contract issue: denied — no eligible template (standing/band)",
"economy_cap_exceeded": "Contract issue: denied — economy cap exceeded",
"invalid_reward_bundle": "Contract issue: denied — invalid reward bundle",
"unknown_template": "Contract issue: denied — unknown template",
}
var _contract_client: Node = null var _contract_client: Node = null
var _item_defs_client: Node = null var _item_defs_client: Node = null
@ -84,13 +91,14 @@ func try_issue_key_input(event: InputEvent) -> bool:
if not event is InputEventKey: if not event is InputEventKey:
return false return false
var k := event as InputEventKey var k := event as InputEventKey
if not k.pressed or k.echo or not k.shift_pressed: if (
return false not k.pressed
if k.physical_keycode != KEY_C and k.keycode != KEY_C: or k.echo
return false or not k.shift_pressed
if not is_instance_valid(_contract_client): or (k.physical_keycode != KEY_C and k.keycode != KEY_C)
return false or not is_instance_valid(_contract_client)
if not _contract_client.has_method("request_issue"): or not _contract_client.has_method("request_issue")
):
return false return false
if bool(_contract_client.call("is_issue_busy")): if bool(_contract_client.call("is_issue_busy")):
return true return true
@ -274,21 +282,13 @@ func _render_reward_label() -> void:
func _format_issue_deny(reason_code: String) -> String: func _format_issue_deny(reason_code: String) -> String:
match reason_code: var rc := reason_code.strip_edges()
"active_contract_exists": if rc.is_empty():
return "Contract issue: denied — active contract already issued" return "Contract issue: denied (no reasonCode)"
"no_eligible_template": var copy_variant: Variant = ISSUE_DENY_COPY.get(rc, null)
return "Contract issue: denied — no eligible template (standing/band)" if copy_variant is String:
"economy_cap_exceeded": return copy_variant as String
return "Contract issue: denied — economy cap exceeded" return "Contract issue: denied — %s" % rc
"invalid_reward_bundle":
return "Contract issue: denied — invalid reward bundle"
"unknown_template":
return "Contract issue: denied — unknown template"
"":
return "Contract issue: denied (no reasonCode)"
_:
return "Contract issue: denied — %s" % reason_code
func _format_reward_summary_lines(summary: Dictionary) -> PackedStringArray: func _format_reward_summary_lines(summary: Dictionary) -> PackedStringArray:
@ -325,7 +325,9 @@ func _format_reward_summary_lines(summary: Dictionary) -> PackedStringArray:
var amount: int = int(grant.get("amount", 0)) var amount: int = int(grant.get("amount", 0))
if faction_id.is_empty() or amount <= 0: if faction_id.is_empty() or amount <= 0:
continue continue
lines.append(" %s +%d rep" % [FactionStandingClient.display_name_for(faction_id), amount]) lines.append(
" %s +%d rep" % [FactionStandingClient.display_name_for(faction_id), amount]
)
if lines.is_empty(): if lines.is_empty():
lines.append(" (no grants)") lines.append(" (no grants)")
return lines return lines

View File

@ -0,0 +1,43 @@
extends RefCounted
## NEO-153: thin main.gd wiring (keeps main.gd under gdlint max-file-lines).
const ContractHudController := preload("res://scripts/contract_hud_controller.gd")
static func setup_on(
main: Node,
hud_root: Node,
contract_client: Node,
apply_http_config: Callable,
item_defs_client: Node = null,
inventory_client: Node = null,
skill_progression_client: Node = null,
faction_standing_client: Node = null
) -> Node:
var controller: Node = ContractHudController.new()
main.add_child(controller)
controller.call(
"setup",
contract_client,
hud_root.get_node("ContractActiveLabel"),
hud_root.get_node("ContractIssueFeedbackLabel"),
hud_root.get_node("ContractRewardDeliveryLabel"),
apply_http_config,
item_defs_client,
inventory_client,
skill_progression_client,
faction_standing_client
)
return controller
static func request_refresh(controller: Node) -> void:
if is_instance_valid(controller):
controller.call("request_contract_refresh")
static func try_issue_key_input(controller: Node, event: InputEvent) -> bool:
if not is_instance_valid(controller):
return false
return bool(controller.call("try_issue_key_input", event))

View File

@ -0,0 +1 @@
uid://bneo153mainwire01

View File

@ -120,3 +120,10 @@ static func _add_prop_visual(
prop_mat.albedo_color = Color(0.28, 0.38, 0.45, 1.0) prop_mat.albedo_color = Color(0.28, 0.38, 0.45, 1.0)
prop_mesh.material_override = prop_mat prop_mesh.material_override = prop_mat
body.add_child(prop_mesh) body.add_child(prop_mesh)
static func set_collision_shapes_disabled(root: Node, disabled: bool) -> void:
if root is CollisionShape3D:
(root as CollisionShape3D).disabled = disabled
for c in root.get_children():
set_collision_shapes_disabled(c, disabled)

View File

@ -57,7 +57,8 @@ const BREACH_GIG_ID := "breach"
const PROTOTYPE_ENCOUNTER_ID := "prototype_combat_pocket" const PROTOTYPE_ENCOUNTER_ID := "prototype_combat_pocket"
const PROTOTYPE_ENCOUNTER_REQUIRED_TARGETS := 3 const PROTOTYPE_ENCOUNTER_REQUIRED_TARGETS := 3
const QuestHudController := preload("res://scripts/quest_hud_controller.gd") const QuestHudController := preload("res://scripts/quest_hud_controller.gd")
const ContractHudController := preload("res://scripts/contract_hud_controller.gd") const ContractHudMainWiring := preload("res://scripts/contract_hud_main_wiring.gd")
const InteractableWorldBuilder := preload("res://scripts/interactable_world_builder.gd")
## Bump on each rejection so older one-shot timers do not clear a newer message. ## Bump on each rejection so older one-shot timers do not clear a newer message.
var _move_reject_msg_token: int = 0 var _move_reject_msg_token: int = 0
@ -139,9 +140,6 @@ var _dev_pulse_bootstrap_attempted: bool = false
@onready var _quest_progress_label: Label = _hud_root.get_node("QuestProgressLabel") @onready var _quest_progress_label: Label = _hud_root.get_node("QuestProgressLabel")
@onready var _quest_accept_feedback_label: Label = _hud_root.get_node("QuestAcceptFeedbackLabel") @onready var _quest_accept_feedback_label: Label = _hud_root.get_node("QuestAcceptFeedbackLabel")
@onready var _quest_reward_delivery_label: Label = _hud_root.get_node("QuestRewardDeliveryLabel") @onready var _quest_reward_delivery_label: Label = _hud_root.get_node("QuestRewardDeliveryLabel")
@onready var _contract_active_label: Label = _hud_root.get_node("ContractActiveLabel")
@onready var _contract_issue_feedback_label: Label = _hud_root.get_node("ContractIssueFeedbackLabel")
@onready var _contract_reward_delivery_label: Label = _hud_root.get_node("ContractRewardDeliveryLabel")
@onready var _faction_standing_label: Label = _hud_root.get_node("FactionStandingLabel") @onready var _faction_standing_label: Label = _hud_root.get_node("FactionStandingLabel")
@onready var _npc_state_label: Label = _hud_root.get_node("NpcStateLabel") @onready var _npc_state_label: Label = _hud_root.get_node("NpcStateLabel")
@onready var _telegraph_label: Label = _hud_root.get_node("TelegraphLabel") @onready var _telegraph_label: Label = _hud_root.get_node("TelegraphLabel")
@ -213,7 +211,16 @@ func _ready() -> void:
_setup_gig_progression_sync() _setup_gig_progression_sync()
_setup_encounter_progress_sync() _setup_encounter_progress_sync()
_setup_quest_progress_sync() _setup_quest_progress_sync()
_setup_contract_hud() _contract_hud = ContractHudMainWiring.setup_on(
self,
_hud_root,
_contract_client,
Callable(self, "_apply_authority_http_config_to_client"),
_item_defs_client,
_inventory_client,
_skill_progression_client,
_faction_standing_client
)
_setup_gather_interact_feedback() _setup_gather_interact_feedback()
_setup_craft_ui() _setup_craft_ui()
@ -756,7 +763,7 @@ func _on_encounter_progress_received(snapshot: Dictionary) -> void:
if str(row.get("state", "")) == "completed": if str(row.get("state", "")) == "completed":
_request_inventory_refresh() _request_inventory_refresh()
_request_quest_progress_refresh() _request_quest_progress_refresh()
_request_contract_refresh() ContractHudMainWiring.request_refresh(_contract_hud)
func _on_encounter_progress_sync_failed(reason: String) -> void: func _on_encounter_progress_sync_failed(reason: String) -> void:
@ -893,35 +900,6 @@ func _try_quest_accept_key_input(event: InputEvent) -> bool:
return false return false
func _setup_contract_hud() -> void:
# NEO-153: contract issue + progress HUD; boot hydrate + refresh after issue/encounter clear.
_contract_hud = ContractHudController.new()
add_child(_contract_hud)
_contract_hud.call(
"setup",
_contract_client,
_contract_active_label,
_contract_issue_feedback_label,
_contract_reward_delivery_label,
Callable(self, "_apply_authority_http_config_to_client"),
_item_defs_client,
_inventory_client,
_skill_progression_client,
_faction_standing_client
)
func _request_contract_refresh() -> void:
if is_instance_valid(_contract_hud):
_contract_hud.call("request_contract_refresh")
func _try_contract_issue_key_input(event: InputEvent) -> bool:
if is_instance_valid(_contract_hud):
return bool(_contract_hud.call("try_issue_key_input", event))
return false
func _render_gather_feedback_label(text: String = "Gather: —") -> void: func _render_gather_feedback_label(text: String = "Gather: —") -> void:
if is_instance_valid(_gather_feedback_label): if is_instance_valid(_gather_feedback_label):
_gather_feedback_label.text = text _gather_feedback_label.text = text
@ -1374,27 +1352,19 @@ func _render_player_combat_hp_label() -> void:
func _render_npc_combat_hud_labels() -> void: func _render_npc_combat_hud_labels() -> void:
var rows: Dictionary = _resolve_hud_npc_rows() var rows: Dictionary = _resolve_hud_npc_rows()
_render_npc_state_label(rows) if is_instance_valid(_npc_state_label):
_render_telegraph_label(rows) _npc_state_label.text = NpcCombatHudHelpers.build_npc_state_label(
rows, _npc_runtime_sync_error
)
if is_instance_valid(_telegraph_label):
_telegraph_label.text = NpcCombatHudHelpers.build_telegraph_label(
rows, _npc_runtime_hud_state, _npc_runtime_sync_error
)
_npc_combat_hud_needs_tick = NpcCombatHudHelpers.has_interpolated_telegraph_display( _npc_combat_hud_needs_tick = NpcCombatHudHelpers.has_interpolated_telegraph_display(
rows, _npc_runtime_hud_state rows, _npc_runtime_hud_state
) )
func _render_npc_state_label(rows: Dictionary) -> void:
if not is_instance_valid(_npc_state_label):
return
_npc_state_label.text = NpcCombatHudHelpers.build_npc_state_label(rows, _npc_runtime_sync_error)
func _render_telegraph_label(rows: Dictionary) -> void:
if not is_instance_valid(_telegraph_label):
return
_telegraph_label.text = NpcCombatHudHelpers.build_telegraph_label(
rows, _npc_runtime_hud_state, _npc_runtime_sync_error
)
func _on_move_rejected(reason_code: String) -> void: func _on_move_rejected(reason_code: String) -> void:
_authority_force_snap_next = true _authority_force_snap_next = true
# Rejected stream: server state may differ; next snap is forced. # Rejected stream: server state may differ; next snap is forced.
@ -1444,7 +1414,7 @@ func _try_route_gameplay_key_input(event: InputEvent) -> bool:
return true return true
if _try_quest_accept_key_input(event): if _try_quest_accept_key_input(event):
return true return true
if _try_contract_issue_key_input(event): if ContractHudMainWiring.try_issue_key_input(_contract_hud, event):
return true return true
if _try_inventory_refresh_input(event): if _try_inventory_refresh_input(event):
return true return true
@ -1580,7 +1550,7 @@ func _dev_toggle_obstacle_smoke_deferred() -> void:
_dev_saved_obstacle_mask = co.collision_mask _dev_saved_obstacle_mask = co.collision_mask
co.collision_layer = 0 co.collision_layer = 0
co.collision_mask = 0 co.collision_mask = 0
_dev_collision_shapes_set_disabled(obstacle, true) InteractableWorldBuilder.set_collision_shapes_disabled(obstacle, true)
obstacle.process_mode = Node.PROCESS_MODE_DISABLED obstacle.process_mode = Node.PROCESS_MODE_DISABLED
obstacle.visible = false obstacle.visible = false
if obstacle.get_parent() == _nav_region: if obstacle.get_parent() == _nav_region:
@ -1594,7 +1564,7 @@ func _dev_toggle_obstacle_smoke_deferred() -> void:
if co2 != null and _dev_saved_obstacle_layer >= 0: if co2 != null and _dev_saved_obstacle_layer >= 0:
co2.collision_layer = _dev_saved_obstacle_layer co2.collision_layer = _dev_saved_obstacle_layer
co2.collision_mask = _dev_saved_obstacle_mask co2.collision_mask = _dev_saved_obstacle_mask
_dev_collision_shapes_set_disabled(obstacle, false) InteractableWorldBuilder.set_collision_shapes_disabled(obstacle, false)
call_deferred("_dev_rebake_nav_after_obstacle_toggle_deferred") call_deferred("_dev_rebake_nav_after_obstacle_toggle_deferred")
@ -1610,28 +1580,19 @@ func _dev_rebake_nav_after_obstacle_toggle_deferred() -> void:
_player.sync_navigation_agent_after_map_rebuild(_nav_region) _player.sync_navigation_agent_after_map_rebuild(_nav_region)
func _dev_collision_shapes_set_disabled(root: Node, disabled: bool) -> void:
if root is CollisionShape3D:
(root as CollisionShape3D).disabled = disabled
for c in root.get_children():
_dev_collision_shapes_set_disabled(c, disabled)
func _on_interactables_catalog_ready(descriptors: Array) -> void: func _on_interactables_catalog_ready(descriptors: Array) -> void:
_interactables_catalog = descriptors.duplicate(true) _interactables_catalog = descriptors.duplicate(true)
var groups: Variant = load("res://scripts/interactable_world_builder.gd").call( var groups: Variant = InteractableWorldBuilder.build_from_catalog(
"build_from_catalog", descriptors, _interactables_root, _radius_preview descriptors, _interactables_root, _radius_preview
) )
if _radius_preview.has_method("setup_glow_groups"): if _radius_preview.has_method("setup_glow_groups"):
_radius_preview.call("setup_glow_groups", groups as Array) _radius_preview.call("setup_glow_groups", groups as Array)
func _on_interactables_catalog_failed(reason: String) -> void: func _on_interactables_catalog_failed(reason: String) -> void:
# `InteractablesCatalogClient` already `push_error`s; this hook exists for wiring clarity.
var readme := "Interaction + range preview (NEO-9 + NEO-25)"
push_warning( push_warning(
( (
"Interactables catalog failed (%s) — start the game server first; see client README (%s)." "Interactables catalog failed (%s) — start the game server first; see client README."
% [reason, readme] % reason
) )
) )

View File

@ -0,0 +1,27 @@
extends GdUnitTestSuite
## NEO-153: thin main wiring helpers.
const ContractHudMainWiring := preload("res://scripts/contract_hud_main_wiring.gd")
const InteractableWorldBuilder := preload("res://scripts/interactable_world_builder.gd")
func test_try_issue_key_input_returns_false_without_controller() -> void:
# Arrange
var event := InputEventKey.new()
# Act
var handled: bool = ContractHudMainWiring.try_issue_key_input(null, event)
# Assert
assert_bool(handled).is_false()
func test_set_collision_shapes_disabled_recurses_tree() -> void:
# Arrange
var root := Node3D.new()
var shape := CollisionShape3D.new()
root.add_child(shape)
# Act
InteractableWorldBuilder.set_collision_shapes_disabled(root, true)
# Assert
assert_bool(shape.disabled).is_true()
root.free()

View File

@ -0,0 +1 @@
uid://bneo153mwtest01