spaceship/tests/test_research_double_tap.gd

140 lines
4.9 KiB
GDScript3
Raw Normal View History

Implement #80, #81, #82 and #83 #80 - desktop combat. The mouse aims, the left button fires, and the number keys pick the weapon: 1 the fleet's guns, 2 and up the 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. The pointer path hands its heading to FireStickController rather than driving weapons itself, so exactly one thing in the scene aims a gun, and a held stick wins - a finger on it is a deliberate act, a mouse resting somewhere is not. Abilities gained fire_at(), which aims and fires in one call instead of pretending to be a press-drag-release touch. It is gated on the absence of a touchscreen, because Godot emulates a mouse button from every touch and without that a tap on a phone would fire the guns. The gate has an explicit override, and not for tidiness: headless reports a touchscreen, so with the automatic answer alone the aiming could never be exercised by a test and the only covered behaviour would be the guard. #81 - the bottom-left control hint is gone, from both scenes and from GameController. #82 - a chaser is destroyed by the impact when it reaches a vessel. It used to pass through the wreck untouched and line up another pass, which made a ramming enemy strictly better than any other. Data-driven per enemy (dies_on_contact), because a sniper that happens to drift into a ship has not spent itself doing it - and routed through take_damage, so it dies exactly as it dies to gunfire. The enemy is told about the impact BEFORE the ship resolves its own damage, since resolving it can free the ship. #83 - the research tree's buy button is gone; tapping a node twice buys it. Deliberately "the same node twice" rather than a timed double-tap: no window to miss on a slow phone, no accidental purchase while browsing, and the first tap always does something visible, so the second is never a blind commitment. The status line now says "Tap again to research" - with the button gone it is the only thing distinguishing an affordable node from an unaffordable one. test_enemy_collision asserted the exact behaviour #82 reversed, and now checks the ram costs both. Suite 117/117. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:49:47 +02:00
extends SceneTree
# Headless gameplay test: run with
# godot --headless -s res://tests/test_research_double_tap.gd
# Forgejo #83: the research tree's separate buy button is gone. Tapping a node
# selects it; tapping the SAME node again buys it.
#
# Covers:
# A. the buy button really is gone from the scene, not merely bypassed - a
# leftover would still be tappable and would still be the thing a player
# reaches for
# B. the first tap selects and does NOT buy, so browsing the tree is free
# C. a second tap on the same node buys it
# D. moving to a DIFFERENT node resets that - two taps on two nodes must
# never add up to a purchase
# E. tapping an unaffordable node twice buys nothing and says why
var game_state: Node
var main: Node
var tree_view: Node
var controller: Node
var stage: int = 0
var elapsed: float = 0.0
const NODE_ID := "shipyards"
func _initialize() -> void:
game_state = root.get_node("GameState")
game_state.buildings.clear()
game_state.extractors.clear()
game_state.reset_progression()
main = load("res://Main.tscn").instantiate()
root.add_child(main)
current_scene = main
controller = main.get_node("BuildController")
tree_view = main.get_node("UI/SkillMenu/Tree")
func _tap(id: String) -> void:
# Straight at the node's own position in the tree's local space, so this
# exercises the same hit test a finger does.
var node: Dictionary = game_state.skill_node(id)
var event := InputEventScreenTouch.new()
event.index = 1
event.pressed = true
event.position = tree_view.global_position + tree_view.node_screen_pos(node)
tree_view.handle_touch(event)
var release := InputEventScreenTouch.new()
release.index = 1
release.pressed = false
release.position = event.position
tree_view.handle_touch(release)
func _process(delta: float) -> bool:
elapsed += delta
if stage != 0:
return false
if elapsed < 0.3:
return false
stage = 1
# --- A. the button is gone ---------------------------------------------
if main.get_node_or_null("UI/SkillMenu/UnlockButton") != null:
return _fail("the research panel still has an UnlockButton node")
print("the separate buy button is gone from the panel")
main.get_node("UI/SkillMenu").visible = true
tree_view.visible = true
tree_view.reset_scroll()
game_state.unlocked.clear()
game_state.research_points = 99
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 500)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
var cost: int = int(game_state.skill_node(NODE_ID).cost)
var before: int = game_state.available_ore()
# --- B. the first tap only selects -------------------------------------
_tap(NODE_ID)
if tree_view.selected_id != NODE_ID:
return _fail("the first tap did not select the node")
if game_state.is_unlocked(NODE_ID):
return _fail("the first tap bought the research - browsing the tree must be free")
if game_state.available_ore() != before:
return _fail("the first tap spent %d" % (before - game_state.available_ore()))
print("first tap selects and spends nothing")
# --- C. the second tap buys --------------------------------------------
_tap(NODE_ID)
if not game_state.is_unlocked(NODE_ID):
return _fail("a second tap on the same node did not buy it")
if game_state.available_ore() != before - cost:
return _fail("bought for %d, expected %d" % [before - game_state.available_ore(), cost])
print("second tap on the same node buys it (%d ore)" % cost)
# --- D. two taps on two nodes are not a purchase -----------------------
var first: String = ""
var second: String = ""
for node in GameState.SKILL_TREE:
var id: String = String(node.id)
if game_state.is_unlocked(id) or not game_state.prerequisite_met(id):
continue
if first == "":
first = id
elif second == "":
second = id
break
if second == "":
return _fail("could not find two unlocked-but-available nodes for the cross-node case")
var ore_before: int = game_state.available_ore()
_tap(first)
_tap(second)
if game_state.is_unlocked(first) or game_state.is_unlocked(second):
return _fail("tapping two different nodes bought one of them")
if game_state.available_ore() != ore_before:
return _fail("tapping two different nodes spent %d" % (ore_before - game_state.available_ore()))
print("one tap each on two nodes buys neither")
# --- E. unaffordable stays unaffordable --------------------------------
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
_tap(second)
_tap(second)
if game_state.is_unlocked(second):
return _fail("a node was bought with no ore at all")
controller._refresh_skill_menu(second)
if controller.skill_status.text == "":
return _fail("an unaffordable node was refused silently")
print("unaffordable node refused, and says so: '%s'" % controller.skill_status.text)
print("TEST_RESEARCH_DOUBLE_TAP: PASS")
quit(0)
return true
func _fail(message: String) -> bool:
print("TEST_RESEARCH_DOUBLE_TAP: FAIL (%s)" % message)
quit(1)
return true