178 lines
7.0 KiB
GDScript
178 lines
7.0 KiB
GDScript
extends CanvasLayer
|
|
signal closed
|
|
var panel: Control
|
|
var heading: Label
|
|
var prompt: Label
|
|
var monitor: Label
|
|
var details: Label
|
|
var buttons: HBoxContainer
|
|
var draft: Dictionary = {}
|
|
var step := -1
|
|
var waiting: InputEvent
|
|
var armed_at := 0
|
|
|
|
func _ready() -> void:
|
|
layer = 10
|
|
panel = ColorRect.new()
|
|
panel.color = Color("080d18")
|
|
panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_child(panel)
|
|
var center := CenterContainer.new()
|
|
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
panel.add_child(center)
|
|
var column := VBoxContainer.new()
|
|
column.custom_minimum_size = Vector2(1100, 0)
|
|
column.add_theme_constant_override("separation", 18)
|
|
center.add_child(column)
|
|
heading = label(column, 36)
|
|
prompt = label(column, 26)
|
|
details = label(column, 18)
|
|
monitor = label(column, 20)
|
|
monitor.modulate = Color("75e8ef")
|
|
buttons = HBoxContainer.new()
|
|
buttons.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
buttons.add_theme_constant_override("separation", 24)
|
|
column.add_child(buttons)
|
|
welcome()
|
|
Input.joy_connection_changed.connect(_controller_changed)
|
|
|
|
func _controller_changed(_id: int, _connected: bool) -> void:
|
|
waiting = null
|
|
draft.clear()
|
|
welcome()
|
|
prompt.text = "Controllers changed. Begin again with all controllers connected."
|
|
|
|
func label(parent: Node, font_size: int) -> Label:
|
|
var result := Label.new()
|
|
result.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
result.custom_minimum_size.x = 1100
|
|
result.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
result.add_theme_font_size_override("font_size", font_size)
|
|
parent.add_child(result)
|
|
return result
|
|
|
|
func clear_buttons() -> void:
|
|
for child in buttons.get_children():
|
|
buttons.remove_child(child)
|
|
child.queue_free()
|
|
|
|
func button(text: String, callback: Callable) -> void:
|
|
var control := Button.new()
|
|
control.text = text
|
|
control.add_theme_font_size_override("font_size", 24)
|
|
control.focus_mode = Control.FOCUS_NONE
|
|
control.pressed.connect(callback)
|
|
buttons.add_child(control)
|
|
|
|
func welcome() -> void:
|
|
step = -1
|
|
heading.text = "CONTROL SETUP"
|
|
prompt.text = "Configure this cabinet — no editor needed"
|
|
details.text = "Use PLAYER ONE for movement, Start, Select and game buttons.\nFor Robotron firing, use four face buttons OR your second joystick.\nEach controller is recorded separately. Release each input when asked.\n\nThe trackball works as a mouse; move it to see it detected below.\nKeyboard encoders work too. Escape quits the application. F1 opens setup.\n\nPress any controller button / Enter to begin, or click Begin Setup.\nCancel keeps your saved controls. Escape quits without saving."
|
|
if not ArcadeControls.problem.is_empty(): details.text += "\n\n" + ArcadeControls.problem
|
|
monitor.text = "Waiting for input…"
|
|
clear_buttons()
|
|
button("Begin Setup", begin)
|
|
button("Cancel / Keep Current Controls", close)
|
|
armed_at = Time.get_ticks_msec() + 350
|
|
|
|
func begin() -> void:
|
|
draft.clear()
|
|
waiting = null
|
|
step = 0
|
|
armed_at = Time.get_ticks_msec() + 350
|
|
show_step()
|
|
|
|
func show_step() -> void:
|
|
clear_buttons()
|
|
if step >= ArcadeControls.STEPS.size():
|
|
heading.text = "REVIEW & TEST CONTROLS"
|
|
prompt.text = "Try both joysticks and buttons; watch the detected actions below."
|
|
details.text = ""
|
|
for entry in ArcadeControls.STEPS:
|
|
details.text += "%s: %s\n" % [entry[0].replace("arcade_", "").replace("robotron_", "Robotron ").replace("_", " ").capitalize(), ArcadeControls.describe(draft[entry[0]])]
|
|
details.text += "\nPress your learned START to save, SELECT to cancel, or click a button.\nTrackball motion needs no button binding. Identical USB encoders may swap after reconnecting."
|
|
button("Save Controls", save)
|
|
button("Start Over", begin)
|
|
button("Cancel", close)
|
|
return
|
|
heading.text = "CONTROL SETUP %d / %d" % [step + 1, ArcadeControls.STEPS.size()]
|
|
prompt.text = ArcadeControls.STEPS[step][1]
|
|
details.text = "Move one direction or press one button, then release it.\nUse the actual cabinet controls. Nothing is saved until you finish.\nCancel keeps your saved controls; Escape quits. Trackball motion is shown below but is not bound as a button."
|
|
button("Start Over", begin)
|
|
button("Cancel", close)
|
|
|
|
func close() -> void:
|
|
closed.emit()
|
|
queue_free()
|
|
|
|
func save() -> void:
|
|
var error := ArcadeControls.save_profile(draft)
|
|
if error != OK:
|
|
prompt.text = "Could not save controls (%s). Your previous settings are unchanged." % error_string(error)
|
|
return
|
|
close()
|
|
|
|
func released(event: InputEvent) -> bool:
|
|
if waiting == null or event.device != waiting.device: return false
|
|
if waiting is InputEventJoypadMotion and event is InputEventJoypadMotion:
|
|
return event.axis == waiting.axis and absf(event.axis_value) < .25
|
|
if waiting is InputEventJoypadButton and event is InputEventJoypadButton:
|
|
return event.button_index == waiting.button_index and not event.pressed
|
|
if waiting is InputEventKey and event is InputEventKey:
|
|
return event.physical_keycode == waiting.physical_keycode and event.keycode == waiting.keycode and not event.pressed
|
|
return false
|
|
|
|
func conflict(action: String, binding: Dictionary) -> String:
|
|
for other in draft:
|
|
if draft[other] != binding: continue
|
|
# Reusing game fire buttons for Robotron is deliberate; within each game it isn't.
|
|
if action.begins_with("robotron_") != other.begins_with("robotron_") and other not in ["arcade_start", "arcade_menu"]:
|
|
continue
|
|
return other
|
|
return ""
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if ArcadeControls.handle_quit(event): return
|
|
if event.is_echo(): return
|
|
if event is InputEventMouseMotion:
|
|
monitor.text = "Trackball / mouse motion: %s" % event.relative
|
|
return
|
|
if event is InputEventMouseButton: return # Leave setup buttons clickable.
|
|
if not (event is InputEventKey or event is InputEventJoypadButton or event is InputEventJoypadMotion): return
|
|
get_viewport().set_input_as_handled()
|
|
if waiting != null:
|
|
if released(event):
|
|
waiting = null
|
|
step += 1
|
|
armed_at = Time.get_ticks_msec() + 200
|
|
show_step()
|
|
return
|
|
if Time.get_ticks_msec() < armed_at: return
|
|
if event is InputEventJoypadMotion:
|
|
if absf(event.axis_value) < .65: return
|
|
elif not event.is_pressed(): return
|
|
var binding: Dictionary = ArcadeControls.encode(event, ArcadeControls.devices())
|
|
if binding.is_empty(): return
|
|
monitor.text = "Detected: " + ArcadeControls.describe(binding)
|
|
if step < 0:
|
|
if event is InputEventJoypadButton or (event is InputEventKey and event.physical_keycode == KEY_ENTER):
|
|
begin()
|
|
return
|
|
if step >= ArcadeControls.STEPS.size():
|
|
var actions: Array[String] = []
|
|
for action in draft:
|
|
if draft[action] == binding: actions.append(action)
|
|
monitor.text += "\nActions: " + (", ".join(actions) if not actions.is_empty() else "unassigned — will not control gameplay")
|
|
if binding == draft.arcade_start: save()
|
|
elif binding == draft.arcade_menu: close()
|
|
return
|
|
var action: String = ArcadeControls.STEPS[step][0]
|
|
var duplicate := conflict(action, binding)
|
|
if not duplicate.is_empty():
|
|
monitor.text = "Already used for %s. Choose a different input." % duplicate
|
|
return
|
|
draft[action] = binding
|
|
waiting = event.duplicate()
|
|
prompt.text = "Recorded. Release the button / center the joystick to continue."
|