diff --git a/CaptureLabel.gd b/CaptureLabel.gd index 1c6bd07..c52f8fb 100644 --- a/CaptureLabel.gd +++ b/CaptureLabel.gd @@ -1,7 +1,7 @@ extends Label -# Polls GameState directly, same convention as ResourceCounter/GasCounter, -# so it stays correct without anyone having to push updates into it. Hidden +# Polls GameState directly, same convention as ResourceReadout, so it stays +# correct without anyone having to push updates into it. Hidden # entirely when the current planet has no capture points (e.g. no setup # resolved for it) rather than showing "0/0". diff --git a/CargoCounter.gd b/CargoCounter.gd deleted file mode 100644 index bf060f7..0000000 --- a/CargoCounter.gd +++ /dev/null @@ -1,33 +0,0 @@ -extends NeonNumber - -# What the fleet is CARRYING, shown under the banked total (#15, part 2 of -# #12). Since #14 ore is cargo until the fleet delivers it to the mothership, -# so without this, mining visibly does nothing - the banked figure above does -# not move until the fleet flies home, which reads as gathering being broken. -# -# Three things separate it from the banked readout, because colour alone is a -# weak signal on a phone screen in a game that is mostly teal: -# - amber rather than ore-teal, the same amber used elsewhere for ore in -# transit (the extractor derrick, the hauler route) -# - smaller, sitting UNDER the banked figure: subordinate to it, because -# this is the number that is not yours yet -# - present only while actually carrying something, so an empty hold is not -# a permanent zero taking up space next to the real total -# -# Reads GameState directly for the same reason ResourceCounter does: it stays -# correct across the space <-> planet-surface scene swap with nobody having -# to push updates into it. - -const CARGO_GLOW: Color = Color(1.0, 0.78, 0.25) -const CARGO_CORE: Color = Color(0.22, 0.12, 0.0) - - -func _ready() -> void: - glow_color = CARGO_GLOW - core_color = CARGO_CORE - - -func _process(_delta: float) -> void: - visible = GameState.fleet_cargo > 0 - if visible: - set_amount(GameState.fleet_cargo) diff --git a/CargoCounter.gd.uid b/CargoCounter.gd.uid deleted file mode 100644 index dfef251..0000000 --- a/CargoCounter.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bmrcja23g5oi6 diff --git a/GasCargoCounter.gd b/GasCargoCounter.gd deleted file mode 100644 index ffa8cf4..0000000 --- a/GasCargoCounter.gd +++ /dev/null @@ -1,10 +0,0 @@ -extends "res://GasCounter.gd" - -# Fleet-side companion to GasCounter. It keeps the gas bubble glyph instead -# of recolouring ore, and occupies the same row as banked gas in the HUD. - - -func _process(_delta: float) -> void: - visible = GameState.fleet_gas_cargo > 0 - if visible: - set_amount(GameState.fleet_gas_cargo) diff --git a/GasCargoCounter.gd.uid b/GasCargoCounter.gd.uid deleted file mode 100644 index a5a329c..0000000 --- a/GasCargoCounter.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dn5wt1hyaui81 diff --git a/GasCounter.gd b/GasCounter.gd deleted file mode 100644 index 177ccae..0000000 --- a/GasCounter.gd +++ /dev/null @@ -1,35 +0,0 @@ -extends NeonNumber - -# Banked gas companion to the ore counter. Keeping it as the same neon -# numeral language makes both resources equally legible without implying gas -# can pay for ore-only construction. -const GAS_GLOW: Color = Color(0.85, 0.52, 1.0) -const GAS_CORE: Color = Color(0.2, 0.04, 0.28) - - -func _ready() -> void: - glow_color = GAS_GLOW - core_color = GAS_CORE - - -func _process(_delta: float) -> void: - set_amount(GameState.gas) - - -func _draw() -> void: - # Gas keeps the segmented count used by ore, but replaces ore's mining - # hex with the three-bubble resource glyph. The label above it now names - # the same icon rather than describing a pink recolour of ore. - var h: float = digit_height - var s: float = h / 60.0 - for bubble in [[Vector2(21, 36), 13.0], [Vector2(35, 42), 17.0], [Vector2(39, 23), 9.0]]: - var ring := PackedVector2Array() - for i in range(12): - var angle := TAU * float(i) / 12.0 - ring.append((bubble[0] + Vector2(cos(angle), sin(angle)) * bubble[1]) * s) - NeonDraw.draw_glowing_polygon(self, ring, GAS_GLOW, 1.0, GAS_CORE, 0.55 * s) - var x: float = h * 1.10 - var w: float = h * 0.56 - for digit in str(maxi(amount, 0)): - _draw_digit(int(digit), Vector2(x, 0.0), w, h) - x += w + digit_gap diff --git a/GasCounter.gd.uid b/GasCounter.gd.uid deleted file mode 100644 index 4f2dca2..0000000 --- a/GasCounter.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://wykjjb3qc01h diff --git a/Main.tscn b/Main.tscn index df66353..cfa2e8f 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=30 format=3] +[gd_scene load_steps=27 format=3] [ext_resource type="Script" path="res://Starfield.gd" id="1"] [ext_resource type="Script" path="res://Fleet.gd" id="2"] @@ -15,18 +15,15 @@ [ext_resource type="Script" path="res://TouchJoystick.gd" id="13"] [ext_resource type="Script" path="res://BuildingField.gd" id="14"] [ext_resource type="Script" path="res://BuildController.gd" id="15"] -[ext_resource type="Script" path="res://ResourceCounter.gd" id="16"] +[ext_resource type="Script" path="res://ResourceReadout.gd" id="16"] [ext_resource type="Script" path="res://FormationEditor.gd" id="17"] [ext_resource type="Script" path="res://NeonNumber.gd" id="19"] [ext_resource type="Script" path="res://NeonPanel.gd" id="20"] [ext_resource type="Script" path="res://SkillTreeView.gd" id="21"] -[ext_resource type="Script" path="res://CargoCounter.gd" id="22"] [ext_resource type="Script" path="res://TransporterField.gd" id="23"] [ext_resource type="Script" path="res://ResourceBar.gd" id="24"] -[ext_resource type="Script" path="res://GasCounter.gd" id="25"] [ext_resource type="Script" path="res://VesselDeliveryField.gd" id="26"] [ext_resource type="Script" path="res://ResourceTransferList.gd" id="27"] -[ext_resource type="Script" path="res://GasCargoCounter.gd" id="28"] [ext_resource type="Script" path="res://GasBombController.gd" id="29"] [sub_resource type="ShaderMaterial" id="1"] @@ -93,16 +90,16 @@ script = ExtResource("12") [node name="UI" type="CanvasLayer" parent="."] layer = 1 -[node name="ResourceCounter" type="Control" parent="UI"] +[node name="ResourceReadout" type="Control" parent="UI"] mouse_filter = 2 offset_left = 28 offset_top = 38 -offset_right = 320 -offset_bottom = 82 +offset_right = 696 +offset_bottom = 154 script = ExtResource("16") digit_height = 38.0 -[node name="OreCaption" type="Label" parent="UI"] +[node name="BankCaption" type="Label" parent="UI"] mouse_filter = 2 offset_left = 28 offset_top = 6 @@ -112,16 +109,7 @@ theme_override_font_sizes/font_size = 18 theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1) text = "BANKED" -[node name="GasCounter" type="Control" parent="UI"] -mouse_filter = 2 -offset_left = 28 -offset_top = 96 -offset_right = 320 -offset_bottom = 140 -script = ExtResource("25") -digit_height = 38.0 - -[node name="GasCaption" type="Label" parent="UI"] +[node name="FleetCaption" type="Label" parent="UI"] mouse_filter = 2 offset_left = 356 offset_top = 6 @@ -131,24 +119,6 @@ theme_override_font_sizes/font_size = 18 theme_override_colors/font_color = Color(1, 0.78, 0.25, 1) text = "FLEET HOLD" -[node name="CargoCounter" type="Control" parent="UI"] -mouse_filter = 2 -offset_left = 356 -offset_top = 38 -offset_right = 696 -offset_bottom = 82 -script = ExtResource("22") -digit_height = 38.0 - -[node name="GasCargoCounter" type="Control" parent="UI"] -mouse_filter = 2 -offset_left = 356 -offset_top = 96 -offset_right = 696 -offset_bottom = 140 -script = ExtResource("28") -digit_height = 38.0 - [node name="PlanetNameLabel" type="Label" parent="UI"] visible = false offset_left = 24 diff --git a/NeonNumber.gd b/NeonNumber.gd index 162261a..660d8db 100644 --- a/NeonNumber.gd +++ b/NeonNumber.gd @@ -6,7 +6,7 @@ extends Control # no place in this look, which is why the digits are strokes rather than # text. # -# Used both for the live resource readout (see ResourceCounter, which feeds +# Used both for the live resource readout (see ResourceReadout, which feeds # it from GameState) and for the fixed prices under each build option. @export var amount: int = 0: set = set_amount diff --git a/PlanetSurface.tscn b/PlanetSurface.tscn index 82626a5..3b8bd19 100644 --- a/PlanetSurface.tscn +++ b/PlanetSurface.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=17 format=3] +[gd_scene load_steps=14 format=3] [ext_resource type="Script" path="res://GameController.gd" id="1"] [ext_resource type="Script" path="res://LunarBackground.gd" id="2"] @@ -7,10 +7,7 @@ [ext_resource type="Script" path="res://PlanetSurfaceController.gd" id="5"] [ext_resource type="Script" path="res://IconButton.gd" id="6"] [ext_resource type="Script" path="res://TouchJoystick.gd" id="7"] -[ext_resource type="Script" path="res://ResourceCounter.gd" id="9"] -[ext_resource type="Script" path="res://GasCounter.gd" id="11"] -[ext_resource type="Script" path="res://CargoCounter.gd" id="12"] -[ext_resource type="Script" path="res://GasCargoCounter.gd" id="13"] +[ext_resource type="Script" path="res://ResourceReadout.gd" id="9"] [ext_resource type="Script" path="res://GasBombController.gd" id="14"] [ext_resource type="Script" path="res://CaptureLabel.gd" id="15"] @@ -60,16 +57,15 @@ script = ExtResource("5") [node name="UI" type="CanvasLayer" parent="."] layer = 1 -[node name="ResourceCounter" type="Control" parent="UI"] +[node name="ResourceReadout" type="Control" parent="UI"] mouse_filter = 2 offset_left = 28 offset_top = 38 -offset_right = 320 -offset_bottom = 82 +offset_right = 696 +offset_bottom = 154 script = ExtResource("9") digit_height = 38.0 -glow_color = Color(0, 0.42, 0.48, 1) -core_color = Color(0, 0.08, 0.1, 1) +bright_background = true [node name="BankCaption" type="Label" parent="UI"] mouse_filter = 2 @@ -83,15 +79,6 @@ theme_override_colors/font_outline_color = Color(0.9, 0.95, 0.96, 0.8) theme_override_constants/outline_size = 4 text = "BANKED" -[node name="GasCounter" type="Control" parent="UI"] -mouse_filter = 2 -offset_left = 28 -offset_top = 96 -offset_right = 320 -offset_bottom = 140 -script = ExtResource("11") -digit_height = 38.0 - [node name="FleetCaption" type="Label" parent="UI"] mouse_filter = 2 offset_left = 356 @@ -104,24 +91,6 @@ theme_override_colors/font_outline_color = Color(0.96, 0.93, 0.82, 0.85) theme_override_constants/outline_size = 4 text = "FLEET HOLD" -[node name="CargoCounter" type="Control" parent="UI"] -mouse_filter = 2 -offset_left = 356 -offset_top = 38 -offset_right = 696 -offset_bottom = 82 -script = ExtResource("12") -digit_height = 38.0 - -[node name="GasCargoCounter" type="Control" parent="UI"] -mouse_filter = 2 -offset_left = 356 -offset_top = 96 -offset_right = 696 -offset_bottom = 140 -script = ExtResource("13") -digit_height = 38.0 - [node name="PlanetNameLabel" type="Label" parent="UI"] offset_left = 24 offset_top = 156 diff --git a/ResourceCounter.gd b/ResourceCounter.gd deleted file mode 100644 index 23a8d54..0000000 --- a/ResourceCounter.gd +++ /dev/null @@ -1,10 +0,0 @@ -extends NeonNumber - -# Top-of-screen ore readout. All the drawing lives in NeonNumber; this only -# keeps the displayed amount in step with GameState, which it reads directly -# so it stays correct across the space <-> planet-surface scene swap without -# anyone having to push updates into it. - - -func _process(_delta: float) -> void: - set_amount(GameState.rocks) diff --git a/ResourceCounter.gd.uid b/ResourceCounter.gd.uid deleted file mode 100644 index ed6edb5..0000000 --- a/ResourceCounter.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://5u8a3kx3gsh1 diff --git a/ResourceReadout.gd b/ResourceReadout.gd new file mode 100644 index 0000000..cec47fb --- /dev/null +++ b/ResourceReadout.gd @@ -0,0 +1,95 @@ +class_name ResourceReadout +extends Control + +# Generalizes ResourceCounter/GasCounter/CargoCounter/GasCargoCounter - one +# hardcoded node plus one script per resource, times two scenes - into one +# row per GameState.resource_types() entry, built at _ready(). A new +# resource added to ResourceDefinitions.ORDER/DEFINITIONS gets a HUD row +# with no scene edit and no new script, the same way ResourceBar and +# ResourceTransferList already pick up a new resource automatically. +# +# Layout mirrors what it replaces: a BANKED column (left) and a FLEET HOLD +# column (right), one row per resource, each row a resource glyph plus a +# digit readout. Banked rows are always visible, even at zero (matching +# ResourceCounter/GasCounter today); fleet rows hide at zero, since that +# hold isn't the player's yet (see CargoCounter's own note on why). + +const FLEET_GLOW: Color = Color(1.0, 0.78, 0.25) +const FLEET_CORE: Color = Color(0.22, 0.12, 0.0) + +const ROW_HEIGHT: float = 58.0 +const COLUMN_GAP: float = 328.0 +const GLYPH_GAP: float = 8.0 + +@export var digit_height: float = 38.0 +# The bright rocky/gas surfaces need a darker banked glow than deep space +# for the same contrast reason PlanetSurface.tscn's old ResourceCounter +# node overrode its colour - set true on the surface-scene instance only. +@export var bright_background: bool = false + +var _banked_numbers: Dictionary = {} +var _fleet_numbers: Dictionary = {} + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + var y: float = 0.0 + for resource_type in GameState.resource_types(): + var banked_color: Color = GameState.resource_color(resource_type) + if bright_background: + banked_color = banked_color.darkened(0.4) + + _add_glyph(resource_type, Vector2(0.0, y)) + var banked: NeonNumber = _add_number(Vector2(digit_height + GLYPH_GAP, y), + banked_color, banked_color.darkened(0.8)) + _banked_numbers[resource_type] = banked + + _add_glyph(resource_type, Vector2(COLUMN_GAP, y)) + var fleet: NeonNumber = _add_number(Vector2(COLUMN_GAP + digit_height + GLYPH_GAP, y), + FLEET_GLOW, FLEET_CORE) + _fleet_numbers[resource_type] = fleet + + y += ROW_HEIGHT + + +func _add_glyph(resource_type: String, at: Vector2) -> void: + var glyph: ResourceGlyph = ResourceGlyph.new() + glyph.resource_type = resource_type + glyph.position = at + glyph.size = Vector2(digit_height, digit_height) + add_child(glyph) + + +func _add_number(at: Vector2, glow: Color, core: Color) -> NeonNumber: + var number: NeonNumber = NeonNumber.new() + number.show_icon = false + number.position = at + number.size = Vector2(0.0, digit_height) + number.glow_color = glow + number.core_color = core + number.digit_height = digit_height + add_child(number) + return number + + +func banked_number(resource_type: String) -> NeonNumber: + return _banked_numbers.get(resource_type) + + +func fleet_number(resource_type: String) -> NeonNumber: + return _fleet_numbers.get(resource_type) + + +func _process(_delta: float) -> void: + for resource_type in GameState.resource_types(): + var banked: NeonNumber = _banked_numbers.get(resource_type) + if banked != null: + banked.set_amount(GameState.banked_resource(resource_type)) + + var fleet: NeonNumber = _fleet_numbers.get(resource_type) + if fleet == null: + continue + var held: int = GameState.fleet_resource(resource_type) + fleet.visible = held > 0 + if fleet.visible: + fleet.set_amount(held) diff --git a/tests/_resource_readout_surface_preview.gd b/tests/_resource_readout_surface_preview.gd new file mode 100644 index 0000000..dd7d685 --- /dev/null +++ b/tests/_resource_readout_surface_preview.gd @@ -0,0 +1,17 @@ +extends Node2D + +# Preview, not a test. Populates both banked and fleet-held resources so +# PlanetSurface.tscn's bright_background ResourceReadout row can be checked +# for contrast against the lunar surface. +# +# bash tests/screenshot.sh res://tests/_resource_readout_surface_preview.tscn /tmp/hud.png 20 720x1280 + + +func _ready() -> void: + GameState.buildings.clear() + GameState.extractors.clear() + GameState.rocks = 128 + GameState.gas = 46 + GameState.fleet_cargo = 34 + GameState.fleet_gas_cargo = 17 + add_child(load("res://PlanetSurface.tscn").instantiate()) diff --git a/tests/_resource_readout_surface_preview.tscn b/tests/_resource_readout_surface_preview.tscn new file mode 100644 index 0000000..44e8a2b --- /dev/null +++ b/tests/_resource_readout_surface_preview.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/_resource_readout_surface_preview.gd" id="1"] + +[node name="ResourceReadoutSurfacePreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/test_cargo_readout.gd b/tests/test_cargo_readout.gd index 02bae96..e24f2cc 100644 --- a/tests/test_cargo_readout.gd +++ b/tests/test_cargo_readout.gd @@ -44,8 +44,6 @@ func _initialize() -> void: mothership_id = game_state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) scene = load("res://Main.tscn").instantiate() root.add_child(scene) - banked = scene.get_node("UI/ResourceCounter") - cargo = scene.get_node("UI/CargoCounter") func _process(delta: float) -> bool: @@ -54,6 +52,14 @@ func _process(delta: float) -> bool: 0: if elapsed < 0.2: return false + # ResourceReadout builds its rows in _ready(), which for a -s + # entry-point script does not fire synchronously inside + # _initialize()'s add_child() - it is deferred to the first + # frame. Looking these up here, after the elapsed wait above, + # instead of right after add_child(), is what makes them non-null. + var readout: Node = scene.get_node("UI/ResourceReadout") + banked = readout.banked_number(GameState.RESOURCE_ORE) + cargo = readout.fleet_number(GameState.RESOURCE_ORE) # --- 3. nothing carried -> no cargo readout ----------------- if cargo.visible: print("TEST_CARGO_READOUT: FAIL (cargo readout showing with an empty hold)") diff --git a/tests/test_resource_readout.gd b/tests/test_resource_readout.gd new file mode 100644 index 0000000..121995d --- /dev/null +++ b/tests/test_resource_readout.gd @@ -0,0 +1,124 @@ +extends SceneTree + +# Headless gameplay test: run with +# godot --headless -s res://tests/test_resource_readout.gd +# Verifies ResourceReadout.gd (#49) generalizes ResourceCounter/GasCounter/ +# CargoCounter/GasCargoCounter into one row per GameState.resource_types() +# entry, in both Main.tscn and PlanetSurface.tscn, with no per-resource +# code: +# 1. exactly one banked + one fleet NeonNumber per current resource type, +# each correctly reading its own resource's amount +# 2. fleet rows hide at zero and show once carried, banked rows always show +# 3. banked and fleet rows are visually distinct (colour, position) +# 4. the script itself contains no hardcoded resource id - the row count +# and mapping come entirely from GameState.resource_types() +# 5. PlanetSurface.tscn's instance renders too, with its own bright- +# background contrast +# +# A -s entry-point script can only reach the GameState autoload's CONSTANTS +# through the bare "GameState" identifier (GameState.RESOURCE_ORE etc.) - +# calling any of its METHODS that way fails to compile with "Identifier not +# found: GameState", even from _initialize(). Every other test in this suite +# already routes method calls through the actual instance +# (root.get_node("GameState")) instead, and this file follows the same rule. + +var game_state: Node +var main: Node +var readout: Node +var stage: int = 0 +var elapsed: float = 0.0 + + +func _initialize() -> void: + game_state = root.get_node("GameState") + game_state.bank_inventory.set_amount(GameState.RESOURCE_ORE, 11) + game_state.bank_inventory.set_amount(GameState.RESOURCE_GAS, 4) + game_state.fleet_inventory.set_amount(GameState.RESOURCE_ORE, 6) + # Gas left at 0 in the fleet hold deliberately, to check the hide-at-zero + # rule holds per-resource, not just per-readout. + + # --- 4. no hardcoded resource id in the generalized script --- + var source: String = FileAccess.get_file_as_string("res://ResourceReadout.gd") + for literal in ['"ore"', '"gas"', 'RESOURCE_ORE', 'RESOURCE_GAS']: + if literal in source: + return _fail("ResourceReadout.gd hardcodes %s - it must read GameState.resource_types() only" % literal) + + main = load("res://Main.tscn").instantiate() + root.add_child(main) + readout = main.get_node("UI/ResourceReadout") + + +func _process(delta: float) -> bool: + elapsed += delta + match stage: + 0: + if elapsed < 0.2: + return false + + # --- 1. one row per resource type, correctly mapped --- + var types: PackedStringArray = game_state.resource_types() + for resource_type in types: + var banked: Node = readout.banked_number(resource_type) + var fleet: Node = readout.fleet_number(resource_type) + if banked == null or fleet == null: + return _fail("no readout row for resource '%s'" % resource_type) + if banked.amount != game_state.banked_resource(resource_type): + return _fail("banked row for '%s' shows %d, bank has %d" % [ + resource_type, banked.amount, game_state.banked_resource(resource_type) + ]) + + # --- 2. hide-at-zero is per resource --- + var ore_fleet: Node = readout.fleet_number(GameState.RESOURCE_ORE) + var gas_fleet: Node = readout.fleet_number(GameState.RESOURCE_GAS) + if not ore_fleet.visible: + return _fail("fleet ore is 6 but its row is hidden") + if gas_fleet.visible: + return _fail("fleet gas is 0 but its row is showing") + if ore_fleet.amount != 6: + return _fail("fleet ore row shows %d, hold has 6" % ore_fleet.amount) + + # --- 3. banked and fleet are visually distinct --- + var ore_banked: Node = readout.banked_number(GameState.RESOURCE_ORE) + if ore_banked.glow_color == ore_fleet.glow_color: + return _fail("banked and fleet rows share a colour") + if ore_banked.position == ore_fleet.position: + return _fail("banked and fleet rows share a position") + + # Depositing gas into the fleet hold should reveal its row live. + game_state.fleet_inventory.deposit(GameState.RESOURCE_GAS, 3) + elapsed = 0.0 + stage = 1 + return false + 1: + if elapsed < 0.2: + return false + var gas_fleet: Node = readout.fleet_number(GameState.RESOURCE_GAS) + if not gas_fleet.visible or gas_fleet.amount != 3: + return _fail("fleet gas row did not update live after a deposit (visible=%s amount=%d)" % [ + gas_fleet.visible, gas_fleet.amount + ]) + + # --- 5. the surface scene's instance also works, bright-background --- + main.free() + var surface: Node = load("res://PlanetSurface.tscn").instantiate() + root.add_child(surface) + var surface_readout: Node = surface.get_node("UI/ResourceReadout") + if not surface_readout.bright_background: + surface.free() + return _fail("PlanetSurface's ResourceReadout instance is not set to bright_background") + elapsed = 0.0 + stage = 2 + return false + 2: + if elapsed < 0.2: + return false + print("TEST_RESOURCE_READOUT: PASS") + quit(0) + return true + return false + + +func _fail(message: String) -> bool: + print("TEST_RESOURCE_READOUT: FAIL (%s)" % message) + quit(1) + return true