diff --git a/BuildController.gd b/BuildController.gd index c8ed446..be1adf7 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -680,6 +680,13 @@ func _confirm_build() -> void: if not GameState.is_planet_conquered(planet.planet_id): build_status.text = "%s is not captured yet" % planet.planet_name return + if String(planet.resource_type) == "": + # #63: a research_lab planet's whole point is that it yields + # nothing to mine - an extractor there would sit on an empty + # resource id with no ResourceDefinition, breaking its own + # glyph/HUD rendering the moment it produced anything. + build_status.text = "%s has nothing to extract" % planet.planet_name + return if not GameState.spend(definition.cost): build_status.text = "Not enough rocks (need %d, have %d)" % [definition.cost, GameState.rocks] return @@ -723,14 +730,18 @@ func _refresh_skill_menu(id: String = "") -> void: # leaving the panel blank, which reads as broken. skill_detail.text = "Select a node to see what it unlocks." skill_cost.amount = 0 - skill_status.text = "Fleet capacity %d" % GameState.max_fleet_size() + skill_status.text = "Fleet capacity %d · Research points %d" \ + % [GameState.max_fleet_size(), GameState.research_points] skill_unlock_button.visible = false return var node: Dictionary = GameState.skill_node(id) if node.is_empty(): return + var points_needed: int = int(node.get("research_points", 0)) skill_detail.text = "%s\n%s" % [node.name, node.desc] + if points_needed > 0: + skill_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, points_needed] skill_cost.amount = int(node.cost) if GameState.is_unlocked(id): @@ -742,6 +753,12 @@ func _refresh_skill_menu(id: String = "") -> void: elif not GameState.can_afford(int(node.cost)): skill_status.text = "Not enough rocks - you have %d" % GameState.rocks skill_unlock_button.visible = false + elif GameState.research_points < points_needed: + # #63: a distinct reason from the ore one above - conflating them + # would leave the player trying to fix the wrong shortage. + skill_status.text = "Not enough research points - you have %d, need %d" \ + % [GameState.research_points, points_needed] + skill_unlock_button.visible = false else: skill_status.text = "" skill_unlock_button.visible = true @@ -902,7 +919,8 @@ func _refresh_lab() -> void: lab_vessel_labels[i].text = definition.display_name lab_vessel_buttons[i].highlighted = this_vessel == lab_vessel lab_vessel_buttons[i].queue_redraw() - lab_vessel_label.text = "%s research" % GameState.vessel_name(lab_vessel) + lab_vessel_label.text = "%s research · %d RP" % [ + GameState.vessel_name(lab_vessel), GameState.research_points] var options: Array = _lab_options() for i in range(lab_upgrade_buttons.size()): @@ -914,6 +932,9 @@ func _refresh_lab() -> void: var option: Dictionary = options[i] var price_text: String = _format_costs(option.costs if bool(option.unlock) \ else GameState.research_costs(option.id)) + var point_cost: int = GameState.research_point_cost(String(option.id)) + if point_cost > 0: + price_text += " · %d RP" % point_cost if not bool(option.unlock) and GameState.upgrade_maxed(option.id): price_text = "MAX" var level_note: String = "" if bool(option.unlock) else " %d/%d" % [ @@ -932,11 +953,22 @@ func _refresh_lab() -> void: lab_choice = clampi(lab_choice, 0, options.size() - 1) var choice: Dictionary = options[lab_choice] + var choice_points: int = GameState.research_point_cost(String(choice.id)) lab_detail.text = "%s\n%s" % [choice.name, choice.desc] + if choice_points > 0: + lab_detail.text += "\nResearch points: %d/%d" % [GameState.research_points, choice_points] var queued: Dictionary = GameState.lab_research_order(lab_building_id) lab_buy_button.enabled = queued.is_empty() and (choice.unlock or not GameState.upgrade_maxed(choice.id)) if not queued.is_empty(): - lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))] + var queued_points: int = GameState.research_point_cost(String(queued.id)) + if not bool(queued.started) and GameState.research_points < queued_points: + # #63: distinct from "Waiting for resources" - flying more ore/gas + # to this lab cannot fix a research-point shortfall, only + # conquering a research lab elsewhere can. + lab_status.text = "Need %d research point%s (have %d)" \ + % [queued_points, "" if queued_points == 1 else "s", GameState.research_points] + else: + lab_status.text = "%s %d%%" % ["Researching" if bool(queued.started) else "Waiting for resources", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))] lab_buy_button.queue_redraw() diff --git a/CarrierCapacityUpgrade.tres b/CarrierCapacityUpgrade.tres index 8736c1b..99c01ac 100644 --- a/CarrierCapacityUpgrade.tres +++ b/CarrierCapacityUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Reinforced Hold" description = "+30 cargo capacity per level." base_cost = 40 max_level = 3 +research_point_cost = 1 icon = 21 additive_modifiers = {"cargo_capacity": 30.0} diff --git a/CarrierVesselDefinition.tres b/CarrierVesselDefinition.tres index 6d7012b..91d2743 100644 --- a/CarrierVesselDefinition.tres +++ b/CarrierVesselDefinition.tres @@ -22,4 +22,5 @@ unlock_name = "Unlock Carrier" unlock_description = "Research a dedicated hauler hull that trades weapons for a much larger cargo hold." unlock_cost = 0 unlock_resource_costs = {"ore": 60} +research_point_cost = 1 unlock_icon = -1 diff --git a/DistorderVesselDefinition.tres b/DistorderVesselDefinition.tres index 9e2a0de..c9bc3e7 100644 --- a/DistorderVesselDefinition.tres +++ b/DistorderVesselDefinition.tres @@ -21,4 +21,5 @@ unlock_name = "Unlock Distorder" unlock_description = "Research a space-time distortion hull - while one flies with the fleet, a fader lets you push cruise speed beyond its default." unlock_cost = 0 unlock_resource_costs = {"energy": 50} +research_point_cost = 1 unlock_icon = -1 diff --git a/GameState.gd b/GameState.gd index 6f1f295..8397b7e 100644 --- a/GameState.gd +++ b/GameState.gd @@ -4,6 +4,7 @@ const INVENTORY = preload("res://Inventory.gd") const RESOURCE_DEFINITIONS = preload("res://ResourceDefinitions.gd") const BUILDING_DEFINITIONS = preload("res://BuildingDefinitions.gd") const VESSEL_DEFINITIONS = preload("res://VesselDefinitions.gd") +const PLANET_DEFINITIONS = preload("res://PlanetDefinitions.gd") # Autoload singleton - the one thing that survives change_scene_to_file() @@ -44,6 +45,13 @@ var boost_depleted_notice: bool = false func set_fleet_speed_multiplier(value: float) -> void: fleet_speed_multiplier = clampf(value, 1.0, MAX_FLEET_SPEED_MULTIPLIER) + +# #63: currency gated behind conquering a research_lab planet - required by +# both progression paths (skill tree nodes, vessel/upgrade research) on top +# of their existing ore/resource costs. +signal research_points_changed(total: int) +var research_points: int = 0 + # Explored-chunk history for the world map's fog-of-war reveal. Lives # here (not on ExplorationTracker, which gets destroyed and recreated on # every scene swap) so exploration progress isn't lost every time the @@ -272,6 +280,13 @@ var buildings: Array = [] # tree always reads root-at-the-bottom and cannot drift out of step with the # dependencies when nodes are added. `requires` is a single prerequisite id # ("" for a root). +# #63: research_points (default 0 where omitted) is a SEPARATE currency from +# cost (ore), spent only from GameState.research_points - earned by +# conquering a research_lab planet, never bought. Balance, decided +# deliberately: shipyards and logistics stay at 0 so a fresh game can still +# reach a ship crafter and an extractor with zero research points (see the +# closing comment on #63 for the reasoning); every other node - none of them +# required for that opening - costs at least 1. const SKILL_TREE: Array = [ { "id": "shipyards", "name": "Shipyards", "cost": 15, "requires": "", @@ -285,42 +300,42 @@ const SKILL_TREE: Array = [ }, { "id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards", - "x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, + "x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "research_points": 1, "desc": "Build command centers, where the fleet formation is arranged.", }, { "id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics", - "x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, + "x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1, "desc": "Build transport hubs. Each hub includes its own automatic transporters.", }, { "id": "vessel_science", "name": "Vessel Science", "cost": 35, "requires": "shipyards", - "x": 0.82, "unlocks": BUILDING_SHIP_LAB, + "x": 0.82, "unlocks": BUILDING_SHIP_LAB, "research_points": 1, "desc": "Build ship labs, where each vessel type is upgraded and new ones unlocked.", }, { "id": "freight_yards", "name": "Freight Yards", "cost": 80, "requires": "freight", - "x": 0.18, "hub_bonus": 1, + "x": 0.18, "hub_bonus": 1, "research_points": 2, "desc": "Permanent: +1 transporter stationed at every hub, including ones already built.", }, { "id": "power_systems", "name": "Power Systems", "cost": 45, "requires": "freight", - "x": 0.50, "unlocks": BUILDING_GAS_BURNER, + "x": 0.50, "unlocks": BUILDING_GAS_BURNER, "research_points": 2, "desc": "Build gas burners. Each converts gas into energy at its own site - hub transporters keep it stocked and collect what it makes.", }, { "id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination", - "x": 0.82, "fleet_bonus": 1, + "x": 0.82, "fleet_bonus": 1, "research_points": 1, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing", - "x": 0.50, "fleet_bonus": 1, + "x": 0.50, "fleet_bonus": 1, "research_points": 2, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron", - "x": 0.50, "fleet_bonus": 1, + "x": 0.50, "fleet_bonus": 1, "research_points": 3, "desc": "Permanent: +1 vessel in the fleet.", }, ] @@ -370,16 +385,23 @@ func prerequisite_met(id: String) -> bool: func can_unlock(id: String) -> bool: - return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(skill_node(id).cost)) + var node: Dictionary = skill_node(id) + return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(node.cost)) \ + and research_points >= int(node.get("research_points", 0)) func unlock(id: String) -> bool: if not can_unlock(id): return false - if not spend(int(skill_node(id).cost)): + var node: Dictionary = skill_node(id) + if not spend(int(node.cost)): return false + var points_needed: int = int(node.get("research_points", 0)) + if points_needed > 0: + research_points -= points_needed + research_points_changed.emit(research_points) unlocked.append(id) - if int(skill_node(id).get("hub_bonus", 0)) > 0: + if int(node.get("hub_bonus", 0)) > 0: _sync_hub_transporters() return true @@ -424,6 +446,7 @@ func reset_new_game() -> void: planet_id = "" planet_definition_id = "rocky" fleet_speed_multiplier = 1.0 + research_points = 0 explored_chunks.clear() bank_inventory = INVENTORY.new({RESOURCE_ORE: 500}) fleet_inventory = INVENTORY.new() @@ -600,10 +623,14 @@ func unlock_vessel(vessel: String) -> bool: if definition == null or definition.unlock_id == "" or vessel_is_unlocked(vessel) or not has_ship_lab(): return false var costs: Dictionary = research_costs(definition.unlock_id) - if not can_afford_resources(costs): + var points_needed: int = int(definition.research_point_cost) + if not can_afford_resources(costs) or research_points < points_needed: return false for resource_type in costs: bank_inventory.withdraw(String(resource_type), int(costs[resource_type])) + if points_needed > 0: + research_points -= points_needed + research_points_changed.emit(research_points) vessel_levels[definition.unlock_id] = 1 vessel_upgraded.emit(definition.unlock_id) return true @@ -650,7 +677,8 @@ func upgrade_cost(id: String) -> int: func can_buy_upgrade(id: String) -> bool: - return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) + return has_ship_lab() and not upgrade_maxed(id) and can_afford(upgrade_cost(id)) \ + and research_points >= research_point_cost(id) func buy_upgrade(id: String) -> bool: @@ -658,6 +686,10 @@ func buy_upgrade(id: String) -> bool: return false if not spend(upgrade_cost(id)): return false + var points_needed: int = research_point_cost(id) + if points_needed > 0: + research_points -= points_needed + research_points_changed.emit(research_points) vessel_levels[id] = upgrade_level(id) + 1 vessel_upgraded.emit(id) return true @@ -920,6 +952,14 @@ func research_cost(id: String) -> int: return int(research_costs(id).get(RESOURCE_ORE, 0)) +func research_point_cost(id: String) -> int: + var unlock_definition: Resource = vessel_for_unlock(id) + if unlock_definition != null: + return int(unlock_definition.research_point_cost) + var d: Dictionary = upgrade_def(id) + return int(d.resource.research_point_cost) if not d.is_empty() else 0 + + func research_costs(id: String) -> Dictionary: var unlock_definition: Resource = vessel_for_unlock(id) if unlock_definition != null: @@ -948,10 +988,20 @@ func _advance_lab_research(delta: float) -> void: if inventory.amount(String(resource_type)) < int(costs[resource_type]): affordable = false break + # #63: research points are a fleet-wide currency, not delivered + # to the lab like ore/gas - checked and spent here too so a + # shortfall stalls the order the same way an undelivered + # resource does, rather than letting it start regardless. + var points_needed: int = research_point_cost(id) + if points_needed > research_points: + affordable = false if not affordable: continue for resource_type in costs: inventory.withdraw(String(resource_type), int(costs[resource_type])) + if points_needed > 0: + research_points -= points_needed + research_points_changed.emit(research_points) order.started = true order.elapsed = float(order.elapsed) + delta if float(order.elapsed) < lab_research_time(): @@ -1212,7 +1262,7 @@ var planet_capture: Dictionary = {} signal planet_conquered(planet_id: String) -func register_capture_points(planet_key: String, total: int) -> void: +func register_capture_points(planet_key: String, total: int, definition_id: String = "") -> void: # Idempotent: landing again on a planet must not wipe points already # captured on a previous visit. if planet_capture.has(planet_key): @@ -1223,8 +1273,13 @@ func register_capture_points(planet_key: String, total: int) -> void: # A planet demanding zero points is vacuously already held (#55: a # black hole's "void" profile has no authored PlanetSetup at all, so # this is the only registration it ever gets) - every planet WITH real - # points still starts unconquered, exactly as before. - planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0} + # points still starts unconquered, exactly as before. The planet definition + # supplies an optional conquest reward, avoiding planet-id special cases in + # the capture system. + var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) + var point_reward: int = 0 if definition == null else int(definition.conquest_research_points) + planet_capture[planet_key] = {"captured": captured, "conquered": total <= 0, + "research_point_reward": point_reward, "research_awarded": false} func capture_state(planet_key: String) -> Dictionary: @@ -1251,6 +1306,16 @@ func mark_point_captured(planet_key: String, index: int) -> void: if captured.all(func(c): return bool(c)): state.conquered = true planet_conquered.emit(planet_key) + # #63: definition-authored research points, exactly once per planet. + # state.conquered flipping above already makes a second call to this + # whole function a no-op via the early return above it, but + # research_awarded is its own explicit guard on the award itself + # regardless of how conquest was reached. + var reward: int = int(state.get("research_point_reward", 0)) + if reward > 0 and not bool(state.get("research_awarded", false)): + state.research_awarded = true + research_points += reward + research_points_changed.emit(research_points) func is_planet_conquered(planet_key: String) -> bool: diff --git a/GasBomberVesselDefinition.tres b/GasBomberVesselDefinition.tres index d464a4e..aad85ae 100644 --- a/GasBomberVesselDefinition.tres +++ b/GasBomberVesselDefinition.tres @@ -23,4 +23,5 @@ unlock_name = "Unlock Gas Bomber" unlock_description = "Research volatile-gas containment and unlock the Gas Bomber for ship crafters." unlock_cost = 0 unlock_resource_costs = {"gas": 30} +research_point_cost = 1 unlock_icon = 31 diff --git a/InterceptorDamageUpgrade.tres b/InterceptorDamageUpgrade.tres index 835851a..a0845fc 100644 --- a/InterceptorDamageUpgrade.tres +++ b/InterceptorDamageUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Heavy Slugs" description = "+1 damage per shot." base_cost = 30 max_level = 3 +research_point_cost = 1 icon = 15 additive_modifiers = {"damage": 1.0} diff --git a/InterceptorRangeUpgrade.tres b/InterceptorRangeUpgrade.tres index ce01efe..b986ba0 100644 --- a/InterceptorRangeUpgrade.tres +++ b/InterceptorRangeUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Targeting Array" description = "+60 firing range per level." base_cost = 20 max_level = 3 +research_point_cost = 1 icon = 17 additive_modifiers = {"fire_range": 60.0} diff --git a/InterceptorRateUpgrade.tres b/InterceptorRateUpgrade.tres index 4add45e..57dabe6 100644 --- a/InterceptorRateUpgrade.tres +++ b/InterceptorRateUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Autoloader" description = "Fires 15% faster per level." base_cost = 25 max_level = 3 +research_point_cost = 1 icon = 16 multiplicative_modifiers = {"fire_cooldown": 0.85} diff --git a/InterceptorSpeedUpgrade.tres b/InterceptorSpeedUpgrade.tres index 5549971..16ac1fe 100644 --- a/InterceptorSpeedUpgrade.tres +++ b/InterceptorSpeedUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Thrusters" description = "+40 top speed, +60 acceleration per level." base_cost = 25 max_level = 3 +research_point_cost = 1 icon = 18 additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0} diff --git a/LaserCooldownUpgrade.tres b/LaserCooldownUpgrade.tres index 71ad6aa..b89ee4a 100644 --- a/LaserCooldownUpgrade.tres +++ b/LaserCooldownUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Capacitor Bank" description = "Recharges 0.6s sooner per level." base_cost = 30 max_level = 3 +research_point_cost = 1 icon = 20 additive_modifiers = {"beam_cooldown": -0.6} diff --git a/LaserDamageUpgrade.tres b/LaserDamageUpgrade.tres index daa76ab..e9c4c97 100644 --- a/LaserDamageUpgrade.tres +++ b/LaserDamageUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Focusing Lens" description = "+1 damage per shot." base_cost = 30 max_level = 3 +research_point_cost = 1 icon = 15 additive_modifiers = {"damage": 1.0} diff --git a/LaserRangeUpgrade.tres b/LaserRangeUpgrade.tres index 447c58e..755e585 100644 --- a/LaserRangeUpgrade.tres +++ b/LaserRangeUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Beam Collimator" description = "+150 beam range per level." base_cost = 25 max_level = 3 +research_point_cost = 1 icon = 17 additive_modifiers = {"beam_range": 150.0} diff --git a/LaserVesselDefinition.tres b/LaserVesselDefinition.tres index 370a0d5..ea4b7bc 100644 --- a/LaserVesselDefinition.tres +++ b/LaserVesselDefinition.tres @@ -26,4 +26,5 @@ unlock_name = "Unlock Laser Ship" unlock_description = "Research a directed-energy hull for the ship crafter - manually aimed, no ammo, cools down between shots." unlock_cost = 0 unlock_resource_costs = {"energy": 40} +research_point_cost = 1 unlock_icon = -1 diff --git a/Main.tscn b/Main.tscn index be098dc..e7a1427 100644 --- a/Main.tscn +++ b/Main.tscn @@ -2086,9 +2086,9 @@ anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 offset_left = -64.0 -offset_top = -456.0 +offset_top = -400.0 offset_right = 76.0 -offset_bottom = -392.0 +offset_bottom = -336.0 script = ExtResource("19") digit_height = 52.0 @@ -2098,9 +2098,9 @@ anchor_right = 1.0 anchor_top = 1.0 anchor_bottom = 1.0 offset_left = 32.0 -offset_top = -386.0 +offset_top = -326.0 offset_right = -32.0 -offset_bottom = -318.0 +offset_bottom = -250.0 theme_override_font_sizes/font_size = 32 autowrap_mode = 2 horizontal_alignment = 1 @@ -2112,9 +2112,9 @@ anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 offset_left = -74.0 -offset_top = -304.0 +offset_top = -236.0 offset_right = 74.0 -offset_bottom = -156.0 +offset_bottom = -88.0 script = ExtResource("10") icon = 5 glow_color = Color(0.45, 0.9, 0.55, 1) diff --git a/PlanetDefinition.gd b/PlanetDefinition.gd index e184241..6678109 100644 --- a/PlanetDefinition.gd +++ b/PlanetDefinition.gd @@ -8,3 +8,7 @@ extends Resource @export var surface_profile_id: String = "rocky" @export var resource_id: String = "ore" @export var map_color: Color = Color(0.7, 0.65, 0.55) +# Optional reward granted once when every authored capture point is claimed. +# Keeping it on the definition lets conquest remain generic as new planet +# rewards are introduced. +@export var conquest_research_points: int = 0 diff --git a/PlanetDefinitions.gd b/PlanetDefinitions.gd index bada655..197202e 100644 --- a/PlanetDefinitions.gd +++ b/PlanetDefinitions.gd @@ -4,11 +4,13 @@ extends RefCounted const ROCKY_ID: String = "rocky" const GAS_GIANT_ID: String = "gas_giant" const BLACK_HOLE_ID: String = "black_hole" +const RESEARCH_LAB_ID: String = "research_lab" const DEFAULT_DEFINITION_ID: String = ROCKY_ID const DEFINITIONS: Dictionary = { ROCKY_ID: preload("res://RockyPlanetDefinition.tres"), GAS_GIANT_ID: preload("res://GasGiantDefinition.tres"), BLACK_HOLE_ID: preload("res://BlackHolePlanetDefinition.tres"), + RESEARCH_LAB_ID: preload("res://ResearchLabPlanetDefinition.tres"), } diff --git a/PlanetSetupHost.gd b/PlanetSetupHost.gd index 1b79167..99e145e 100644 --- a/PlanetSetupHost.gd +++ b/PlanetSetupHost.gd @@ -19,7 +19,8 @@ func apply_setup(new_setup: Resource) -> void: if setup == null: return - GameState.register_capture_points(GameState.planet_id, setup.capture_points.size()) + GameState.register_capture_points(GameState.planet_id, setup.capture_points.size(), + GameState.planet_definition_id) for i in range(setup.capture_points.size()): _add_capture_point(i, setup.capture_points[i]) for spawn in setup.enemy_spawns: diff --git a/PlanetSetups/ResearchLabSetup.tres b/PlanetSetups/ResearchLabSetup.tres new file mode 100644 index 0000000..20b4075 --- /dev/null +++ b/PlanetSetups/ResearchLabSetup.tres @@ -0,0 +1,42 @@ +[gd_resource type="Resource" script_class="PlanetSetup" format=3] + +[ext_resource type="Script" path="res://PlanetSetupSpawn.gd" id="1"] +[ext_resource type="Script" path="res://PlanetSetup.gd" id="2"] + +[sub_resource type="Resource" id="Resource_ta1"] +script = ExtResource("1") +enemy_id = "turret_spawner" +position = Vector2(-620.0, -180.0) + +[sub_resource type="Resource" id="Resource_ta2"] +script = ExtResource("1") +enemy_id = "turret_spawner" +position = Vector2(640.0, -160.0) + +[sub_resource type="Resource" id="Resource_ta3"] +script = ExtResource("1") +enemy_id = "artillery" +position = Vector2(0.0, -720.0) + +[sub_resource type="Resource" id="Resource_ta4"] +script = ExtResource("1") +enemy_id = "sniper" +position = Vector2(-380.0, 420.0) + +[sub_resource type="Resource" id="Resource_ta5"] +script = ExtResource("1") +enemy_id = "sniper" +position = Vector2(400.0, 440.0) + +[sub_resource type="Resource" id="Resource_ta6"] +script = ExtResource("1") +enemy_id = "chaser" +position = Vector2(120.0, 60.0) + +[resource] +script = ExtResource("2") +setup_id = "research_lab_1" +display_name = "Research Lab Garrison" +surface_profile_id = "research_lab" +capture_points = PackedVector2Array(-450.0, -60.0, 460.0, -40.0, 0.0, 480.0) +enemy_spawns = Array[Resource]([SubResource("Resource_ta1"), SubResource("Resource_ta2"), SubResource("Resource_ta3"), SubResource("Resource_ta4"), SubResource("Resource_ta5"), SubResource("Resource_ta6")]) diff --git a/PlanetSurfaceProfiles.gd b/PlanetSurfaceProfiles.gd index 5624fae..9d42e5e 100644 --- a/PlanetSurfaceProfiles.gd +++ b/PlanetSurfaceProfiles.gd @@ -6,6 +6,7 @@ const PROFILES: Dictionary = { "rocky": preload("res://RockySurfaceProfile.tres"), "gas": preload("res://GasSurfaceProfile.tres"), "void": preload("res://VoidSurfaceProfile.tres"), + "research_lab": preload("res://ResearchLabSurfaceProfile.tres"), } diff --git a/ResearchLabPlanetDefinition.tres b/ResearchLabPlanetDefinition.tres new file mode 100644 index 0000000..ac195df --- /dev/null +++ b/ResearchLabPlanetDefinition.tres @@ -0,0 +1,15 @@ +[gd_resource type="Resource" script_class="PlanetDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://PlanetDefinition.gd" id="1"] +[ext_resource type="PackedScene" path="res://ResearchLabPlanetVisual.tscn" id="2"] + +[resource] +script = ExtResource("1") +definition_id = "research_lab" +display_name = "Research Lab" +names = PackedStringArray("Blacksite Zero", "Cordoned Array", "Dead Signal Station", "The Vault", "Interdicted Post") +visual_scene = ExtResource("2") +surface_profile_id = "research_lab" +resource_id = "" +map_color = Color(0.7, 0.2, 0.15, 1) +conquest_research_points = 1 diff --git a/ResearchLabPlanetVisual.gd b/ResearchLabPlanetVisual.gd new file mode 100644 index 0000000..7f832a9 --- /dev/null +++ b/ResearchLabPlanetVisual.gd @@ -0,0 +1,55 @@ +extends Node2D + +# Same informal interface as GasGiantVisual/RockyPlanetVisual - configure(), +# extractor_anchor(), animates via its own _process(). Deliberately angular +# and armoured rather than a natural silhouette (#63: this is an ENEMY +# installation, not a celestial body), with a rotating dish and blinking +# warning lights to read as occupied/hostile at a glance. + +var phase: float = 0.0 + +const RADIUS: float = 108.0 +const HULL_COLOR: Color = Color(0.55, 0.1, 0.08) +const CORE_COLOR: Color = Color(0.9, 0.25, 0.18) +const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15) +const DARK_CORE: Color = Color(0.14, 0.02, 0.02) + + +func configure(_shape_seed: int) -> void: + queue_redraw() + + +func extractor_anchor() -> Vector2: + return Vector2(0.0, -(RADIUS + 40.0)) + + +func _process(delta: float) -> void: + phase += delta + queue_redraw() + + +func _draw() -> void: + var hull: PackedVector2Array = PackedVector2Array() + for i in range(6): + var angle: float = TAU * float(i) / 6.0 + hull.append(Vector2(cos(angle), sin(angle)) * RADIUS) + NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 0.85, DARK_CORE) + + var inner: PackedVector2Array = PackedVector2Array() + for i in range(6): + var angle: float = TAU * float(i) / 6.0 + PI / 6.0 + inner.append(Vector2(cos(angle), sin(angle)) * RADIUS * 0.55) + NeonDraw.draw_glowing_polygon(self, inner, CORE_COLOR, 0.9, DARK_CORE) + + # A slow-rotating dish - reads as actively transmitting, not derelict. + var dish_angle: float = phase * 0.6 + var dish_tip: Vector2 = Vector2(cos(dish_angle), sin(dish_angle)) * RADIUS * 1.35 + NeonDraw.draw_glowing_line(self, Vector2.ZERO, dish_tip, CORE_COLOR, 0.8, 0.55, DARK_CORE) + NeonDraw.draw_glowing_circle(self, dish_tip, 11.0, CORE_COLOR, 1.0, DARK_CORE) + + # Blinking hull-corner warning lights. + var blink: float = 0.4 + 0.6 * absf(sin(phase * 4.0)) + for i in range(6): + var angle: float = TAU * float(i) / 6.0 + var at: Vector2 = Vector2(cos(angle), sin(angle)) * RADIUS + NeonDraw.draw_glowing_circle(self, at, 5.0, WARNING_COLOR, blink, DARK_CORE) diff --git a/ResearchLabPlanetVisual.tscn b/ResearchLabPlanetVisual.tscn new file mode 100644 index 0000000..bb2ebaa --- /dev/null +++ b/ResearchLabPlanetVisual.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ResearchLabPlanetVisual.gd" id="1"] + +[node name="Visual" type="Node2D"] +script = ExtResource("1") diff --git a/ResearchLabSurfaceProfile.tres b/ResearchLabSurfaceProfile.tres new file mode 100644 index 0000000..98e83c8 --- /dev/null +++ b/ResearchLabSurfaceProfile.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="PlanetSurfaceProfile" load_steps=2 format=3] + +[ext_resource type="Script" path="res://PlanetSurfaceProfile.gd" id="1"] + +[resource] +script = ExtResource("1") +profile_id = "research_lab" +base_color = Color(0.13, 0.1, 0.1, 1) +crater_color = Color(0.05, 0.03, 0.03, 1) +highlight_color = Color(0.55, 0.15, 0.1, 1) +caption_color = Color(0.9, 0.6, 0.55, 1) +caption_outline_color = Color(0.05, 0.02, 0.02, 0.9) diff --git a/ShieldHitsUpgrade.tres b/ShieldHitsUpgrade.tres index 2530597..bbbd180 100644 --- a/ShieldHitsUpgrade.tres +++ b/ShieldHitsUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Layered Emitters" description = "+1 hit absorbed before the shield drops." base_cost = 60 max_level = 2 +research_point_cost = 1 icon = 19 additive_modifiers = {"shield_charges": 1.0} diff --git a/ShieldRechargeUpgrade.tres b/ShieldRechargeUpgrade.tres index d73cf09..756523c 100644 --- a/ShieldRechargeUpgrade.tres +++ b/ShieldRechargeUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Fast Cycling" description = "Shield returns 1.2s sooner per level." base_cost = 35 max_level = 3 +research_point_cost = 1 icon = 20 additive_modifiers = {"shield_recharge": -1.2} diff --git a/ShieldSpeedUpgrade.tres b/ShieldSpeedUpgrade.tres index fe5a4b6..ef2b8a4 100644 --- a/ShieldSpeedUpgrade.tres +++ b/ShieldSpeedUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Thrusters" description = "+40 top speed, +60 acceleration per level." base_cost = 25 max_level = 3 +research_point_cost = 1 icon = 18 additive_modifiers = {"max_speed": 40.0, "acceleration": 60.0} diff --git a/ShieldVesselDefinition.tres b/ShieldVesselDefinition.tres index aa4c440..0a99ba1 100644 --- a/ShieldVesselDefinition.tres +++ b/ShieldVesselDefinition.tres @@ -24,4 +24,5 @@ unlock_id = "unlock_shield" unlock_name = "Unlock Shield Vessel" unlock_description = "Lets the ship crafter build shield vessels. Its shield absorbs one lethal hit, then cycles back up." unlock_cost = 45 +research_point_cost = 1 unlock_icon = 23 diff --git a/TransportCapacityUpgrade.tres b/TransportCapacityUpgrade.tres index 2c2b920..87b331c 100644 --- a/TransportCapacityUpgrade.tres +++ b/TransportCapacityUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Expanded Hold" description = "Carries 8 more resources per supply run." base_cost = 30 max_level = 3 +research_point_cost = 1 icon = 21 additive_modifiers = {"cargo_capacity": 8.0} diff --git a/TransportSpeedUpgrade.tres b/TransportSpeedUpgrade.tres index 43c3c96..06c3f42 100644 --- a/TransportSpeedUpgrade.tres +++ b/TransportSpeedUpgrade.tres @@ -7,5 +7,6 @@ display_name = "Freight Engines" description = "+70 speed on supply runs." base_cost = 30 max_level = 3 +research_point_cost = 1 icon = 22 additive_modifiers = {"transport_speed": 70.0} diff --git a/VesselDefinition.gd b/VesselDefinition.gd index 1aecbfb..f7af0a0 100644 --- a/VesselDefinition.gd +++ b/VesselDefinition.gd @@ -23,6 +23,10 @@ extends Resource @export_multiline var unlock_description: String @export var unlock_cost: int @export var unlock_resource_costs: Dictionary = {} +# #63: spent from GameState.research_points, alongside unlock_resource_costs +# rather than instead of it. 0 (the default) means this unlock needs no +# research points at all. +@export var research_point_cost: int = 0 @export var unlock_icon: int = -1 diff --git a/VesselUpgradeDefinition.gd b/VesselUpgradeDefinition.gd index 083e092..0bc3872 100644 --- a/VesselUpgradeDefinition.gd +++ b/VesselUpgradeDefinition.gd @@ -9,3 +9,7 @@ extends Resource @export var icon: int @export var additive_modifiers: Dictionary = {} @export var multiplicative_modifiers: Dictionary = {} +# #63: spent from GameState.research_points per level bought, alongside +# base_cost (ore) rather than instead of it. 0 (the default) means this +# level needs no research points at all. +@export var research_point_cost: int = 0 diff --git a/WorldGen.gd b/WorldGen.gd index 3238051..b95837d 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -22,6 +22,10 @@ const GAS_GIANT_THRESHOLD: float = 0.55 # chance independent of biome - not a second smooth region to tune against # the first. const BLACK_HOLE_CHANCE: float = 0.05 +# #63: findable without excessive exploration - research points gate mid/ +# late skill-tree nodes and vessel research, so a player needs to run into +# one of these reasonably often, not just as a rare curiosity. +const RESEARCH_LAB_CHANCE: float = 0.08 # A predictable early gas source for play-testing. The origin chunk is # deterministically normal space. Its explicit in-chunk position puts it @@ -95,6 +99,8 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: # roll doesn't shift which random values any LATER draw (planet_name, # below) consumes for chunks that would have existed before it. var black_hole_roll: float = rng.randf() + # Same principle, its own independent roll (#63). + var research_lab_roll: float = rng.randf() # A stable per-chunk roll makes gas giants persistent landmarks rather # than a different resource whenever the scene reloads. var gas_giant: bool = guaranteed_nearby_giant or biome_at_chunk(chunk) == Biome.SPACE @@ -104,6 +110,8 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: # that chunk's whole purpose is to always be the same reachable source. if not guaranteed_nearby_giant and black_hole_roll < BLACK_HOLE_CHANCE: definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID + elif not guaranteed_nearby_giant and research_lab_roll < RESEARCH_LAB_CHANCE: + definition_id = PLANET_DEFINITIONS.RESEARCH_LAB_ID var definition: Resource = PLANET_DEFINITIONS.get_definition(definition_id) var names: PackedStringArray = definition.names var planet_name: String = names[rng.randi() % names.size()] diff --git a/tests/_skill_menu_preview.gd b/tests/_skill_menu_preview.gd index 96d4581..6999bdf 100644 --- a/tests/_skill_menu_preview.gd +++ b/tests/_skill_menu_preview.gd @@ -8,7 +8,13 @@ extends Node2D # ... and again at 720x960 to check a shorter screen format. -func _ready() -> void: +var configured: bool = false + + +func _process(_delta: float) -> void: + if configured: + return + configured = true GameState.buildings.clear() GameState.extractors.clear() GameState.transporters.clear() @@ -16,9 +22,12 @@ func _ready() -> void: GameState.gas = 36 GameState.fleet_cargo = 24 GameState.fleet_gas_cargo = 12 - GameState.unlocked = ["shipyards", "logistics", "freight"] + GameState.research_points = 0 + GameState.unlocked = ["shipyards"] var main: Node = load("res://Main.tscn").instantiate() add_child(main) + main.get_node("EnemySpawner").set_process(false) + main.get_node("AsteroidField").set_process(false) main.get_node("BuildingField").place(GameState.BUILDING_MOTHERSHIP, Vector2(400.0, 0.0)) var mothership_id: int = int(GameState.buildings[-1].id) var bc: Node = main.get_node("BuildController") @@ -27,5 +36,5 @@ func _ready() -> void: main.get_node("UI/SkillMenu").visible = true # Select a node so the description, price and status all have text - an # empty description cannot collide with anything. - bc.skill_tree.selected_id = "freight" - bc._refresh_skill_menu("freight") + bc.skill_tree.selected_id = "coordination" + bc._refresh_skill_menu("coordination") diff --git a/tests/test_carrier_vessel.gd b/tests/test_carrier_vessel.gd index adf159b..a5deec4 100644 --- a/tests/test_carrier_vessel.gd +++ b/tests/test_carrier_vessel.gd @@ -48,6 +48,7 @@ func _initialize() -> void: game_state.reset_progression() game_state.build_time_scale = 0.02 game_state.rocks = 500 + game_state.research_points = 1 scene = load("res://Main.tscn").instantiate() root.add_child(scene) fleet = scene.get_node("Fleet") diff --git a/tests/test_extractor_income.gd b/tests/test_extractor_income.gd index b89037e..f72ac4d 100644 --- a/tests/test_extractor_income.gd +++ b/tests/test_extractor_income.gd @@ -21,6 +21,7 @@ func _initialize() -> void: game_state.extractors.clear() game_state.transporters.clear() game_state.reset_progression() + game_state.research_points = 1 game_state.add_extractor("Test Planet", EXTRACTOR_AT) extractor_index = 0 diff --git a/tests/test_gas_bomber.gd b/tests/test_gas_bomber.gd index 8dc10cd..8ba50e7 100644 --- a/tests/test_gas_bomber.gd +++ b/tests/test_gas_bomber.gd @@ -27,6 +27,7 @@ func _initialize() -> void: game_state.fleet_cargo = 0 game_state.fleet_gas_cargo = 0 game_state.build_time_scale = 0.01 + game_state.research_points = 1 # #60: after research/crafting resets the fleet hold below, top it up # with fleet-carried gas for the shot itself (ammunition, not the ore/gas # spent researching or crafting the bomber). diff --git a/tests/test_lab_resources.gd b/tests/test_lab_resources.gd index e4cbeb7..57eabcb 100644 --- a/tests/test_lab_resources.gd +++ b/tests/test_lab_resources.gd @@ -10,6 +10,7 @@ func _initialize() -> void: game.vessel_levels.clear() game.build_time_scale = 0.02 game.rocks = 500 + game.research_points = 1 lab_id = game.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO) if not game.queue_lab_research(lab_id, "int_damage"): _fail("could not queue research") diff --git a/tests/test_lab_transporter_delivery.gd b/tests/test_lab_transporter_delivery.gd index dff33b2..6c1fe94 100644 --- a/tests/test_lab_transporter_delivery.gd +++ b/tests/test_lab_transporter_delivery.gd @@ -12,6 +12,7 @@ func _initialize() -> void: state.transporters.clear() state.reset_progression() state.rocks = 100 + state.research_points = 1 state.build_time_scale = 0.02 state.add_building(state.BUILDING_MOTHERSHIP, Vector2.ZERO) state.add_building(state.BUILDING_TRANSPORT_HUB, Vector2(50, 0)) diff --git a/tests/test_multi_resource_supply.gd b/tests/test_multi_resource_supply.gd index d29b574..ecf956c 100644 --- a/tests/test_multi_resource_supply.gd +++ b/tests/test_multi_resource_supply.gd @@ -10,6 +10,7 @@ func _initialize() -> void: state.reset_progression() state.rocks = 0 state.gas = 10 + state.research_points = 1 state.build_time_scale = 0.01 state.add_building(GameState.BUILDING_MOTHERSHIP, Vector2.ZERO) diff --git a/tests/test_research_points.gd b/tests/test_research_points.gd new file mode 100644 index 0000000..72adc94 --- /dev/null +++ b/tests/test_research_points.gd @@ -0,0 +1,143 @@ +extends SceneTree + +# Headless gameplay test: run with +# godot --headless -s res://tests/test_research_points.gd +# Forgejo #63: a rare enemy planet type that yields no resources but grants +# one research point when conquered - a required currency for mid/late +# skill-tree nodes and vessel/lab research, gated alongside their existing +# ore/resource costs. +# +# Covers: +# A. research labs generate deterministically and are rare +# (structural) the definition yields no resources and its surface has no +# resource provider at all +# F. a fresh game can still reach a ship crafter and an extractor with +# zero research points +# D. a research-point-gated skill node is refused while unaffordable, and +# explains why +# E. conquering a research_lab awards exactly one point, once (guarded +# even against a redundant re-mark), emits the signal once, and a +# non-research_lab planet's conquest never awards one +# D continued: unlocking spends the points and the balance updates +# G. the ship-lab research path gates and spends research points the +# same way, independent of the skill tree + +var game_state: Node + + +func _initialize() -> void: + game_state = root.get_node("GameState") + + # --- A. deterministic and rare ------------------------------------------- + var lab_count: int = 0 + var total_planets: int = 0 + for x in range(-40, 40): + for y in range(-40, 40): + var chunk: Vector2i = Vector2i(x, y) + if not WorldGen.has_planet_at_chunk(chunk): + continue + total_planets += 1 + var first: Dictionary = WorldGen.planet_data_at_chunk(chunk) + var second: Dictionary = WorldGen.planet_data_at_chunk(chunk) + if first.definition_id != second.definition_id or first.local_pos != second.local_pos: + return _fail("planet_data_at_chunk(%s) is not deterministic" % chunk) + if first.definition_id == "research_lab": + lab_count += 1 + print("scanned %d planet chunks: %d research labs" % [total_planets, lab_count]) + if lab_count == 0: + return _fail("no research labs generated across a wide scan - the rare outcome never fires") + if lab_count * 3 >= total_planets: + return _fail("research labs (%d of %d) are not rare" % [lab_count, total_planets]) + print("research labs are deterministic and rare (%d of %d planet chunks)" % [lab_count, total_planets]) + + # --- structural: yields nothing at all ------------------------------------ + var lab_def: Resource = load("res://PlanetDefinitions.gd").get_definition("research_lab") + if String(lab_def.resource_id) != "": + return _fail("research_lab planet's resource_id is '%s', expected empty" % lab_def.resource_id) + var profile: Resource = load("res://PlanetSurfaceProfiles.gd").get_profile("research_lab") + if profile.features_scene != null: + return _fail("research_lab surface profile has a features_scene - it must yield nothing") + print("research_lab yields no resource id and no surface feature provider") + + # --- F. a fresh game still reaches a crafter and an extractor ------------- + game_state.reset_new_game() + if not game_state.can_unlock("shipyards"): + return _fail("shipyards is not reachable with 0 research points") + game_state.unlock("shipyards") + if not game_state.can_unlock("logistics"): + return _fail("logistics is not reachable with 0 research points after shipyards") + game_state.unlock("logistics") + print("fresh game reaches a ship crafter (shipyards) and an extractor (logistics) at 0 research points") + + # --- D. a points-gated node is refused and explains why ------------------- + if game_state.can_unlock("coordination"): + return _fail("coordination (costs research points) was unlockable with 0 research points") + print("a research-point-gated node (coordination) correctly refused at 0 points") + + # --- E. earning exactly one point, once, with the signal ------------------ + var totals_seen: Array = [] + game_state.research_points_changed.connect(func(total): totals_seen.append(total)) + game_state.planet_capture.clear() + game_state.register_capture_points("test_lab_planet", 2, "research_lab") + if game_state.is_planet_conquered("test_lab_planet"): + return _fail("a lab with 2 real capture points started conquered") + game_state.mark_point_captured("test_lab_planet", 0) + if game_state.research_points != 0: + return _fail("a research point was awarded before every point was captured") + game_state.mark_point_captured("test_lab_planet", 1) + if game_state.research_points != 1: + return _fail("conquering a research_lab did not award exactly 1 point: %d" % game_state.research_points) + if totals_seen != [1]: + return _fail("research_points_changed did not fire exactly once with the new total: %s" % [totals_seen]) + print("conquering a research_lab awarded exactly 1 point and fired the signal once") + + game_state.mark_point_captured("test_lab_planet", 0) # already captured - redundant re-mark + if game_state.research_points != 1: + return _fail("re-marking an already-captured point re-awarded a research point: %d" % game_state.research_points) + print("double-award guarded against") + + game_state.register_capture_points("test_rocky_planet", 1, "rocky") + game_state.mark_point_captured("test_rocky_planet", 0) + if game_state.research_points != 1: + return _fail("conquering a non-research_lab planet awarded a research point: %d" % game_state.research_points) + print("a non-research_lab planet's conquest never awards a point") + + # --- D continued: unlocking spends the points, balance updates ------------ + if not game_state.can_unlock("coordination"): + return _fail("coordination is still not unlockable with 1 research point and enough ore") + if not game_state.unlock("coordination"): + return _fail("unlock(coordination) failed despite can_unlock() being true") + if game_state.research_points != 0: + return _fail("unlocking a 1-point node did not spend the point: %d remaining" % game_state.research_points) + print("unlocking a research-point-gated skill node spent the point, balance now 0") + + # --- G. the ship-lab research path gates and spends the same currency ----- + var carrier_def: Resource = game_state.vessel_definition(GameState.VESSEL_CARRIER) + var point_cost: int = int(carrier_def.research_point_cost) + if point_cost <= 0: + return _fail("carrier production data has no research-point cost") + game_state.vessel_levels.erase(carrier_def.unlock_id) + var lab_id: int = game_state.add_building(GameState.BUILDING_SHIP_LAB, Vector2.ZERO) + if not game_state.queue_lab_research(lab_id, carrier_def.unlock_id): + return _fail("could not queue carrier research") + game_state.deposit_resource_to_building(lab_id, GameState.RESOURCE_ORE, 1000) + game_state._advance_lab_research(100.0) + if game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER): + return _fail("carrier research completed despite 0 research points") + print("lab research correctly stalled on a research-point shortfall") + + game_state.research_points = point_cost + game_state._advance_lab_research(100.0) + if not game_state.vessel_is_unlocked(GameState.VESSEL_CARRIER): + return _fail("carrier research did not complete once research points were available") + if game_state.research_points != 0: + return _fail("lab research did not spend the research points: %d remaining" % game_state.research_points) + print("lab research spent the research points once affordable, and completed") + + print("TEST_RESEARCH_POINTS: PASS") + quit(0) + + +func _fail(message: String) -> void: + print("TEST_RESEARCH_POINTS: FAIL (%s)" % message) + quit(1) diff --git a/tests/test_ship_lab.gd b/tests/test_ship_lab.gd index df2c168..1342cce 100644 --- a/tests/test_ship_lab.gd +++ b/tests/test_ship_lab.gd @@ -81,6 +81,7 @@ func _process(delta: float) -> bool: print("no lab -> no upgrades") game_state.unlocked = ["shipyards", "vessel_science", "coordination", "wing", "squadron"] + game_state.research_points = 10 field.place(GameState.BUILDING_SHIP_LAB, Vector2(-700.0, 0.0)) field.place(GameState.BUILDING_SHIP_CRAFTER, Vector2(700.0, 0.0)) crafter_id = int(game_state.buildings[-1].id) diff --git a/tests/test_skill_tree.gd b/tests/test_skill_tree.gd index 5c9d21c..e7dc526 100644 --- a/tests/test_skill_tree.gd +++ b/tests/test_skill_tree.gd @@ -155,6 +155,7 @@ func _process(delta: float) -> bool: # --- fleet capacity ------------------------------------------- var base_cap: int = game_state.max_fleet_size() + game_state.research_points = 2 game_state.unlock("coordination") var wing_ok: bool = game_state.unlock("wing") var raised_cap: int = game_state.max_fleet_size()