diff --git a/buildings/GasBurnerBuildingDefinition.tres b/buildings/GasBurnerBuildingDefinition.tres index 6cf34ef..59589aa 100644 --- a/buildings/GasBurnerBuildingDefinition.tres +++ b/buildings/GasBurnerBuildingDefinition.tres @@ -21,7 +21,10 @@ definition_id = "gas_burner" display_name = "Gas Burner" description = "Burns gas into energy at its own site. Hub transporters keep it stocked with gas and collect the energy it makes." cost = 40 -build_resource_costs = {"ore": 40, "gas": 22} +build_resource_costs = { +"gas": 22, +"ore": 40 +} action_icon = 33 menu_icon = 33 visual_scene = ExtResource("2") diff --git a/tests/test_definitions_intact.gd b/tests/test_definitions_intact.gd new file mode 100644 index 0000000..9adf52f --- /dev/null +++ b/tests/test_definitions_intact.gd @@ -0,0 +1,130 @@ +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) diff --git a/tests/test_definitions_intact.gd.uid b/tests/test_definitions_intact.gd.uid new file mode 100644 index 0000000..d7ebbe4 --- /dev/null +++ b/tests/test_definitions_intact.gd.uid @@ -0,0 +1 @@ +uid://d3ury2lcjwer7