diff --git a/surface/PlanetSurface.tscn b/surface/PlanetSurface.tscn index 304ae58..08a7998 100644 --- a/surface/PlanetSurface.tscn +++ b/surface/PlanetSurface.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=20 format=3] +[gd_scene load_steps=21 format=3] [ext_resource type="Script" path="res://controllers/GameController.gd" id="1"] [ext_resource type="PackedScene" path="res://surface/SurfaceBackground.tscn" id="3"] @@ -12,6 +12,7 @@ [ext_resource type="Script" path="res://controllers/GasBombController.gd" id="14"] [ext_resource type="Script" path="res://controllers/LaserController.gd" id="16"] [ext_resource type="Script" path="res://controllers/DashController.gd" id="dash_ctrl"] +[ext_resource type="Script" path="res://ui/AbilityButtonLayout.gd" id="ability_layout"] [ext_resource type="Script" path="res://ui/SpeedFader.gd" id="17"] [ext_resource type="Script" path="res://surface/CaptureLabel.gd" id="15"] [ext_resource type="Script" path="res://ui/NarrationBanner.gd" id="banner"] @@ -43,6 +44,9 @@ script = ExtResource("16") [node name="DashController" type="Node2D" parent="."] script = ExtResource("dash_ctrl") +[node name="AbilityButtonLayout" type="Node" parent="."] +script = ExtResource("ability_layout") + [node name="Camera2D" type="Camera2D" parent="."] position = Vector2(0, 0) zoom = Vector2(1, 1) diff --git a/tests/test_ability_button_layout.gd b/tests/test_ability_button_layout.gd new file mode 100644 index 0000000..b285881 --- /dev/null +++ b/tests/test_ability_button_layout.gd @@ -0,0 +1,90 @@ +extends SceneTree + +# The three aimed abilities share one responsive quarter-circle layout around +# the fire stick in both playable scenes. This exercises the real space HUD; +# PlanetSurface carries the same layout node and is checked structurally. + +var main: Node +var fleet: Node +var stage: int = 0 +var elapsed: float = 0.0 + + +func _initialize() -> void: + var state: Node = root.get_node("GameState") + state.reset_new_game() + state.intro_active = false + state.vessel_levels["unlock_laser"] = 1 + state.vessel_levels["unlock_gas_bomber"] = 1 + state.vessel_levels["unlock_distorder"] = 1 + state.fleet_inventory.total_capacity = -1 + state.deposit_fleet_resource(GameState.RESOURCE_ENERGY, 500) + state.deposit_fleet_resource(GameState.RESOURCE_GAS, 500) + main = load("res://world/Main.tscn").instantiate() + root.add_child(main) + current_scene = main + fleet = main.get_node("Fleet") + + +func _process(delta: float) -> bool: + elapsed += delta + if stage == 0 and elapsed >= 0.2: + main.get_node("EnemySpawner").set_process(false) + main.get_node("AsteroidField").set_process(false) + fleet.spawn_ships(3, Vector2.ZERO, PackedStringArray([ + GameState.VESSEL_DISTORDER, + GameState.VESSEL_GAS_BOMBER, + GameState.VESSEL_LASER, + ])) + _refresh_layout() + var layout: Node = main.get_node("AbilityButtonLayout") + var center: Vector2 = _fire_center() + var previous_angle: float = -INF + for path in ["UI/DashButton", "UI/GasBombButton", "UI/LaserButton"]: + var button: Control = main.get_node(path) + if not button.visible: + return _fail("%s is hidden in a fleet that provides every ability" % path) + var offset: Vector2 = button.global_position + button.size * 0.5 - center + if absf(offset.length() - layout.arc_radius) > 1.0 \ + or offset.x > 0.1 or offset.y > 0.1: + return _fail("%s is not on the fire stick's upper-left quarter circle" % path) + var angle: float = offset.angle() + if angle < 0.0: + angle += TAU + if previous_angle != -INF and angle <= previous_angle: + return _fail("ability buttons do not follow the quarter-circle order") + previous_angle = angle + + fleet.spawn_ships(1, Vector2.ZERO, PackedStringArray([GameState.VESSEL_LASER])) + _refresh_layout() + var laser: Control = main.get_node("UI/LaserButton") + var single_offset: Vector2 = laser.global_position + laser.size * 0.5 - _fire_center() + if absf(single_offset.x) > 1.0 or single_offset.y >= 0.0: + return _fail("a single ability does not sit directly above the fire stick") + var surface: Node = load("res://surface/PlanetSurface.tscn").instantiate() + if surface.get_node_or_null("AbilityButtonLayout") == null: + return _fail("planet surface is missing the shared ability layout") + surface.free() + print("abilities fan through the fire stick's upper-left quarter circle") + print("TEST_ABILITY_BUTTON_LAYOUT: PASS") + quit(0) + return true + return false + + +func _refresh_layout() -> void: + main.get_node("DashController")._process(0.0) + main.get_node("GasBombController")._process(0.0) + main.get_node("LaserController")._process(0.0) + main.get_node("AbilityButtonLayout")._process(0.0) + + +func _fire_center() -> Vector2: + var stick: Control = main.get_node("UI/FireStick") + return stick.global_position + stick.size * 0.5 + + +func _fail(message: String) -> bool: + print("TEST_ABILITY_BUTTON_LAYOUT: FAIL (%s)" % message) + quit(1) + return true diff --git a/tests/test_laser_vessel.gd b/tests/test_laser_vessel.gd index df23551..3177010 100644 --- a/tests/test_laser_vessel.gd +++ b/tests/test_laser_vessel.gd @@ -97,6 +97,8 @@ func _process(delta: float) -> bool: first_shooter = skill._next_ready_laser_ship() if first_shooter == null: return _fail_now("no laser ship is ready right after spawning") + if absf(first_shooter.get_component("laser").beam_range - 2900.0) > 0.1: + return _fail_now("laser ship beam range was not doubled to 2900") var press := InputEventScreenTouch.new() press.index = 7 @@ -131,7 +133,7 @@ func _process(delta: float) -> bool: off_line_enemy.speed = 0.0 beyond_range_enemy = load("res://enemies/Enemy.tscn").instantiate() - beyond_range_enemy.position = first_shooter.global_position + Vector2(2000.0, 0.0) + beyond_range_enemy.position = first_shooter.global_position + Vector2(3400.0, 0.0) main.add_child(beyond_range_enemy) beyond_range_enemy.speed = 0.0 diff --git a/ui/AbilityButtonLayout.gd b/ui/AbilityButtonLayout.gd new file mode 100644 index 0000000..204283b --- /dev/null +++ b/ui/AbilityButtonLayout.gd @@ -0,0 +1,49 @@ +class_name AbilityButtonLayout +extends Node + +# Keeps the aimed fleet abilities close to the firing thumb. One ability sits +# directly above the fire stick; multiple abilities fan through its upper-left +# quadrant instead of becoming a detached horizontal toolbar. + +@export var arc_radius: float = 280.0 + +@onready var fire_stick: Control = get_node("../UI/FireStick") +@onready var buttons: Array = [ + get_node("../UI/DashButton"), + get_node("../UI/GasBombButton"), + get_node("../UI/LaserButton"), +] + +var visibility_signature: int = -1 + + +func _ready() -> void: + get_viewport().size_changed.connect(_layout_visible_buttons) + _layout_visible_buttons.call_deferred() + + +func _process(_delta: float) -> void: + var signature: int = 0 + for i in range(buttons.size()): + if buttons[i].visible: + signature |= 1 << i + if signature == visibility_signature: + return + visibility_signature = signature + _layout_visible_buttons() + + +func _layout_visible_buttons() -> void: + var visible_buttons: Array = buttons.filter(func(button): return button.visible) + if visible_buttons.is_empty(): + return + var center: Vector2 = fire_stick.global_position + fire_stick.size * 0.5 + for i in range(visible_buttons.size()): + # Screen coordinates grow downward: PI points left and 3*PI/2 points + # upward, tracing exactly the useful quarter around a bottom-right stick. + var weight: float = 1.0 if visible_buttons.size() == 1 \ + else float(i) / float(visible_buttons.size() - 1) + var angle: float = lerpf(PI, PI * 1.5, weight) + var button: Control = visible_buttons[i] + button.global_position = center + Vector2.from_angle(angle) * arc_radius \ + - button.size * 0.5 diff --git a/vessels/LaserRangeUpgrade.tres b/vessels/LaserRangeUpgrade.tres index 061a994..6c7c06d 100644 --- a/vessels/LaserRangeUpgrade.tres +++ b/vessels/LaserRangeUpgrade.tres @@ -4,9 +4,9 @@ script = ExtResource("1") upgrade_id = "lsr_range" display_name = "Beam Collimator" -description = "+150 beam range per level." +description = "+300 beam range per level." base_cost = 25 max_level = 3 research_point_cost = 1 icon = 17 -additive_modifiers = {"beam_range": 150.0} +additive_modifiers = {"beam_range": 300.0} diff --git a/vessels/LaserVesselDefinition.tres b/vessels/LaserVesselDefinition.tres index 76a840d..820b6cb 100644 --- a/vessels/LaserVesselDefinition.tres +++ b/vessels/LaserVesselDefinition.tres @@ -18,7 +18,7 @@ hull_points = PackedVector2Array(0, -26, -9, 14, -4, 10, 4, 10, 9, 14) fleet_eligible = true capabilities = PackedStringArray("laser") component_scenes = Array[PackedScene]([ExtResource("2")]) -base_stats = {"max_speed": 660.0, "acceleration": 1890.0, "delivery_speed": 180.0, "damage": 2.0, "beam_range": 1450.0, "beam_cooldown": 4.0} +base_stats = {"max_speed": 660.0, "acceleration": 1890.0, "delivery_speed": 180.0, "damage": 2.0, "beam_range": 2900.0, "beam_cooldown": 4.0} minimum_stats = {"beam_cooldown": 1.5} upgrades = Array[Resource]([ExtResource("3"), ExtResource("4"), ExtResource("5")]) unlock_id = "unlock_laser" diff --git a/vessels/VesselLaserComponent.gd b/vessels/VesselLaserComponent.gd index aa03d12..553357d 100644 --- a/vessels/VesselLaserComponent.gd +++ b/vessels/VesselLaserComponent.gd @@ -3,7 +3,7 @@ extends "res://vessels/VesselCooldownFillComponent.gd" var capability_id := "laser" var damage: int = 2 -var beam_range: float = 950.0 +var beam_range: float = 2900.0 const READY_COLOR := Color(0.3, 0.9, 1.0) const COOLING_COLOR := Color(0.08, 0.28, 0.34) diff --git a/world/Main.tscn b/world/Main.tscn index 67ceec6..63e8c1e 100644 --- a/world/Main.tscn +++ b/world/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=45 format=3] +[gd_scene load_steps=46 format=3] [ext_resource type="PackedScene" path="res://ui/menus/BuildMenu.tscn" id="menu_build"] [ext_resource type="PackedScene" path="res://ui/menus/HangarMenu.tscn" id="menu_hangar"] [ext_resource type="PackedScene" path="res://ui/menus/LabMenu.tscn" id="menu_lab"] @@ -41,6 +41,7 @@ [ext_resource type="Script" path="res://controllers/GasBombController.gd" id="29"] [ext_resource type="Script" path="res://controllers/LaserController.gd" id="30"] [ext_resource type="Script" path="res://controllers/DashController.gd" id="dash_ctrl"] +[ext_resource type="Script" path="res://ui/AbilityButtonLayout.gd" id="ability_layout"] [ext_resource type="Script" path="res://ui/SpeedFader.gd" id="31"] [ext_resource type="Script" path="res://world/QuestDirector.gd" id="quest_dir"] [ext_resource type="Script" path="res://ui/QuestPanel.gd" id="quest_panel"] @@ -87,6 +88,9 @@ script = ExtResource("30") [node name="DashController" type="Node2D" parent="."] script = ExtResource("dash_ctrl") +[node name="AbilityButtonLayout" type="Node" parent="."] +script = ExtResource("ability_layout") + [node name="Camera2D" type="Camera2D" parent="."] position = Vector2(0, 0) zoom = Vector2(1, 1)