264 lines
12 KiB
GDScript
264 lines
12 KiB
GDScript
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
|
|
# E. #147 - a price with several resources goes one per line and stays
|
|
# inside its own box, close to the button it is about
|
|
# F. #147 - and no craft entry overlaps another
|
|
# 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.<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.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_menu.vessel_labels.size(), fleet_defs.size())):
|
|
var caption: String = String(controller.craft_menu.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_menu.vessel_labels.size()):
|
|
for j in range(i + 1, controller.craft_menu.vessel_labels.size()):
|
|
var a: Control = controller.craft_menu.vessel_labels[i]
|
|
var b: Control = controller.craft_menu.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_menu.vessel_labels.size())
|
|
|
|
# --- B. and the price is really there ---------------------
|
|
if controller.craft_menu.price_tags.size() != controller.craft_menu.vessel_buttons.size():
|
|
return _fail("%d price strips for %d vessels" \
|
|
% [controller.craft_menu.price_tags.size(),
|
|
controller.craft_menu.vessel_buttons.size()])
|
|
var priced: int = 0
|
|
var stacked: int = 0
|
|
for i in range(controller.craft_menu.price_tags.size()):
|
|
var definition: Resource = VesselDefinitions.fleet()[i]
|
|
var costs: Dictionary = game_state.product_costs(definition.definition_id)
|
|
var strip: Control = controller.craft_menu.price_tags[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
|
|
# --- E. #147 - a price stays inside its own box ----------
|
|
# A two-resource price used to be laid out across a strip
|
|
# narrower than it needed, so it ran off the end and into the
|
|
# vessel beside it.
|
|
if costs.size() > 1 and not strip.is_stacked():
|
|
return _fail("%s is priced in %d resources on one line - they will not fit %.0fpx" \
|
|
% [definition.definition_id, costs.size(), strip.size.x])
|
|
if costs.size() > 1:
|
|
stacked += 1
|
|
if strip.wanted_height() > strip.size.y + 0.5:
|
|
return _fail("%s's price needs %.0fpx and its box is %.0f" \
|
|
% [definition.definition_id, strip.wanted_height(), strip.size.y])
|
|
for child in strip.get_children():
|
|
var box: Rect2 = Rect2((child as Control).position, (child as Control).size)
|
|
if box.position.x < -0.5 or box.end.x > strip.size.x + 0.5:
|
|
return _fail("%s's price runs from %.0f to %.0f in a %.0fpx strip" \
|
|
% [definition.definition_id, box.position.x, box.end.x, strip.size.x])
|
|
# ...and the price sits with the button it is about, not
|
|
# adrift below the caption.
|
|
var button: Control = controller.craft_menu.vessel_buttons[i]
|
|
var gap: float = strip.position.y - (button.position.y + button.size.y)
|
|
if gap > 80.0:
|
|
return _fail("%s's price sits %.0fpx below its button" \
|
|
% [definition.definition_id, gap])
|
|
if priced == 0:
|
|
return _fail("no vessel showed a price at all")
|
|
if stacked == 0:
|
|
return _fail("no vessel costs more than one resource - E proves nothing")
|
|
print("%d vessels show their price as glyphs, %d of them stacked one per line" \
|
|
% [priced, stacked])
|
|
|
|
# --- F. #147 - and the rows do not run into each other -------
|
|
for i in range(controller.craft_menu.vessel_buttons.size()):
|
|
for j in range(i + 1, controller.craft_menu.vessel_buttons.size()):
|
|
for part in [controller.craft_menu.vessel_buttons,
|
|
controller.craft_menu.vessel_labels, controller.craft_menu.price_tags]:
|
|
if i >= part.size() or j >= part.size():
|
|
continue
|
|
var a: Control = part[i]
|
|
var b: Control = part[j]
|
|
if Rect2(a.position, a.size).intersects(Rect2(b.position, b.size)):
|
|
return _fail("craft entries %d and %d overlap" % [i, j])
|
|
# A price must not reach the button of the row below it.
|
|
var mine: Control = controller.craft_menu.price_tags[i]
|
|
for k in range(controller.craft_menu.vessel_buttons.size()):
|
|
var other: Control = controller.craft_menu.vessel_buttons[k]
|
|
if other.position.y > mine.position.y \
|
|
and Rect2(mine.position, mine.size).intersects(
|
|
Rect2(other.position, other.size)):
|
|
return _fail("the price under vessel %d runs into vessel %d's button" % [i, k])
|
|
print("craft grid: nothing overlapping, prices under their own buttons")
|
|
|
|
# --- C. the hangar ---------------------------------------
|
|
controller._layout_hangar()
|
|
if controller.hangar_menu.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_menu.available_labels:
|
|
available_bottom = maxf(available_bottom, label.position.y + label.size.y)
|
|
for slot in controller.hangar_menu.fleet_slots:
|
|
fleet_top = minf(fleet_top, slot.position.y)
|
|
if not (controller.hangar_menu.divider_y > available_bottom - 40.0
|
|
and controller.hangar_menu.divider_y < fleet_top):
|
|
return _fail("the divider at %.0f is not between the two grids (%.0f / %.0f)" \
|
|
% [controller.hangar_menu.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_menu.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_menu.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_menu.available_labels, controller.hangar_menu.available_slots],
|
|
[controller.hangar_menu.fleet_labels, controller.hangar_menu.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_menu.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
|