Charge more for each berth, and show the bill

#220: every berth past the first paid one costs 1.12x the one before it, on
top of the flat per-vessel charge. A linear bill makes the tenth hull no more
of a decision than the second; compounding weighs each addition against the
whole fleet behind it. Shallow on purpose - the thirteenth berth costs about
three and a half times the second, the twentieth about seven and a half.

Fleet Offices gain a menu naming the whole bill per minute, a line per paying
berth with its multiplier, and the arithmetic in words. It renders
fleet_upkeep_breakdown(), which is the calculation the charging loop itself
runs: a readout that summed independently could quote a bill the game does
not take, which is the thing worth preventing here. The test pins that what
the menu quotes for a berth is what that office is charged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
Loïc Wisniewski 2026-09-05 22:36:21 +02:00
commit 2bb46cba94
9 changed files with 369 additions and 5 deletions

View file

@ -776,6 +776,16 @@ signal fleet_upkeep_failed(slot_index: int, vessel_type: String)
# 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
# #220: and each berth costs more than the one before it. A flat per-vessel
# charge means a big fleet is only linearly harder to keep, so the tenth hull
# is no more of a decision than the second; compounding makes every addition
# weigh against the whole fleet behind it.
#
# Deliberately shallow - the ticket asks for a slow one. At 1.12 the thirteenth
# berth costs about three and a half times the second, and the twentieth about
# seven and a half. A steeper rate reaches "unaffordable" while the player is
# still learning what the fleet is for.
const FLEET_UPKEEP_GROWTH: float = 1.12
# Offices in the order they were built, which is the order they are matched to
@ -800,16 +810,62 @@ func supplied_fleet_office_count() -> int:
# 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:
# What one berth costs per charge. `slot_index` is the fleet position it
# keeps, so the first berth past the free one pays the base rate and every
# berth after it pays the compounded one (#220).
func fleet_office_upkeep(vessel_type: String, slot_index: int = BASE_FLEET_SIZE) -> Dictionary:
var upkeep: Dictionary = {}
var scale: float = fleet_upkeep_scale(slot_index)
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)))
/ float(FLEET_OFFICE_UPKEEP_DIVISOR) * scale))
if share > 0:
upkeep[resource_type] = share
return upkeep
# The multiplier on a berth at this fleet position. The free hull and the
# first paid berth both sit at 1.0, so the curve starts where the charging
# starts rather than partway up it.
func fleet_upkeep_scale(slot_index: int) -> float:
return pow(FLEET_UPKEEP_GROWTH, maxi(slot_index - BASE_FLEET_SIZE, 0))
# The whole bill, berth by berth, for the readout #220 asks for: what each
# one is keeping, what it is charged, and what that is per minute. Computed
# here rather than in the menu so the numbers shown are the numbers charged -
# a readout that recomputes them separately can disagree with the loop that
# actually takes the resources.
func fleet_upkeep_breakdown() -> Dictionary:
var berths: Array = []
var per_minute: Dictionary = {}
var charges_per_minute: float = 60.0 / FLEET_OFFICE_UPKEEP_SECONDS
var offices: Array = fleet_offices()
for i in range(offices.size()):
var slot_index: int = i + BASE_FLEET_SIZE
var vessel_type: String = fleet_office_vessel(slot_index)
if vessel_type == "":
continue # a berth standing empty is charged nothing
var costs: Dictionary = fleet_office_upkeep(vessel_type, slot_index)
berths.append({
"slot": slot_index + 1,
"vessel_type": vessel_type,
"scale": fleet_upkeep_scale(slot_index),
"costs": costs,
"starved": bool(offices[i].get("starved", false)),
})
for resource_type in costs:
per_minute[resource_type] = float(per_minute.get(resource_type, 0.0)) \
+ float(costs[resource_type]) * charges_per_minute
return {
"berths": berths,
"per_minute": per_minute,
"charge_seconds": FLEET_OFFICE_UPKEEP_SECONDS,
"divisor": FLEET_OFFICE_UPKEEP_DIVISOR,
"growth": FLEET_UPKEEP_GROWTH,
}
# 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:
@ -841,7 +897,8 @@ func _advance_fleet_office_upkeep(delta: float) -> void:
office.upkeep_progress = progress
continue
office.upkeep_progress = progress - FLEET_OFFICE_UPKEEP_SECONDS
var upkeep: Dictionary = fleet_office_upkeep(vessel_type)
var upkeep: Dictionary = fleet_office_upkeep(vessel_type,
i + BASE_FLEET_SIZE)
var inventory: RefCounted = _inventory_for_record(office)
var affordable: bool = true
for resource_type in upkeep: