182 lines
7 KiB
GDScript
182 lines
7 KiB
GDScript
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
|
|
# F. one physical tap counts ONCE, even though the device delivers it twice
|
|
# - as a touch and as the mouse button emulated from it. It did not, and
|
|
# that is #162: the duplicate found the node already selected, took
|
|
# itself for the confirming tap, and bought the research on first touch
|
|
|
|
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()
|
|
game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO)
|
|
main = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(main)
|
|
current_scene = main
|
|
controller = main.get_node("BuildController")
|
|
tree_view = main.get_node("UI/SkillMenu/Tree")
|
|
|
|
|
|
func _node_position(id: String) -> Vector2:
|
|
return tree_view.global_position + tree_view.node_screen_pos(game_state.skill_node(id))
|
|
|
|
|
|
# ONE physical tap, delivered the way a real device delivers it: as a touch
|
|
# AND as the mouse button Godot emulates from it. Both settings are on
|
|
# (emulate_mouse_from_touch and emulate_touch_from_mouse), and the menu's
|
|
# dispatcher routes both kinds to the tree - so a handler that is not
|
|
# idempotent per gesture sees every tap twice. It did, and that made the FIRST
|
|
# tap buy (#162). Going through both paths here is the whole point; tapping
|
|
# only one of them would pass while the game stayed broken.
|
|
func _tap(id: String) -> void:
|
|
var at: Vector2 = _node_position(id)
|
|
var press := InputEventScreenTouch.new()
|
|
press.index = 1
|
|
press.pressed = true
|
|
press.position = at
|
|
tree_view.handle_touch(press)
|
|
|
|
var emulated := InputEventMouseButton.new()
|
|
emulated.button_index = MOUSE_BUTTON_LEFT
|
|
emulated.pressed = true
|
|
emulated.position = at
|
|
emulated.global_position = at
|
|
tree_view.handle_mouse_button(emulated)
|
|
|
|
var release := InputEventScreenTouch.new()
|
|
release.index = 1
|
|
release.pressed = false
|
|
release.position = at
|
|
tree_view.handle_touch(release)
|
|
var emulated_release := InputEventMouseButton.new()
|
|
emulated_release.button_index = MOUSE_BUTTON_LEFT
|
|
emulated_release.pressed = false
|
|
emulated_release.position = at
|
|
emulated_release.global_position = at
|
|
tree_view.handle_mouse_button(emulated_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")
|
|
|
|
# Open through the menu's public API so research has the mothership as its
|
|
# payment site. Since fleet-held resources became location-aware, merely
|
|
# forcing the Control visible no longer represents a reachable game state.
|
|
var mothership_id: int = 0
|
|
for building in game_state.buildings:
|
|
if String(building.type) == GameState.BUILDING_MOTHERSHIP:
|
|
mothership_id = int(building.id)
|
|
break
|
|
if mothership_id == 0:
|
|
return _fail("the test has no mothership research site")
|
|
main.get_node("UI/SkillMenu").open_for(mothership_id)
|
|
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)
|
|
# #134: the refusal is shown on the price rather than written out in a
|
|
# sentence below it - what you hold over what it costs, in red.
|
|
if controller.skill_menu.cost.is_affordable():
|
|
return _fail("a node the player cannot pay for reads as affordable")
|
|
if not controller.skill_menu.cost.is_short(GameState.RESOURCE_ORE):
|
|
return _fail("the ore the player does not have is not marked short")
|
|
print("unaffordable node refused, and its price says so: %d/%d ore" \
|
|
% [controller.skill_menu.cost.held(GameState.RESOURCE_ORE),
|
|
controller.skill_menu.cost.priced(GameState.RESOURCE_ORE)])
|
|
|
|
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
|