192 lines
8.6 KiB
GDScript
192 lines
8.6 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless test: run with
|
|
# godot --headless -s res://tests/test_menu_price_unification.gd
|
|
# Forgejo #137: "the resources at the top are not always shown the same way,
|
|
# the price is not always put at the same place... the build menu, the
|
|
# research menu, the ship crafter menu would benefit from a more unified
|
|
# UX." And #134, which is the same complaint aimed at the skill panel: the
|
|
# explanation could land on top of the price, and a shortfall was a sentence
|
|
# instead of a number.
|
|
#
|
|
# The point of the ticket is CONSISTENCY, which is a property of several
|
|
# things at once - so this test compares the menus against each other rather
|
|
# than against written-down coordinates. A menu that moves is fine; a menu
|
|
# that moves away from the others is the bug.
|
|
#
|
|
# Covers:
|
|
# A. every buying menu has a resource bar, in the same place, the same size
|
|
# B. every price is the same widget - one class, not three presentations
|
|
# C. a price says what you HAVE as well as what it COSTS, with the held
|
|
# figure smaller than the price (#134) and red when it is short
|
|
# D. #134 - the skill panel's explanation is left of its price, not on top
|
|
# of it, and the two do not overlap at any description length
|
|
# E. the shortfall is no longer also spelled out in a sentence
|
|
|
|
const PRICED_MENUS: Array = ["BuildMenu", "CraftMenu", "LabMenu"]
|
|
# Compared by SCRIPT PATH, as a string, and that is not fussiness.
|
|
# ResourceBar and PriceTag both read the GameState autoload, and an
|
|
# `extends SceneTree` script that so much as pulls one of them in - naming
|
|
# the class, or preloading the file - fails to compile, because GameState is
|
|
# not an identifier in this context. Worse, GDScript caches that failure, so
|
|
# the scene instantiated a few lines below would get a Control with no script
|
|
# where its resource bar should be. A string names the file without asking
|
|
# for it, and "is this the same script" is the question anyway.
|
|
const RESOURCE_BAR_PATH: String = "res://ui/ResourceBar.gd"
|
|
const PRICE_TAG_PATH: String = "res://ui/PriceTag.gd"
|
|
|
|
var game_state: Node
|
|
var scene: Node
|
|
var controller: Node
|
|
var stage: int = 0
|
|
var elapsed: float = 0.0
|
|
|
|
|
|
func _initialize() -> void:
|
|
# GameState is an autoload: a bare GameState.<instance member> does not
|
|
# compile in a SceneTree script, so the node is fetched by name instead.
|
|
game_state = root.get_node("GameState")
|
|
game_state.reset_new_game()
|
|
game_state.intro_active = false
|
|
game_state.reset_quests()
|
|
game_state.rocks = 40
|
|
|
|
|
|
func _fail(message: String) -> bool:
|
|
print("TEST_MENU_PRICE_UNIFICATION: FAIL (%s)" % message)
|
|
quit(1)
|
|
return true
|
|
|
|
|
|
func _script_path(node: Node) -> String:
|
|
var script: Script = node.get_script()
|
|
return "" if script == null else script.resource_path
|
|
|
|
|
|
# The RESOLVED rectangle, not the offsets. Two Controls with different
|
|
# anchors have offsets measured from different edges, so comparing those
|
|
# compares nothing - and subtracting them produces negative sizes, which
|
|
# Rect2.intersects refuses outright.
|
|
func _rect(node: Control) -> Rect2:
|
|
return node.get_rect()
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
elapsed += delta
|
|
match stage:
|
|
0:
|
|
scene = load("res://world/Main.tscn").instantiate()
|
|
root.add_child(scene)
|
|
current_scene = scene
|
|
controller = scene.get_node("BuildController")
|
|
elapsed = 0.0
|
|
stage = 1
|
|
return false
|
|
1:
|
|
if elapsed < 0.4:
|
|
return false
|
|
|
|
# --- A. the same bar, in the same place ------------------
|
|
var bars: Dictionary = {}
|
|
for menu in PRICED_MENUS:
|
|
var bar: Control = scene.get_node_or_null("UI/%s/Bar" % menu)
|
|
if bar == null:
|
|
return _fail("%s has no resource bar - the other menus do" % menu)
|
|
if _script_path(bar) != RESOURCE_BAR_PATH:
|
|
return _fail("%s's bar is not a ResourceBar" % menu)
|
|
bars[menu] = bar
|
|
var reference: Rect2 = _rect(bars[PRICED_MENUS[0]])
|
|
for menu in PRICED_MENUS:
|
|
var here: Rect2 = _rect(bars[menu])
|
|
if here != reference:
|
|
return _fail("%s's resource bar sits at %s, the others at %s" \
|
|
% [menu, here, reference])
|
|
if bars[menu].anchor_right != bars[PRICED_MENUS[0]].anchor_right:
|
|
return _fail("%s's resource bar is anchored differently" % menu)
|
|
print("%d menus, one resource bar geometry: %s" % [PRICED_MENUS.size(), reference])
|
|
|
|
# --- B. one price widget everywhere ----------------------
|
|
var tags: Array = []
|
|
for entry in controller.build_menu.entries:
|
|
tags.append(entry.cost)
|
|
tags.append_array(controller.craft_menu.price_tags)
|
|
tags.append_array(controller.lab_menu.price_tags)
|
|
tags.append(controller.build_menu.info_cost)
|
|
tags.append(controller.skill_menu.cost)
|
|
if tags.size() < 15:
|
|
return _fail("only %d price widgets found - the menus cannot all be covered" % tags.size())
|
|
for tag in tags:
|
|
if _script_path(tag) != PRICE_TAG_PATH:
|
|
return _fail("a price is drawn by %s, not the shared price tag" \
|
|
% _script_path(tag))
|
|
print("%d prices across the build, craft, lab and skill menus, all one widget" % tags.size())
|
|
|
|
# --- C. have over cost, smaller, red when short ----------
|
|
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 3)
|
|
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
|
var probe: Control = controller.build_menu.info_cost
|
|
probe.show_price({GameState.RESOURCE_ORE: 25})
|
|
if probe.priced(GameState.RESOURCE_ORE) != 25:
|
|
return _fail("the tag does not carry the price it was given")
|
|
if probe.held(GameState.RESOURCE_ORE) != 3:
|
|
return _fail("the tag reads %d held, the player has 3" \
|
|
% probe.held(GameState.RESOURCE_ORE))
|
|
if not probe.is_short(GameState.RESOURCE_ORE):
|
|
return _fail("3 out of 25 is not marked short")
|
|
if probe.is_affordable():
|
|
return _fail("a tag the player cannot pay reads as affordable")
|
|
# Read off the labels the player actually sees, rather than off
|
|
# the constants behind them.
|
|
var entry: Dictionary = probe._entry_for(GameState.RESOURCE_ORE)
|
|
var have_size: int = entry.have_label.get_theme_font_size("font_size")
|
|
var cost_size: int = entry.cost_label.get_theme_font_size("font_size")
|
|
if have_size >= cost_size:
|
|
return _fail("what you hold (size %d) is not drawn smaller than the price (size %d)" \
|
|
% [have_size, cost_size])
|
|
var short_colour: Color = entry.have_label.get_theme_color("font_color")
|
|
if short_colour.r <= short_colour.g or short_colour.r <= short_colour.b:
|
|
return _fail("the short figure is drawn in %s, which is not a warning colour" % short_colour)
|
|
print("price tag: %d/%d ore, held figure at size %d in %s, price at size %d" \
|
|
% [probe.held(GameState.RESOURCE_ORE), probe.priced(GameState.RESOURCE_ORE),
|
|
have_size, short_colour, cost_size])
|
|
# ...and the affordable case is not red, or the colour says nothing.
|
|
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 900)
|
|
probe.show_price({GameState.RESOURCE_ORE: 25})
|
|
if probe.is_short(GameState.RESOURCE_ORE) or not probe.is_affordable():
|
|
return _fail("900 ore does not cover a price of 25")
|
|
if probe._entry_for(GameState.RESOURCE_ORE).have_label \
|
|
.get_theme_color("font_color") == short_colour:
|
|
return _fail("an affordable price is still drawn in the shortfall colour")
|
|
print("and an affordable price is drawn differently")
|
|
|
|
# --- D. #134: the explanation is beside the price --------
|
|
var longest: String = ""
|
|
for node in GameState.SKILL_TREE:
|
|
if String(node.desc).length() > longest.length():
|
|
longest = String(node.desc)
|
|
controller._refresh_skill_menu(String(node.id))
|
|
var detail: Rect2 = _rect(controller.skill_menu.detail)
|
|
var price: Rect2 = _rect(controller.skill_menu.cost)
|
|
if detail.intersects(price):
|
|
return _fail("the explanation (%s) overlaps the price (%s)" % [detail, price])
|
|
if detail.position.x >= price.position.x:
|
|
return _fail("the explanation is not left of the price")
|
|
print("skill panel: explanation at x=%.0f, price at x=%.0f, no overlap (longest of %d descriptions)" \
|
|
% [detail.position.x, price.position.x, GameState.SKILL_TREE.size()])
|
|
|
|
# --- E. and the shortfall is not also a sentence ---------
|
|
game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
|
game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 0)
|
|
game_state.unlocked.clear()
|
|
controller._refresh_skill_menu("shipyards")
|
|
if controller.skill_menu.cost.is_affordable():
|
|
return _fail("a node reads as affordable with no ore at all")
|
|
if controller.skill_menu.status.text.to_lower().contains("not enough"):
|
|
return _fail("the shortfall is still written out as well: '%s'" \
|
|
% controller.skill_menu.status.text)
|
|
print("the shortfall is shown once, on the price, and not repeated in words")
|
|
|
|
print("TEST_MENU_PRICE_UNIFICATION: PASS")
|
|
quit(0)
|
|
return true
|
|
return false
|