132 lines
5.1 KiB
GDScript
132 lines
5.1 KiB
GDScript
extends Node2D
|
|
|
|
# Forgejo #80: on a computer the mouse aims and the left button fires, and the
|
|
# number keys choose which weapon the click uses - 1 the fleet's own guns, 2
|
|
# and up the special abilities.
|
|
#
|
|
# The twin sticks are built for thumbs. On a desktop they mean dragging a
|
|
# virtual stick with a pointer that could simply BE the crosshair, so this
|
|
# offers the shape a mouse actually wants: point, click, hold to keep firing.
|
|
#
|
|
# Touch is unaffected, and deliberately so: Godot emulates a mouse button from
|
|
# every touch, so without a guard a tap anywhere on a phone would fire the
|
|
# guns.
|
|
#
|
|
# #171 changed what that guard asks. It used to be the presence of a
|
|
# touchscreen, which fails safe but fails often: a desktop browser reporting a
|
|
# touch-capable panel switched mouse aiming off on exactly the machine it was
|
|
# written for. It now asks what the player last actually used
|
|
# (GameState.pointer_is_touch), which still fails safe - a tap makes the
|
|
# reading "touch" before the emulated click arrives, so a phone can never fire
|
|
# through this path - and stops being wrong about desktops.
|
|
|
|
# A literal, not PackedStringArray(...): a constructor call is not a constant
|
|
# expression, and the script fails to PARSE rather than to run - which shows up
|
|
# as "nonexistent function" on a node that looks fine in the scene.
|
|
const ABILITY_ORDER: PackedStringArray = ["GasBombController", "LaserController", "DashController"]
|
|
|
|
@onready var fleet: Node2D = get_node("../Fleet")
|
|
@onready var camera: Camera2D = get_node("../Camera2D")
|
|
@onready var fire_stick_controller: Node = get_node_or_null("../FireStickController")
|
|
|
|
# 0 is the fleet's own guns; 1.. index into `abilities` below.
|
|
var selected_weapon: int = 0
|
|
var firing: bool = false
|
|
var aim_world: Vector2 = Vector2.ZERO
|
|
var abilities: Array = []
|
|
|
|
|
|
func _ready() -> void:
|
|
# Resolved by NAME in a fixed order, so "2" is the gas bomb on every
|
|
# scene and in every session. Discovering them by scanning children would
|
|
# make the number keys depend on scene node order, which is not something
|
|
# a player can see or rely on.
|
|
for node_name in ABILITY_ORDER:
|
|
var controller: Node = get_node_or_null("../%s" % node_name)
|
|
if controller != null:
|
|
abilities.append(controller)
|
|
|
|
|
|
# AUTO follows the last input the player actually used, which is what every
|
|
# scene ships with. The explicit settings exist because that reading is seeded
|
|
# from the display in an environment - headless - where the answer varies by
|
|
# platform, so with AUTO alone the aiming below could never be exercised by a
|
|
# test at all, and the only covered behaviour would be the guard itself.
|
|
enum PointerMode { AUTO, ON, OFF }
|
|
|
|
@export var pointer_mode: PointerMode = PointerMode.AUTO
|
|
|
|
|
|
func is_enabled() -> bool:
|
|
match pointer_mode:
|
|
PointerMode.ON:
|
|
return true
|
|
PointerMode.OFF:
|
|
return false
|
|
return not GameState.pointer_is_touch
|
|
|
|
|
|
func weapon_count() -> int:
|
|
return abilities.size() + 1
|
|
|
|
|
|
# Which weapon a given number key selects. Public so a test can ask rather
|
|
# than hardcode the mapping a second time.
|
|
func select_weapon(index: int) -> bool:
|
|
if index < 0 or index >= weapon_count():
|
|
return false
|
|
selected_weapon = index
|
|
_refresh_highlight()
|
|
return true
|
|
|
|
|
|
func _refresh_highlight() -> void:
|
|
# The chosen ability's own button carries the selection, so the answer to
|
|
# "what will a click do" is on screen rather than remembered.
|
|
for i in range(abilities.size()):
|
|
var button: Node = abilities[i].get("button")
|
|
if button != null:
|
|
button.highlighted = selected_weapon == i + 1
|
|
button.queue_redraw()
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if not is_enabled():
|
|
return
|
|
if event is InputEventMouseMotion:
|
|
aim_world = get_global_mouse_position()
|
|
return
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
var index: int = event.keycode - KEY_1
|
|
if index >= 0 and index < weapon_count() and select_weapon(index):
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if not (event is InputEventMouseButton) or event.button_index != MOUSE_BUTTON_LEFT:
|
|
return
|
|
|
|
aim_world = get_global_mouse_position()
|
|
firing = event.pressed
|
|
if not event.pressed:
|
|
return
|
|
get_viewport().set_input_as_handled()
|
|
if selected_weapon > 0:
|
|
# One shot per click. Abilities have their own cooldowns and costs;
|
|
# holding the button down must not queue them up.
|
|
abilities[selected_weapon - 1].fire_at(aim_world)
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if fire_stick_controller == null:
|
|
return
|
|
# Handed to the stick controller rather than driving the weapons here, so
|
|
# there is still exactly one thing in the scene that aims a gun. The stick
|
|
# wins when both are live - a finger on it is a deliberate act, while a
|
|
# mouse merely resting somewhere is not.
|
|
#
|
|
# #172: the cursor's world position goes across as a PLACE. It always was
|
|
# one; it used to be flattened into a heading from the fleet's centre on
|
|
# the way out, which threw away the distance and made every hull fire
|
|
# parallel past the thing being clicked on.
|
|
fire_stick_controller.has_external_target = is_enabled() and firing \
|
|
and selected_weapon == 0 and not fleet.ships.is_empty()
|
|
fire_stick_controller.external_target = aim_world
|