diff --git a/BuildController.gd b/BuildController.gd index 8b64164..8e7bc3d 100644 --- a/BuildController.gd +++ b/BuildController.gd @@ -98,6 +98,7 @@ extends Node2D # Buy controls below them only describe the one the player tapped. var lab_vessel: String = GameState.VESSEL_INTERCEPTOR var lab_choice: int = 0 +var lab_building_id: int = 0 @onready var planet_field: Node2D = get_node("../PlanetField") @onready var info_popup: Control = get_node("../UI/BuildMenu/InfoPopup") @onready var info_title: Label = get_node("../UI/BuildMenu/InfoPopup/Panel/TitleLabel") @@ -488,6 +489,7 @@ func _on_action_pressed() -> void: craft_menu.visible = true _refresh_craft_status() elif current_building.building_type == GameState.BUILDING_SHIP_LAB: + lab_building_id = current_building.building_id lab_menu.visible = true _refresh_lab() elif current_building.building_type == GameState.BUILDING_COMMAND_CENTER: @@ -878,25 +880,11 @@ func _on_lab_buy() -> void: if options.is_empty(): return var choice: Dictionary = options[lab_choice] - if choice.unlock: - if GameState.unlock_shield_vessel(): - lab_status.text = "Shield vessel unlocked - build one at a ship crafter" - else: - lab_status.text = "Not enough ore: needs %d, you have %d" % [ - GameState.UNLOCK_SHIELD_COST, GameState.rocks - ] - lab_choice = 0 - _refresh_lab() + var id: String = GameState.UNLOCK_SHIELD_VESSEL if choice.unlock else String(choice.id) + if not GameState.queue_lab_research(lab_building_id, id): + lab_status.text = "Research is already queued or complete" return - var id: String = choice.id - if GameState.upgrade_maxed(id): - lab_status.text = "%s is already fully researched" % choice.name - return - var cost: int = GameState.upgrade_cost(id) - if not GameState.buy_upgrade(id): - lab_status.text = "Not enough ore: needs %d, you have %d" % [cost, GameState.rocks] - return - lab_status.text = "%s is now level %d" % [choice.name, GameState.upgrade_level(id)] + lab_status.text = "%s queued - deliver ore to this lab" % choice.name _refresh_lab() @@ -938,10 +926,10 @@ func _refresh_lab() -> void: var choice: Dictionary = options[lab_choice] lab_detail.text = "%s\n%s" % [choice.name, choice.desc] - if choice.unlock: - lab_buy_button.enabled = GameState.can_afford(GameState.UNLOCK_SHIELD_COST) - else: - lab_buy_button.enabled = GameState.can_buy_upgrade(choice.id) + 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 ore", int(100.0 * float(queued.elapsed) / maxf(GameState.lab_research_time(), 0.001))] lab_buy_button.queue_redraw() diff --git a/GameState.gd b/GameState.gd index b95049b..0c0ae7d 100644 --- a/GameState.gd +++ b/GameState.gd @@ -357,6 +357,7 @@ const BUILD_TIMES: Dictionary = { VESSEL_SHIELD: 16.0, VESSEL_TRANSPORTER: 8.0, } +const LAB_RESEARCH_TIME: float = 8.0 # Test seam. Headless tests need the timed path - queued, started, finishing @@ -368,6 +369,10 @@ var build_time_scale: float = 1.0 func build_time(product: String) -> float: return float(BUILD_TIMES.get(product, 10.0)) * build_time_scale + +func lab_research_time() -> float: + return LAB_RESEARCH_TIME * build_time_scale + # Researched at the ship lab, per vessel type. Separate from SKILL_TREE on # purpose: that gates what the player may build at all and is a strategic # spine, while these are repeatable tuning on a vessel you already have. @@ -712,6 +717,50 @@ func _advance_production(delta: float) -> void: order_completed.emit(int(b.id), product) +func queue_lab_research(lab_id: int, upgrade_id: String) -> bool: + var lab: Dictionary = find_building(lab_id) + if lab.is_empty() or lab.type != BUILDING_SHIP_LAB or upgrade_id == "": + return false + var orders: Array = lab.get("research_orders", []) + if not orders.is_empty() or (upgrade_id != UNLOCK_SHIELD_VESSEL and upgrade_maxed(upgrade_id)): + return false + orders.append({"id": upgrade_id, "elapsed": 0.0, "started": false}) + lab.research_orders = orders + return true + + +func lab_research_order(lab_id: int) -> Dictionary: + var lab: Dictionary = find_building(lab_id) + var orders: Array = lab.get("research_orders", []) + return {} if orders.is_empty() else orders[0] + + +func _advance_lab_research(delta: float) -> void: + for lab in buildings: + if lab.type != BUILDING_SHIP_LAB: + continue + var orders: Array = lab.get("research_orders", []) + if orders.is_empty(): + continue + var order: Dictionary = orders[0] + var id: String = String(order.id) + var cost: int = UNLOCK_SHIELD_COST if id == UNLOCK_SHIELD_VESSEL else upgrade_cost(id) + if not bool(order.started): + if int(lab.get("stock", 0)) < cost: + continue + lab.stock = int(lab.stock) - cost + order.started = true + order.elapsed = float(order.elapsed) + delta + if float(order.elapsed) < lab_research_time(): + continue + if id == UNLOCK_SHIELD_VESSEL: + vessel_levels[id] = 1 + else: + vessel_levels[id] = upgrade_level(id) + 1 + vessel_upgraded.emit(id) + orders.pop_front() + + func claim_stored_vessel(index: int) -> String: # Hands one finished vessel over to be crewed. Returns its type, or "" # when there is nothing there - the caller decides whether the fleet @@ -829,6 +878,7 @@ func _process(delta: float) -> void: # Runs in the autoload, so mining and transport continue while the player # is on a planet's surface or nowhere near either. _advance_production(delta) + _advance_lab_research(delta) _advance_vessel_deliveries(delta) for e in extractors: e.mining_progress = float(e.get("mining_progress", 0.0)) + delta * EXTRACTOR_RATE diff --git a/GasVentField.gd b/GasVentField.gd new file mode 100644 index 0000000..3976287 --- /dev/null +++ b/GasVentField.gd @@ -0,0 +1,21 @@ +extends Node2D +@onready var fleet: Node2D = get_node("../Fleet") +var vents: Array[Vector2] = [] +var progress: float = 0.0 +func _ready() -> void: + var rng := RandomNumberGenerator.new() + rng.seed = hash(GameState.planet_name) + for i in range(6): vents.append(Vector2(rng.randf_range(-800,800), rng.randf_range(-650,650))) + queue_redraw() +func _process(delta: float) -> void: + for vent in vents: + if fleet.get_centroid().distance_to(vent) < 90.0: + progress += delta + if progress >= 1.0: + GameState.gas += int(progress) + progress = fmod(progress, 1.0) + return + progress = 0.0 +func _draw() -> void: + for vent in vents: + NeonDraw.draw_glowing_polygon(self, PackedVector2Array([vent+Vector2(0,-44),vent+Vector2(38,-22),vent+Vector2(38,22),vent+Vector2(0,44),vent+Vector2(-38,22),vent+Vector2(-38,-22)]), Color(0.85,0.52,1),0.8,Color(0.15,0.03,0.22),0.5) diff --git a/PlanetSurface.tscn b/PlanetSurface.tscn index aa4a117..f5058a9 100644 --- a/PlanetSurface.tscn +++ b/PlanetSurface.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=11 format=3] +[gd_scene load_steps=12 format=3] [ext_resource type="Script" path="res://GameController.gd" id="1"] [ext_resource type="Script" path="res://LunarBackground.gd" id="2"] @@ -9,6 +9,7 @@ [ext_resource type="Script" path="res://TouchJoystick.gd" id="7"] [ext_resource type="Script" path="res://RockField.gd" id="8"] [ext_resource type="Script" path="res://ResourceCounter.gd" id="9"] +[ext_resource type="Script" path="res://GasVentField.gd" id="10"] [sub_resource type="ShaderMaterial" id="1"] shader = ExtResource("3") @@ -48,6 +49,10 @@ zoom = Vector2(1, 1) [node name="RockField" type="Node2D" parent="."] script = ExtResource("8") +[node name="GasVentField" type="Node2D" parent="."] +visible = false +script = ExtResource("10") + [node name="SurfaceController" type="Node2D" parent="."] script = ExtResource("5") diff --git a/PlanetSurfaceController.gd b/PlanetSurfaceController.gd index 63279f4..985e1b4 100644 --- a/PlanetSurfaceController.gd +++ b/PlanetSurfaceController.gd @@ -18,6 +18,12 @@ var transitioning: bool = false func _ready() -> void: + if GameState.planet_type == "gas_giant": + # A gas giant is not lunar terrain: its surface view uses a deep, + # coloured atmosphere instead of the rocky field. + get_node("../RockField").visible = false + get_node("../GasVentField").visible = true + get_node("../BackgroundLayer/LunarBackground").color = Color(0.16, 0.035, 0.26, 1.0) fleet.spawn_ships(GameState.surviving_ship_count, Vector2.ZERO) camera.position = Vector2.ZERO camera.zoom = Vector2(GameState.return_camera_zoom, GameState.return_camera_zoom) diff --git a/WorldGen.gd b/WorldGen.gd index ee0ce6f..4490807 100644 --- a/WorldGen.gd +++ b/WorldGen.gd @@ -13,6 +13,7 @@ const CHUNK_SIZE: float = 1200.0 const NOISE_SEED: int = 1337 const ASTEROID_FIELD_THRESHOLD: float = 0.15 const PLANET_THRESHOLD: float = 0.33 +const GAS_GIANT_THRESHOLD: float = 0.55 const PLANET_NAMES: Array = [ "Kepler's Rest", "Ceres Minor", "Vesta's Shadow", "Ironhold", @@ -61,10 +62,8 @@ static func biome_at_chunk(chunk: Vector2i) -> int: static func has_planet_at_chunk(chunk: Vector2i) -> bool: - if biome_at_chunk(chunk) != Biome.ASTEROID_FIELD: - return false var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y)) - return n > PLANET_THRESHOLD + return (biome_at_chunk(chunk) == Biome.ASTEROID_FIELD and n > PLANET_THRESHOLD) or (biome_at_chunk(chunk) == Biome.SPACE and n > GAS_GIANT_THRESHOLD) static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: @@ -82,7 +81,7 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary: var shape_seed: int = rng.randi() # A stable per-chunk roll makes gas giants persistent landmarks rather # than a different resource whenever the scene reloads. - var gas_giant: bool = rng.randf() < 0.28 + var gas_giant: bool = biome_at_chunk(chunk) == Biome.SPACE if gas_giant: planet_name = GAS_GIANT_NAMES[rng.randi() % GAS_GIANT_NAMES.size()] return {"local_pos": local_pos, "name": planet_name, "shape_seed": shape_seed, diff --git a/tests/test_lab_resources.gd b/tests/test_lab_resources.gd new file mode 100644 index 0000000..e4cbeb7 --- /dev/null +++ b/tests/test_lab_resources.gd @@ -0,0 +1,41 @@ +extends SceneTree + +var game: Node +var lab_id: int +var elapsed := 0.0 + +func _initialize() -> void: + game = root.get_node("GameState") + game.buildings.clear() + game.vessel_levels.clear() + game.build_time_scale = 0.02 + game.rocks = 500 + 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") + +func _process(delta: float) -> bool: + elapsed += delta + if elapsed < 0.2: + return false + var order: Dictionary = game.lab_research_order(lab_id) + if not order.is_empty() and not bool(order.started): + if game.rocks != 500: + return _fail("research drew from mothership bank") + game.deposit_to_building(lab_id, game.upgrade_cost("int_damage")) + return false + if game.upgrade_level("int_damage") == 1: + if game.building_stock(lab_id) != 0 or game.rocks != 500: + return _fail("local lab stock/payment was wrong") + print("lab research consumed local ore and completed over time") + print("TEST_LAB_RESOURCES: PASS") + quit(0) + return true + if elapsed > 3.0: + return _fail("research did not complete") + return false + +func _fail(message: String) -> bool: + print("TEST_LAB_RESOURCES: FAIL (%s)" % message) + quit(1) + return true