extends Node # Autoload singleton - the one thing that survives change_scene_to_file() # scene swaps between space (Main.tscn) and a planet's surface # (PlanetSurface.tscn). Everything else in either scene gets torn down # and rebuilt from scratch on each swap, so anything that needs to # survive the trip has to live here instead. var has_return_position: bool = false var surviving_ship_count: int = 5 var return_world_position: Vector2 = Vector2.ZERO var return_camera_zoom: float = 1.0 var planet_name: String = "" var planet_type: String = "" # 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 # player lands on and leaves a planet. var explored_chunks: Dictionary = {} # --- Resources ------------------------------------------------------------- # Rocks mined from asteroid-planet surfaces, spent on buildings and ships. # Starts stocked for play-testing so buildings can be tried without mining # first - drop this back to 0 for a real playthrough. var rocks: int = 500 const MOTHERSHIP_COST: int = 20 const SHIP_CRAFTER_COST: int = 12 const SHIP_COST: int = 6 # --- Buildings ------------------------------------------------------------- # Player-placed structures, as [{"type": String, "position": Vector2}]. # Buildings exist only in space (Main.tscn) but are stored here so they # survive the scene swap to a planet's surface and back. const BUILDING_MOTHERSHIP: String = "mothership" const BUILDING_SHIP_CRAFTER: String = "ship_crafter" const BUILDING_COMMAND_CENTER: String = "command_center" # Extractors are not placed in space like the others - they are built ON a # planet's surface and tracked separately (see `extractors`). const BUILDING_EXTRACTOR: String = "extractor" const COMMAND_CENTER_COST: int = 15 const EXTRACTOR_COST: int = 25 var buildings: Array = [] # --- Skill tree ------------------------------------------------------------ # Researched at the mothership. Two things hang off it: which structures the # player is allowed to build at all, and permanent upgrades that have no # building of their own. The mothership itself is deliberately NOT in the # tree - it is the thing you research AT, so gating it behind research would # be a deadlock. # # `x` is a horizontal position on a 0..1 grid; the vertical position is NOT # stored, it is derived from how many prerequisites deep the node is, so the # 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). const SKILL_TREE: Array = [ { "id": "shipyards", "name": "Shipyards", "cost": 15, "requires": "", "x": 0.5, "unlocks": BUILDING_SHIP_CRAFTER, "desc": "Build ship crafters, and buy extra vessels at them.", }, { "id": "logistics", "name": "Logistics", "cost": 25, "requires": "shipyards", "x": 0.18, "unlocks": BUILDING_EXTRACTOR, "desc": "Build extractors on planets. They mine ore whether or not you are nearby.", }, { "id": "coordination", "name": "Coordination", "cost": 20, "requires": "shipyards", "x": 0.82, "unlocks": BUILDING_COMMAND_CENTER, "desc": "Build command centers, where the fleet formation is arranged.", }, { "id": "wing", "name": "Wing Doctrine", "cost": 30, "requires": "coordination", "x": 0.82, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "squadron", "name": "Squadron Doctrine", "cost": 70, "requires": "wing", "x": 0.65, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, { "id": "armada", "name": "Armada Doctrine", "cost": 150, "requires": "squadron", "x": 0.3, "fleet_bonus": 1, "desc": "Permanent: +1 vessel in the fleet.", }, ] const BASE_FLEET_SIZE: int = 5 var unlocked: Array = [] func skill_node(id: String) -> Dictionary: for node in SKILL_TREE: if node.id == id: return node return {} func skill_depth(id: String) -> int: # How many prerequisites deep this node sits. Drives its height in the # tree view: roots at the bottom, everything that depends on something # above it. var depth: int = 0 var node: Dictionary = skill_node(id) while not node.is_empty() and node.requires != "": depth += 1 node = skill_node(node.requires) if depth > SKILL_TREE.size(): break # a cycle in the data would otherwise hang here return depth func max_skill_depth() -> int: var deepest: int = 0 for node in SKILL_TREE: deepest = maxi(deepest, skill_depth(node.id)) return deepest func is_unlocked(id: String) -> bool: return id in unlocked func prerequisite_met(id: String) -> bool: var node: Dictionary = skill_node(id) if node.is_empty(): return false return node.requires == "" or is_unlocked(node.requires) func can_unlock(id: String) -> bool: return not is_unlocked(id) and prerequisite_met(id) and can_afford(int(skill_node(id).cost)) func unlock(id: String) -> bool: if not can_unlock(id): return false if not spend(int(skill_node(id).cost)): return false unlocked.append(id) return true func is_building_unlocked(type: String) -> bool: # The mothership is always buildable: it is where research happens, so # locking it would leave a player with no mothership unable to ever get # one. if type == BUILDING_MOTHERSHIP: return true for node in SKILL_TREE: if node.get("unlocks", "") == type: return is_unlocked(node.id) return true func research_for_building(type: String) -> String: for node in SKILL_TREE: if node.get("unlocks", "") == type: return node.name return "" func max_fleet_size() -> int: var n: int = BASE_FLEET_SIZE for id in unlocked: n += int(skill_node(id).get("fleet_bonus", 0)) return mini(n, DEFAULT_FORMATION.size()) func reset_progression() -> void: unlocked.clear() func add_building(type: String, at_position: Vector2) -> void: buildings.append({"type": type, "position": at_position}) func has_mothership() -> bool: return get_mothership_position() != null func remove_buildings_of_type(type: String) -> int: var kept: Array = [] var removed: int = 0 for b in buildings: if b.type == type: removed += 1 else: kept.append(b) buildings = kept return removed func get_mothership_position() -> Variant: # Where a wiped-out fleet gets rebuilt. Returns null when no mothership # has been built yet, so the caller can fall back to the world origin. for b in buildings: if b.type == BUILDING_MOTHERSHIP: return b.position return null # --- Fleet formation ------------------------------------------------------- # Where each vessel sits relative to the fleet centre, editable at a command # centre. Lives here (not as a const on Fleet) so a layout the player has # arranged survives the trip to a planet's surface and back, and so a # rebuilt fleet keeps their arrangement. # Slots beyond BASE_FLEET_SIZE only become usable once the matching fleet # capacity upgrade is researched, but they are defined up front so the wedge # keeps a deliberate shape as it grows rather than having positions invented # at unlock time. const DEFAULT_FORMATION: Array = [ Vector2(0, -48), Vector2(-30, -8), Vector2(30, -8), Vector2(-60, 32), Vector2(60, 32), Vector2(-90, 72), Vector2(90, 72), Vector2(0, 56), ] var formation: Array = DEFAULT_FORMATION.duplicate() func reset_formation() -> void: formation = DEFAULT_FORMATION.duplicate() func rotate_formation(radians: float) -> void: # Pivots every slot about the fleet centre, keeping the shape intact # and only changing which way it faces. for i in range(formation.size()): formation[i] = formation[i].rotated(radians) # --- Extractors and their haulers ----------------------------------------- # An extractor sits on a planet and runs a single automated hauler back and # forth to the mothership, delivering ore on each arrival. # # The round trip is SIMULATED HERE, in the autoload, rather than by any # node: it advances whether the player is in space, on a planet's surface, # or far enough away that nothing about the route is loaded. `progress` is # a fraction of one full there-and-back cycle, so a hauler's drawn position # is derived from the same number that decides when ore is delivered - the # visual arrival and the payout cannot drift apart, because they are the # same event. # # Entries are {"position": Vector2 (the planet's position in SPACE), # "planet_name": String, "progress": float}. const HAULER_SPEED: float = 260.0 const HAULER_YIELD: int = 4 # Progress value at which the hauler reaches the mothership: exactly half # way through the cycle, the outbound leg being the first half. const HAULER_ARRIVAL: float = 0.5 var extractors: Array = [] func has_extractor_at(planet_name: String) -> bool: for e in extractors: if e.planet_name == planet_name: return true return false func add_extractor(planet_name: String, at_position: Vector2) -> void: extractors.append({ "position": at_position, "planet_name": planet_name, "progress": 0.0, }) func get_hauler_route_time(extractor: Dictionary) -> float: # Full cycle: out to the mothership and back again. var mothership: Variant = get_mothership_position() if mothership == null: return 0.0 var distance: float = extractor.position.distance_to(mothership) if distance < 1.0: return 0.0 return (distance * 2.0) / HAULER_SPEED func get_hauler_position(extractor: Dictionary) -> Variant: # Where the hauler is right now, derived purely from `progress`. Returns # null when there is no route (no mothership built yet). var mothership: Variant = get_mothership_position() if mothership == null: return null var t: float = extractor.progress if t <= HAULER_ARRIVAL: return extractor.position.lerp(mothership, t / HAULER_ARRIVAL) return mothership.lerp(extractor.position, (t - HAULER_ARRIVAL) / (1.0 - HAULER_ARRIVAL)) func _process(delta: float) -> void: # Runs in the autoload, so extraction keeps earning while the player is # on a planet's surface or nowhere near the route. if extractors.is_empty(): return for e in extractors: var cycle: float = get_hauler_route_time(e) if cycle <= 0.0: continue # nothing to deliver to yet var before: float = e.progress e.progress += delta / cycle # Ore lands the instant the hauler reaches the mothership - the same # instant get_hauler_position() puts it there. if before < HAULER_ARRIVAL and e.progress >= HAULER_ARRIVAL: rocks += HAULER_YIELD while e.progress >= 1.0: e.progress -= 1.0 func can_afford(cost: int) -> bool: return rocks >= cost func spend(cost: int) -> bool: if not can_afford(cost): return false rocks -= cost return true