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. 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[3]) tags.append_array(controller.craft_price_strips) tags.append_array(controller.lab_price_tags) tags.append(controller.info_cost) tags.append(controller.skill_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.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_detail) var price: Rect2 = _rect(controller.skill_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_cost.is_affordable(): return _fail("a node reads as affordable with no ore at all") if controller.skill_status.text.to_lower().contains("not enough"): return _fail("the shortfall is still written out as well: '%s'" \ % controller.skill_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