130 lines
6 KiB
GDScript3
130 lines
6 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Headless test: run with
|
||
|
|
# godot --headless -s res://tests/test_definitions_intact.gd
|
||
|
|
#
|
||
|
|
# A guard against a failure mode that has now cost this project twice, and
|
||
|
|
# both times silently.
|
||
|
|
#
|
||
|
|
# Opening the project in the Godot editor re-saves .tres files, and its
|
||
|
|
# writer DROPS properties it considers default-valued. An empty-looking
|
||
|
|
# PackedStringArray is the usual casualty: GasBurnerBuildingDefinition lost
|
||
|
|
# `capabilities` and `collected_outputs` once (which took the gas burner's
|
||
|
|
# whole conversion with it), and InterceptorVesselDefinition later lost
|
||
|
|
# `capabilities`, `hull_points`, `build_time`, `icon` and `glow_color` -
|
||
|
|
# which cost the starting vessel its cargo hold, so nothing could be mined.
|
||
|
|
#
|
||
|
|
# Neither showed up as an error. The resource still loaded; it was just
|
||
|
|
# quietly less than it had been, and the failure surfaced somewhere else
|
||
|
|
# entirely - a full suite reporting ten unrelated tests broken.
|
||
|
|
#
|
||
|
|
# So this asserts the SHAPE of the catalog rather than any one field: every
|
||
|
|
# definition must carry the things that make it what it is, and a capability
|
||
|
|
# a definition claims must have a component behind it. Anything the editor
|
||
|
|
# eats fails here first, next to the thing that was eaten.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. every vessel is whole - name, cost, icon, hull, and stats
|
||
|
|
# B. a vessel with cargo declares it and has a capacity to go with it
|
||
|
|
# C. every building is whole, and its menu wiring is complete
|
||
|
|
# D. a converter building keeps the fields its conversion needs
|
||
|
|
# E. no definition has lost its component list
|
||
|
|
|
||
|
|
var failures: Array = []
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> String:
|
||
|
|
failures.append(message)
|
||
|
|
return message
|
||
|
|
|
||
|
|
|
||
|
|
# get() answers null for a property a definition's script never declared,
|
||
|
|
# and null has no is_empty(). These keep a missing field from looking like
|
||
|
|
# an empty one - and from taking the rest of the sweep down with it.
|
||
|
|
func _text(definition: Resource, field: String) -> String:
|
||
|
|
var value: Variant = definition.get(field)
|
||
|
|
return "" if value == null else str(value)
|
||
|
|
|
||
|
|
|
||
|
|
func _list(definition: Resource, field: String) -> Array:
|
||
|
|
var value: Variant = definition.get(field)
|
||
|
|
if value == null:
|
||
|
|
return []
|
||
|
|
return Array(value.keys() if value is Dictionary else value)
|
||
|
|
|
||
|
|
|
||
|
|
func _check_vessels() -> void:
|
||
|
|
for definition in VesselDefinitions.all():
|
||
|
|
var id: String = String(definition.definition_id)
|
||
|
|
# --- A. whole ---------------------------------------------------
|
||
|
|
if String(definition.display_name).strip_edges() == "":
|
||
|
|
_fail("vessel %s has no display name" % id)
|
||
|
|
if definition.base_stats.is_empty():
|
||
|
|
_fail("vessel %s has no stats at all" % id)
|
||
|
|
# Only the hulls that actually join the fleet: a repair drone is a
|
||
|
|
# logistics unit driven by GameState, with nothing bolted to it.
|
||
|
|
if definition.fleet_eligible and definition.component_scenes.is_empty():
|
||
|
|
_fail("vessel %s flies in the fleet with no components - an empty hull" % id)
|
||
|
|
if definition.hull_points.is_empty():
|
||
|
|
_fail("vessel %s has no hull points - it would draw as the fallback triangle" % id)
|
||
|
|
# --- B. cargo --------------------------------------------------
|
||
|
|
var carries: bool = float(definition.base_stats.get("cargo_capacity", 0.0)) > 0.0
|
||
|
|
var claims: bool = "cargo" in definition.capabilities
|
||
|
|
if carries != claims:
|
||
|
|
_fail("vessel %s: cargo_capacity %s but capabilities %s - a hold nothing can fill, or the reverse" \
|
||
|
|
% [id, definition.base_stats.get("cargo_capacity", 0.0), definition.capabilities])
|
||
|
|
if "weapon" in definition.capabilities \
|
||
|
|
and float(definition.base_stats.get("fire_range", 0.0)) <= 0.0:
|
||
|
|
_fail("vessel %s claims a weapon and has no range" % id)
|
||
|
|
|
||
|
|
|
||
|
|
func _check_buildings() -> void:
|
||
|
|
for definition in BuildingDefinitions.all():
|
||
|
|
var id: String = String(definition.definition_id)
|
||
|
|
# --- C. whole --------------------------------------------------
|
||
|
|
if String(definition.display_name).strip_edges() == "":
|
||
|
|
_fail("building %s has no display name" % id)
|
||
|
|
if String(definition.description).strip_edges() == "":
|
||
|
|
_fail("building %s has no description - its info popup would be blank" % id)
|
||
|
|
if int(definition.cost) <= 0:
|
||
|
|
_fail("building %s is free" % id)
|
||
|
|
if int(definition.max_hp) <= 0:
|
||
|
|
_fail("building %s has no hit points" % id)
|
||
|
|
# Space-placed structures only: an extractor is a marker on a planet
|
||
|
|
# rather than a thing standing in the world, and has no scene.
|
||
|
|
if definition.placement_mode == "space" and definition.visual_scene == null:
|
||
|
|
_fail("building %s stands in space with nothing to draw" % id)
|
||
|
|
for field in ["menu_button_name", "menu_label_name", "menu_cost_name"]:
|
||
|
|
if str(definition.get(field)).strip_edges() == "":
|
||
|
|
_fail("building %s has no %s - the build menu could not place it" % [id, field])
|
||
|
|
if definition.capabilities.is_empty():
|
||
|
|
_fail("building %s declares no capabilities - it would do nothing at all" % id)
|
||
|
|
# --- D. converters ---------------------------------------------
|
||
|
|
# These live on the LOGISTICS profile, a sub-resource of the
|
||
|
|
# definition, not on the definition itself. Asking the definition
|
||
|
|
# for them answers null and the whole check quietly never fires -
|
||
|
|
# which is how the first draft of this test passed while the gas
|
||
|
|
# burner sat there with its collected_outputs missing.
|
||
|
|
var logistics: Resource = definition.logistics
|
||
|
|
if logistics != null and _text(logistics, "conversion_input") != "":
|
||
|
|
if _text(logistics, "conversion_output") == "":
|
||
|
|
_fail("building %s converts something into nothing" % id)
|
||
|
|
if _list(logistics, "collected_outputs").is_empty():
|
||
|
|
_fail("building %s converts but lists no collected outputs - what it makes would never be fetched" % id)
|
||
|
|
if _list(logistics, "input_buffers").is_empty():
|
||
|
|
_fail("building %s converts with no input buffer" % id)
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
_check_vessels()
|
||
|
|
_check_buildings()
|
||
|
|
if not failures.is_empty():
|
||
|
|
for message in failures:
|
||
|
|
print(" - %s" % message)
|
||
|
|
print("TEST_DEFINITIONS_INTACT: FAIL (%d definition(s) came back incomplete)" % failures.size())
|
||
|
|
quit(1)
|
||
|
|
return
|
||
|
|
print("%d vessels and %d buildings, all intact" \
|
||
|
|
% [VesselDefinitions.all().size(), BuildingDefinitions.all().size()])
|
||
|
|
print("TEST_DEFINITIONS_INTACT: PASS")
|
||
|
|
quit(0)
|