Buy fleet berths with Fleet Offices
#219: fleet capacity is a built, supplied thing rather than a researched one. The eight fleet-size doctrines are gone; capacity is one berth free plus one per Fleet Office standing. An office charges every 25s for the vessel in its berth, priced off that vessel's own build cost so an expensive hull is expensive to keep, and drawn from its own store the way a gas burner draws gas. An office that cannot pay goes dark, its vessel is scuttled, and the player is told through the same queue quest lines use - which reaches them on a planet surface too. An empty berth is idle rather than starved: an office is only failing while there is something in it to fail, and holding it disabled would leave a berth that could never be crewed and so could never be paid for again. Ships have no hit points and a shield absorbs a hit wholesale, so the lost vessel is scuttled rather than damaged - otherwise a shielded hull would survive its own eviction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
6e13b6397c
commit
1f5fecd371
60 changed files with 695 additions and 243 deletions
|
|
@ -472,6 +472,8 @@ const BUILDING_SHIP_LAB: String = "ship_lab"
|
|||
# Converts gas into energy, both stored/moved through the building's own
|
||||
# inventory exactly like every other building (#54).
|
||||
const BUILDING_GAS_BURNER: String = "gas_burner"
|
||||
# #219: the structure that pays for a berth in the fleet.
|
||||
const BUILDING_FLEET_OFFICE: String = "fleet_office"
|
||||
# A map-revealing structure, not a passive sensor bonus. Its menu searches
|
||||
# the same deterministic WorldGen catalogue that PlanetField spawns from.
|
||||
const BUILDING_PLANET_SCANNER: String = "planet_scanner"
|
||||
|
|
@ -597,89 +599,18 @@ const SKILL_TREE: Array = [
|
|||
"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.",
|
||||
},
|
||||
{
|
||||
# Cheaper than it was (30 ore / 1 point): the fleet now starts at
|
||||
# BASE_FLEET_SIZE 3 rather than 5, so the first two doctrines are no
|
||||
# longer a mid-game luxury but the ordinary way out of the opening.
|
||||
# Pricing them as a luxury made the early game feel stuck.
|
||||
"id": "wing", "name": "Wing Doctrine", "cost": 15, "requires": "coordination",
|
||||
"x": 0.82, "fleet_bonus": 1, "research_points": 1,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +1 vessel in the fleet.",
|
||||
},
|
||||
{
|
||||
# #70: was x=0.50 - the same lane freight's branch already occupies at
|
||||
# this same depth (power_systems), so the two nodes were drawn
|
||||
# directly on top of each other. Kept in line with wing (its own
|
||||
# prerequisite, also x=0.82) instead, so the whole fleet-size
|
||||
# doctrine branch now runs straight rather than zigzagging into
|
||||
# someone else's lane.
|
||||
# Also cut (70 ore / 2 points), for the same reason as wing.
|
||||
"id": "squadron", "name": "Squadron Doctrine", "cost": 35, "requires": "wing",
|
||||
"costs": {"ore": 35, "gas": 12},
|
||||
"x": 0.82, "fleet_bonus": 1, "research_points": 1,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +1 vessel in the fleet.",
|
||||
},
|
||||
{
|
||||
"id": "armada", "name": "Armada Doctrine", "cost": 90, "requires": "squadron",
|
||||
"costs": {"ore": 90, "gas": 30, "energy": 15},
|
||||
"x": 0.82, "fleet_bonus": 1, "research_points": 2,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +1 vessel in the fleet.",
|
||||
},
|
||||
{
|
||||
# The last two tiers grant +2 rather than +1. Seven single-vessel
|
||||
# nodes in one lane would be a long, monotonous chain for a branch
|
||||
# that only ever does one thing; two larger, costlier steps carry the
|
||||
# ceiling from 6 to 10 while keeping the branch a readable length.
|
||||
"id": "battlegroup", "name": "Battlegroup Doctrine", "cost": 180, "requires": "armada",
|
||||
"costs": {"ore": 180, "gas": 60, "energy": 40},
|
||||
"x": 0.82, "fleet_bonus": 2, "research_points": 4,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +2 vessels in the fleet.",
|
||||
},
|
||||
{
|
||||
"id": "grand_fleet", "name": "Grand Fleet Doctrine", "cost": 320, "requires": "battlegroup",
|
||||
"costs": {"ore": 320, "gas": 110, "energy": 80, "dark_matter": 20},
|
||||
"x": 0.82, "fleet_bonus": 2, "research_points": 6,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +2 vessels in the fleet.",
|
||||
},
|
||||
{
|
||||
# #206 carries the branch on to twenty. The step size keeps growing
|
||||
# for the reason the +2 tiers were introduced: ten more single-vessel
|
||||
# nodes would be a very long chain for a lane that only ever does one
|
||||
# thing. These three cost late-game money and are meant to be the
|
||||
# thing a finished economy is finally FOR.
|
||||
"id": "task_force", "name": "Task Force Doctrine", "cost": 520, "requires": "grand_fleet",
|
||||
"costs": {"ore": 520, "gas": 180, "energy": 130, "dark_matter": 35},
|
||||
"x": 0.82, "fleet_bonus": 3, "research_points": 8,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +3 vessels in the fleet.",
|
||||
},
|
||||
{
|
||||
"id": "star_legion", "name": "Star Legion Doctrine", "cost": 820, "requires": "task_force",
|
||||
"costs": {"ore": 820, "gas": 280, "energy": 200, "dark_matter": 60},
|
||||
"x": 0.82, "fleet_bonus": 3, "research_points": 10,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +3 vessels in the fleet.",
|
||||
},
|
||||
{
|
||||
"id": "void_armada", "name": "Void Armada Doctrine", "cost": 1300, "requires": "star_legion",
|
||||
"costs": {"ore": 1300, "gas": 440, "energy": 320, "dark_matter": 100},
|
||||
"x": 0.82, "fleet_bonus": 4, "research_points": 14,
|
||||
"icon": 2,
|
||||
"desc": "Permanent: +4 vessels in the fleet. The fleet reaches twenty.",
|
||||
},
|
||||
]
|
||||
|
||||
# How many SLOTS the fleet has before any doctrine is researched. Distinct
|
||||
# from STARTING_SHIP_COUNT below: this is capacity, that is how many vessels
|
||||
# are actually flying. The two used to be one constant, which forced a new
|
||||
# game to open with a full fleet and left the first crafter with nothing to
|
||||
# do - there was no room to put a vessel until a doctrine was researched.
|
||||
const BASE_FLEET_SIZE: int = 3
|
||||
# How many SLOTS the fleet has before a single Fleet Office is built.
|
||||
#
|
||||
# #219 replaced the fleet-size doctrines with a structure that has to be
|
||||
# supplied: every berth past the first is a Fleet Office standing somewhere,
|
||||
# paying for the vessel in it. So this is 1 - the hull the survivors came out
|
||||
# of the breach with, which the ticket exempts from needing an office at all.
|
||||
#
|
||||
# Distinct from STARTING_SHIP_COUNT below: this is capacity, that is how many
|
||||
# vessels are actually flying.
|
||||
const BASE_FLEET_SIZE: int = 1
|
||||
# A new game opens with a SINGLE vessel. Building the fleet up is the early
|
||||
# game, so the two empty slots are the first thing a ship crafter is for.
|
||||
const STARTING_SHIP_COUNT: int = 1
|
||||
|
|
@ -812,13 +743,109 @@ func research_for_building(type: String) -> String:
|
|||
return "" if id == "" else String(skill_node(id).name)
|
||||
|
||||
|
||||
# #219: every berth past the first is a Fleet Office standing somewhere, and
|
||||
# an office only counts while it can still pay for the vessel in it. A starved
|
||||
# one has already had its vessel destroyed; leaving it counting would let the
|
||||
# player rebuild straight into a berth nothing is supplying.
|
||||
signal fleet_upkeep_failed(slot_index: int, vessel_type: String)
|
||||
|
||||
# How often an office is charged, and how much of the vessel's own build price
|
||||
# it charges. Derived from the build cost rather than authored per vessel: a
|
||||
# hull that was expensive to make is expensive to keep, which is the rule the
|
||||
# ticket asks for and needs no second table to fall out of step with.
|
||||
const FLEET_OFFICE_UPKEEP_SECONDS: float = 25.0
|
||||
const FLEET_OFFICE_UPKEEP_DIVISOR: int = 8
|
||||
|
||||
|
||||
# Offices in the order they were built, which is the order they are matched to
|
||||
# the fleet: the first vessel needs none, the second is kept by the first
|
||||
# office, and so on.
|
||||
func fleet_offices() -> Array:
|
||||
var offices: Array = []
|
||||
for building in buildings:
|
||||
if String(building.get("type", "")) == BUILDING_FLEET_OFFICE:
|
||||
offices.append(building)
|
||||
return offices
|
||||
|
||||
|
||||
func supplied_fleet_office_count() -> int:
|
||||
var n: int = 0
|
||||
for office in fleet_offices():
|
||||
if not bool(office.get("starved", false)):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# What one berth costs per charge, for the vessel actually sitting in it. An
|
||||
# empty answer means this vessel is free to keep - which is true of nothing
|
||||
# the crafter builds, and is what an unknown type falls back to.
|
||||
func fleet_office_upkeep(vessel_type: String) -> Dictionary:
|
||||
var upkeep: Dictionary = {}
|
||||
for resource_type in product_costs(vessel_type):
|
||||
var share: int = int(ceil(float(product_costs(vessel_type)[resource_type])
|
||||
/ float(FLEET_OFFICE_UPKEEP_DIVISOR)))
|
||||
if share > 0:
|
||||
upkeep[resource_type] = share
|
||||
return upkeep
|
||||
|
||||
|
||||
# The vessel an office is answerable for, or "" when the fleet is smaller than
|
||||
# the berths built for it - an office with nothing in it costs nothing.
|
||||
func fleet_office_vessel(slot_index: int) -> String:
|
||||
if slot_index < 0 or slot_index >= fleet_vessel_types.size():
|
||||
return ""
|
||||
return String(fleet_vessel_types[slot_index])
|
||||
|
||||
|
||||
func _advance_fleet_office_upkeep(delta: float) -> void:
|
||||
var offices: Array = fleet_offices()
|
||||
for i in range(offices.size()):
|
||||
var office: Dictionary = offices[i]
|
||||
# Office i keeps the (i + 1)th vessel: the first one needs no office.
|
||||
var vessel_type: String = fleet_office_vessel(i + BASE_FLEET_SIZE)
|
||||
if vessel_type == "":
|
||||
# Nothing to keep. The clock is not advanced either, so a berth
|
||||
# that stood empty does not present a bill the moment it is used.
|
||||
#
|
||||
# An empty berth is IDLE, not starved, including one that just
|
||||
# lost its vessel: an office is only failing while there is
|
||||
# something in it to fail. Holding it disabled would leave the
|
||||
# player unable to use the berth they paid for, with no way back
|
||||
# - it can never pay for a vessel it is not allowed to hold.
|
||||
office.upkeep_progress = 0.0
|
||||
office.starved = false
|
||||
continue
|
||||
var progress: float = float(office.get("upkeep_progress", 0.0)) + delta
|
||||
if progress < FLEET_OFFICE_UPKEEP_SECONDS:
|
||||
office.upkeep_progress = progress
|
||||
continue
|
||||
office.upkeep_progress = progress - FLEET_OFFICE_UPKEEP_SECONDS
|
||||
var upkeep: Dictionary = fleet_office_upkeep(vessel_type)
|
||||
var inventory: RefCounted = _inventory_for_record(office)
|
||||
var affordable: bool = true
|
||||
for resource_type in upkeep:
|
||||
if inventory.amount(String(resource_type)) < int(upkeep[resource_type]):
|
||||
affordable = false
|
||||
break
|
||||
if affordable:
|
||||
for resource_type in upkeep:
|
||||
inventory.withdraw(String(resource_type), int(upkeep[resource_type]))
|
||||
# A restocked office goes back to work on its own: the player
|
||||
# fixing their supply line should not also have to rebuild it.
|
||||
office.starved = false
|
||||
continue
|
||||
office.starved = true
|
||||
say_quest_line(("A Fleet Office has run dry. The %s berthed there is gone - "
|
||||
+ "nothing is reaching it, and a berth nobody supplies keeps nobody.")
|
||||
% vessel_name(vessel_type))
|
||||
fleet_upkeep_failed.emit(i + BASE_FLEET_SIZE, vessel_type)
|
||||
|
||||
|
||||
func max_fleet_size() -> int:
|
||||
if intro_fleet_size > 0:
|
||||
return mini(intro_fleet_size, DEFAULT_FORMATION.size())
|
||||
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())
|
||||
return mini(BASE_FLEET_SIZE + supplied_fleet_office_count(),
|
||||
DEFAULT_FORMATION.size())
|
||||
|
||||
|
||||
func reset_progression() -> void:
|
||||
|
|
@ -3071,6 +3098,7 @@ func _process(delta: float) -> void:
|
|||
_advance_planning()
|
||||
_advance_repairs(delta)
|
||||
_advance_resource_converters(delta)
|
||||
_advance_fleet_office_upkeep(delta)
|
||||
_advance_distorder_drain(delta, _distorder_boost_in_use())
|
||||
_advance_quests()
|
||||
for e in extractors:
|
||||
|
|
|
|||
Loading…
Reference in a new issue