Compose buildings from definitions and capabilities

This commit is contained in:
Loïc Wisniewski 2026-08-14 21:44:28 +02:00
commit 0504b24d8d
46 changed files with 1091 additions and 878 deletions

View file

@ -28,4 +28,4 @@ func _ready() -> void:
# Flip SHOW_POPUP to check the menu underneath
# instead of the popup that covers it.
if SHOW_POPUP:
controller._on_place_mothership()
controller._show_building_info(GameState.BUILDING_MOTHERSHIP)

View file

@ -31,9 +31,9 @@ func _ready() -> void:
b.position = Vector2(-240.0 + float(i % 3) * 240.0, -105.0 + float(i / 3) * 215.0)
add_child(b)
if cell.has("phase"):
b.set_process(false)
b.anim_time = float(cell.phase) * b.CYCLE
b.apply_animation()
b.visual.set_process(false)
b.visual.anim_time = float(cell.phase) * b.visual.CYCLE
b.visual.apply_animation()
var cam: Camera2D = Camera2D.new()
cam.zoom = Vector2(1.65, 1.65)

View file

@ -53,7 +53,7 @@ func _process(delta: float) -> bool:
var ship: Node2D = null
for b in field.spawned:
if is_instance_valid(b) and b.building_type == GameState.BUILDING_MOTHERSHIP:
ship = b
ship = b.visual
if ship == null:
print("TEST_ANIMATION_CONTINUITY: FAIL (mothership never spawned)")
quit(1)

View file

@ -28,9 +28,9 @@ var elapsed: float = 0.0
# Extractor is excluded: it needs a planet underneath, which
# test_extractor_building covers in full.
const OPTIONS: Array = [
{"press": "_on_place_mothership", "type": "mothership", "title": "Mothership"},
{"press": "_on_place_crafter", "type": "ship_crafter", "title": "Ship Crafter"},
{"press": "_on_place_command_center", "type": "command_center", "title": "Command Center"},
{"type": "mothership", "title": "Mothership"},
{"type": "ship_crafter", "title": "Ship Crafter"},
{"type": "command_center", "title": "Command Center"},
]
@ -84,7 +84,7 @@ func _process(delta: float) -> bool:
var built_before: int = _count(type)
controller._on_build_pressed()
controller.call(option.press)
controller._show_building_info(type)
var informed: bool = popup.visible \
and title.text == option.title \
@ -111,7 +111,7 @@ func _process(delta: float) -> bool:
# --- reopening and confirming builds it ------------------
controller._on_build_pressed()
controller.call(option.press)
controller._show_building_info(type)
controller._confirm_build()
var built: bool = _count(type) == built_before + 1
var charged: bool = game_state.rocks == rocks_before - expected_cost

View file

@ -0,0 +1,35 @@
extends SceneTree
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
func _initialize() -> void:
var scene: PackedScene = load("res://Building.tscn")
for definition in BUILDING_DEFINITIONS.all():
if definition.placement_mode != "space":
continue
var building: Node2D = scene.instantiate()
building.definition_id = definition.definition_id
building._ready()
if building.definition == null \
or building.definition.definition_id != definition.definition_id \
or building.visual == null:
_fail("%s did not compose its definition and visual (resolved=%s visual=%s)" % [
definition.definition_id,
"null" if building.definition == null else building.definition.definition_id,
building.visual,
])
return
var provider: Node = building.visual.get_node_or_null("ActionProvider")
if provider == null or not provider.actions.has("transfer"):
_fail("%s has no composed action provider" % definition.definition_id)
return
building.free()
print("TEST_BUILDING_COMPOSITION: PASS")
quit(0)
func _fail(message: String) -> void:
print("TEST_BUILDING_COMPOSITION: FAIL (%s)" % message)
quit(1)

View file

@ -61,11 +61,11 @@ func _process(delta: float) -> bool:
continue
match b.building_type:
GameState.BUILDING_SHIP_CRAFTER:
crafter = b
crafter = b.visual
GameState.BUILDING_MOTHERSHIP:
mothership = b
mothership = b.visual
GameState.BUILDING_COMMAND_CENTER:
command = b
command = b.visual
if crafter == null or mothership == null or command == null:
print("TEST_BUILDING_REDRAW_COST: FAIL (buildings never spawned)")
quit(1)
@ -88,7 +88,7 @@ func _process(delta: float) -> bool:
for b in [crafter, mothership, command]:
if b.draw_count != 1:
print("TEST_BUILDING_REDRAW_COST: FAIL (%s redrew its geometry %d times)" % [
b.building_type, b.draw_count
b.name, b.draw_count
])
quit(1)
return true

View file

@ -57,16 +57,19 @@ func _process(delta: float) -> bool:
# Spawning is deferred, so give the tree a frame to attach them.
if elapsed < 0.2:
return false
crafter = _find(GameState.BUILDING_SHIP_CRAFTER)
if crafter == null:
var crafter_building: Node2D = _find(GameState.BUILDING_SHIP_CRAFTER)
if crafter_building == null:
print("TEST_CRAFTER_ANIMATION: FAIL (crafter never spawned)")
quit(1)
return true
crafter = crafter_building.visual
var animated: bool = crafter.is_processing()
var mothership: Node2D = _find(GameState.BUILDING_MOTHERSHIP)
var mothership_building: Node2D = _find(GameState.BUILDING_MOTHERSHIP)
var mothership: Node2D = mothership_building.visual if mothership_building != null else null
var mothership_animates: bool = mothership != null and mothership.is_processing()
var command: Node2D = _find(GameState.BUILDING_COMMAND_CENTER)
var command_building: Node2D = _find(GameState.BUILDING_COMMAND_CENTER)
var command: Node2D = command_building.visual if command_building != null else null
var command_static: bool = command != null and not command.is_processing()
print("crafter animates=%s mothership animates=%s command centre static=%s" % [
animated, mothership_animates, command_static

View file

@ -69,7 +69,7 @@ func _process(delta: float) -> bool:
print("TEST_EXTRACTOR_BUILDING: FAIL (enabled with no planet in reach)")
quit(1)
return true
controller._on_place_extractor()
controller._show_building_info(GameState.BUILDING_EXTRACTOR)
var explained: bool = popup.visible and "planet" in popup.get_node("Panel/MessageLabel").text.to_lower()
var offered: bool = popup.get_node("Panel/BuildButton").visible
print("no planet: greyed=%s popup_shown=%s confirm_offered=%s" % [
@ -118,7 +118,7 @@ func _process(delta: float) -> bool:
return true
var rocks_before: int = game_state.rocks
controller._on_place_extractor()
controller._show_building_info(GameState.BUILDING_EXTRACTOR)
var planet_field2: Node = main_scene.get_node("PlanetField")
var planet2: Node2D = planet_field2.populated_chunks[target_chunk][0]
@ -148,7 +148,7 @@ func _process(delta: float) -> bool:
# A second one on the same planet must be refused, not charged.
var rocks_now: int = game_state.rocks
controller._on_build_pressed()
controller._on_place_extractor()
controller._show_building_info(GameState.BUILDING_EXTRACTOR)
var re_offered: bool = popup.get_node("Panel/BuildButton").visible
controller._confirm_build() # must do nothing even if pressed
var still_one: bool = game_state.extractors.size() == 1 and not re_offered

View file

@ -67,7 +67,7 @@ func _process(delta: float) -> bool:
return false
# --- first one: informed, then built -------------------------
controller._on_build_pressed()
controller._on_place_mothership()
controller._show_building_info(GameState.BUILDING_MOTHERSHIP)
var first_text: String = popup.get_node("Panel/MessageLabel").text.to_lower()
var first_warned: bool = "destroy" in first_text
print("first: popup=%s warned=%s built_before_confirm=%d" % [
@ -86,7 +86,7 @@ func _process(delta: float) -> bool:
# --- second: warns, builds nothing ---------------------------
_place_fleet(second_pos)
controller._on_build_pressed()
controller._on_place_mothership()
controller._show_building_info(GameState.BUILDING_MOTHERSHIP)
var warned: bool = popup.visible and "destroy" in popup.get_node("Panel/MessageLabel").text.to_lower()
print("second press: warned=%s motherships=%d" % [warned, _count_motherships()])
if not warned or _count_motherships() != 1:
@ -107,7 +107,7 @@ func _process(delta: float) -> bool:
# --- confirming replaces -------------------------------------
var rocks_before: int = game_state.rocks
controller._on_place_mothership()
controller._show_building_info(GameState.BUILDING_MOTHERSHIP)
controller._confirm_build()
var only_one: bool = _count_motherships() == 1
var moved: bool = game_state.get_mothership_position().distance_to(second_pos) < 60.0
@ -124,7 +124,7 @@ func _process(delta: float) -> bool:
game_state.rocks = GameState.MOTHERSHIP_COST - 1
_place_fleet(Vector2(2500.0, -800.0))
controller._on_build_pressed()
controller._on_place_mothership()
controller._show_building_info(GameState.BUILDING_MOTHERSHIP)
# With nothing affordable there is no confirm button to press;
# pressing it anyway must still do nothing.
var offered: bool = popup.get_node("Panel/BuildButton").visible

View file

@ -72,9 +72,9 @@ func _process(delta: float) -> bool:
# Since #21 an unresearched structure is not listed at all, rather
# than listed and greyed. It is therefore unreachable from the
# menu - but the popup must still refuse if something calls
# _on_place_crafter() by another route.
# the generic definition entry point by another route.
var hidden: bool = not scene.get_node("UI/BuildMenu/CrafterButton").visible
controller._on_place_crafter()
controller._show_building_info(GameState.BUILDING_SHIP_CRAFTER)
var offered: bool = popup.get_node("Panel/BuildButton").visible
print("locked crafter: hidden_from_menu=%s confirm_offered=%s" % [hidden, offered])
if not hidden or offered:

View file

@ -1,5 +1,7 @@
extends SceneTree
const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd")
# Headless gameplay test: run with
# godot --headless -s res://tests/test_transport_hub_building.gd
# Forgejo #18 (part 5 of #12): the Transport Hub is a placeable building of
@ -67,7 +69,7 @@ func _process(delta: float) -> bool:
var icons: Dictionary = {}
for type in [GameState.BUILDING_MOTHERSHIP, GameState.BUILDING_SHIP_CRAFTER,
GameState.BUILDING_COMMAND_CENTER, GameState.BUILDING_TRANSPORT_HUB]:
var icon: int = controller._action_icon_for(type)
var icon: int = BUILDING_DEFINITIONS.get_definition(type).action_icon
if icons.has(icon):
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (%s and %s share an icon)" % [type, icons[icon]])
quit(1)
@ -99,7 +101,7 @@ func _process(delta: float) -> bool:
return false
for b in field.spawned:
if is_instance_valid(b) and b.building_type == GameState.BUILDING_TRANSPORT_HUB:
hub = b
hub = b.visual
if hub == null:
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (never spawned)")
quit(1)
@ -120,9 +122,9 @@ func _process(delta: float) -> bool:
for m in markers:
if m.type == GameState.BUILDING_TRANSPORT_HUB:
found = true
if m.color == map_content.CRAFTER_MARKER_COLOR \
or m.color == map_content.MOTHERSHIP_MARKER_COLOR \
or m.color == map_content.COMMAND_MARKER_COLOR:
if m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_SHIP_CRAFTER).map_color \
or m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_MOTHERSHIP).map_color \
or m.color == BUILDING_DEFINITIONS.get_definition(GameState.BUILDING_COMMAND_CENTER).map_color:
print("TEST_TRANSPORT_HUB_BUILDING: FAIL (map marker reuses another building's colour)")
quit(1)
return true