extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_menu_polish.gd # Forgejo #111, #112, #119 and #120 - four panels that were hard to read, # each for its own reason. Grouped because they are all "what does this # panel actually show", and because each alone is a handful of assertions. # # Covers: # A. #111 - the craft menu names no resources: prices are glyphs, and # every vessel gets a caption that does not overlap its neighbour # B. #111 - and a price strip really carries the vessel's cost # D. #143 - a hangar caption fits the box it was given and cannot draw # outside it, so it never lands on the icon of the row below # C. #120 - the hangar's captions have room for two lines, and there is a # rule between what is parked and what is flying # D. #112 - every skill node has a mark, and a node that unlocks a # building uses that building's own icon # E. #119 - the command centre draws the vessels actually in the fleet, # not one placeholder repeated 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.buildings.clear() game_state.extractors.clear() game_state.reset_progression() game_state.reset_waves() game_state.reset_formation() game_state.intro_active = false game_state.rocks = 5000 scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) current_scene = scene controller = scene.get_node("BuildController") func _fail(message: String) -> bool: print("TEST_MENU_POLISH: FAIL (%s)" % message) quit(1) return true func _process(delta: float) -> bool: elapsed += delta match stage: 0: if elapsed < 0.4: return false scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) # --- A. the craft menu spells no resource out ------------- controller.craft_building_id = 1 controller._refresh_craft_status() # The caption is the vessel's NAME and nothing else. Checked as an # exact match rather than by hunting for resource words in it: a # Gas Bomber is named after a resource and always will be, and # what #111 removed was the PRICE spelled out underneath. var fleet_defs: Array = VesselDefinitions.fleet() for i in range(mini(controller.craft_vessel_labels.size(), fleet_defs.size())): var caption: String = String(controller.craft_vessel_labels[i].text) var expected: String = String(fleet_defs[i].display_name) if caption != expected: return _fail("a craft caption reads \"%s\", expected just \"%s\"" \ % [caption, expected]) # Captions must not sit on top of one another, which is what # made the old single row unreadable. for i in range(controller.craft_vessel_labels.size()): for j in range(i + 1, controller.craft_vessel_labels.size()): var a: Control = controller.craft_vessel_labels[i] var b: Control = controller.craft_vessel_labels[j] if Rect2(a.position, a.size).intersects(Rect2(b.position, b.size)): return _fail("craft captions %d and %d overlap" % [i, j]) print("craft captions: %d, one line each, none overlapping" \ % controller.craft_vessel_labels.size()) # --- B. and the price is really there --------------------- if controller.craft_price_strips.size() != controller.craft_vessel_buttons.size(): return _fail("%d price strips for %d vessels" \ % [controller.craft_price_strips.size(), controller.craft_vessel_buttons.size()]) var priced: int = 0 for i in range(controller.craft_price_strips.size()): var definition: Resource = VesselDefinitions.fleet()[i] var costs: Dictionary = game_state.product_costs(definition.definition_id) var strip: Control = controller.craft_price_strips[i] if costs.is_empty(): continue if strip.is_free(): return _fail("%s costs %s and its strip is empty" \ % [definition.definition_id, str(costs)]) priced += 1 if priced == 0: return _fail("no vessel showed a price at all") print("%d vessels show their price as glyphs" % priced) # --- C. the hangar --------------------------------------- controller._layout_hangar() if controller.hangar_divider_y <= 0.0: return _fail("the hangar laid out with no divider between its halves") var available_bottom: float = 0.0 var fleet_top: float = 1.0e9 for label in controller.hangar_available_labels: available_bottom = maxf(available_bottom, label.position.y + label.size.y) for slot in controller.hangar_fleet_slots: fleet_top = minf(fleet_top, slot.position.y) if not (controller.hangar_divider_y > available_bottom - 40.0 and controller.hangar_divider_y < fleet_top): return _fail("the divider at %.0f is not between the two grids (%.0f / %.0f)" \ % [controller.hangar_divider_y, available_bottom, fleet_top]) # Two lines of caption. The font is 24-ish, so anything under # about 44 cannot show a second line at all. var caption_height: float = controller.hangar_available_labels[0].size.y if caption_height < 44.0: return _fail("hangar captions are %.0fpx tall - a two-word name is clipped" \ % caption_height) print("hangar: %.0fpx captions, divider at %.0f between %.0f and %.0f" \ % [caption_height, controller.hangar_divider_y, available_bottom, fleet_top]) # --- D. #143 - and the caption stays in its box -------------- # The caption is two lines before autowrap and three after it in # a narrow column, at a size fixed in the scene - so the third # line was drawn over the icon of the row below. It is fitted to # the box now, and the box clips, so nothing can reach the next # row however the vessel names grow. controller._refresh_hangar() for pair in [[controller.hangar_available_labels, controller.hangar_available_slots], [controller.hangar_fleet_labels, controller.hangar_fleet_slots]]: for i in range(pair[0].size()): var caption: Label = pair[0][i] if not caption.clip_text: return _fail("hangar caption %d can draw outside its box" % i) var font: Font = caption.get_theme_font("font") var size_px: int = caption.get_theme_font_size("font_size") var needed: float = font.get_multiline_string_size(caption.text, HORIZONTAL_ALIGNMENT_CENTER, caption.size.x, size_px).y if needed > caption.size.y + 0.5: return _fail("caption \"%s\" needs %.0fpx at size %d and has %.0f" \ % [caption.text.replace("\n", " / "), needed, size_px, caption.size.y]) # And the box itself must not reach the row below it. for slot in pair[1]: var below: Rect2 = Rect2(slot.position, slot.size) if below.position.y > caption.position.y \ and Rect2(caption.position, caption.size).intersects(below): return _fail("caption \"%s\" runs into the icon below it" \ % caption.text.replace("\n", " / ")) var probe: Label = controller.hangar_available_labels[0] print("captions fitted to %dpx and clipped; \"%s\" fits its %.0fpx box" \ % [probe.get_theme_font_size("font_size"), probe.text.replace("\n", " / "), probe.size.y]) # --- D. every skill node has a mark ----------------------- var tree_view: Control = scene.get_node("UI/SkillMenu/Tree") var marked: int = 0 for node in GameState.SKILL_TREE: var icon: int = game_state.skill_node_icon(node) if icon < 0: return _fail("skill node \"%s\" has no icon" % node.id) marked += 1 var unlocks: String = String(node.get("unlocks", "")) if unlocks == "": continue var definition: Resource = game_state.building_definition(unlocks) if definition != null and icon != int(definition.menu_icon): return _fail("\"%s\" unlocks the %s but draws a different icon" \ % [node.id, unlocks]) if tree_view._glyphs.size() != marked: return _fail("%d nodes have icons but the tree built %d glyphs" \ % [marked, tree_view._glyphs.size()]) print("%d skill nodes, every one marked, unlocks matching their building" % marked) # --- E. the formation shows the real fleet ---------------- scene.get_node("Fleet").spawn_ships(2, Vector2.ZERO, PackedStringArray([ GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD])) elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.2: return false var editor: Control = scene.get_node("UI/FormationMenu/Editor") var crewed: PackedStringArray = game_state.fleet_vessel_types if crewed.size() != 2: return _fail("the fleet mirror holds %d types, expected 2" % crewed.size()) var interceptor: Resource = game_state.vessel_definition(GameState.VESSEL_INTERCEPTOR) var shield: Resource = game_state.vessel_definition(GameState.VESSEL_SHIELD) if interceptor.hull_points == shield.hull_points: return _fail("the two vessels share a hull, so this case proves nothing") if not editor.has_method("_draw_vessel_slot"): return _fail("the editor has no per-vessel drawing at all") # The panel draws from the mirror, so the mirror having the right # types in the right order IS what makes the markers differ. if String(crewed[0]) != GameState.VESSEL_INTERCEPTOR \ or String(crewed[1]) != GameState.VESSEL_SHIELD: return _fail("the fleet mirror is %s, not the order they were crewed in" \ % str(crewed)) print("formation draws a %s and a %s, in the slots they fly in" \ % [interceptor.display_name, shield.display_name]) print("TEST_MENU_POLISH: PASS") quit(0) return true return false