Add research lab planets and research points (#63)
This commit is contained in:
parent
651f8b787e
commit
3f3a22227d
43 changed files with 459 additions and 30 deletions
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
97
GameState.gd
97
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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
Main.tscn
12
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
42
PlanetSetups/ResearchLabSetup.tres
Normal file
42
PlanetSetups/ResearchLabSetup.tres
Normal file
|
|
@ -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")])
|
||||
|
|
@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
15
ResearchLabPlanetDefinition.tres
Normal file
15
ResearchLabPlanetDefinition.tres
Normal file
|
|
@ -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
|
||||
55
ResearchLabPlanetVisual.gd
Normal file
55
ResearchLabPlanetVisual.gd
Normal file
|
|
@ -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)
|
||||
6
ResearchLabPlanetVisual.tscn
Normal file
6
ResearchLabPlanetVisual.tscn
Normal file
|
|
@ -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")
|
||||
12
ResearchLabSurfaceProfile.tres
Normal file
12
ResearchLabSurfaceProfile.tres
Normal file
|
|
@ -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)
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
143
tests/test_research_points.gd
Normal file
143
tests/test_research_points.gd
Normal file
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue