#102/#103 (#97d/#97e): the outpost, and the garrison you compose for it
Landed together because an outpost you cannot crew is decoration, and a garrison menu with no building to hang it off is nothing at all. #102, the building. Researched at "Forward Bases", two prerequisites deep and cheaper than any mid-game structure - the ticket asks for early, and the answer to waves attacking your buildings should arrive at about the time the waves can hurt them. It is drawn as a watch mast on a tripod with two open berths, deliberately the least machinery of any structure here: it makes nothing and refines nothing, it holds a position. 120hp, more than a crafter, because being shot at is its job. #103, the garrison. Two separate things live on its record, and conflating them was the first thing to get wrong: the ORDER, what the player asked for, and the CREW, who is actually there. This menu edits the order only; #104 fills the crew. Keeping the order as a wish is what lets an outpost ask for a hull nobody has built yet and wait for it, rather than the request evaporating - and it is why the panel shows "here / ordered" instead of one number. "Only ships that attack can be assigned" is expressed as capabilities rather than a list of types, so a new armed vessel is eligible the day it is added and an unarmed one never is by accident. Today that means the interceptor and the laser vessel; a shield, a hauler and an engine block cannot hold a position however much one might want them to. Two berths by default, and Garrison Bays raises it at outposts ALREADY standing - the same shape as freight_yards for hubs, because a bonus that only reached new buildings would mean demolishing and rebuilding to collect it. The panel builds its own rows from the catalog rather than from nodes placed in the scene, so adding an armed vessel later needs no scene surgery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3debcd7edd
commit
3c632276af
12 changed files with 909 additions and 1 deletions
|
|
@ -1,11 +1,12 @@
|
||||||
class_name BuildingDefinitions
|
class_name BuildingDefinitions
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "extractor", "transport_hub", "ship_lab", "gas_burner"]
|
const ORDER: PackedStringArray = ["mothership", "ship_crafter", "command_center", "outpost", "extractor", "transport_hub", "ship_lab", "gas_burner"]
|
||||||
const DEFINITIONS: Dictionary = {
|
const DEFINITIONS: Dictionary = {
|
||||||
"mothership": preload("res://buildings/MothershipBuildingDefinition.tres"),
|
"mothership": preload("res://buildings/MothershipBuildingDefinition.tres"),
|
||||||
"ship_crafter": preload("res://buildings/CrafterBuildingDefinition.tres"),
|
"ship_crafter": preload("res://buildings/CrafterBuildingDefinition.tres"),
|
||||||
"command_center": preload("res://buildings/CommandBuildingDefinition.tres"),
|
"command_center": preload("res://buildings/CommandBuildingDefinition.tres"),
|
||||||
|
"outpost": preload("res://buildings/OutpostBuildingDefinition.tres"),
|
||||||
"extractor": preload("res://buildings/ExtractorBuildingDefinition.tres"),
|
"extractor": preload("res://buildings/ExtractorBuildingDefinition.tres"),
|
||||||
"transport_hub": preload("res://buildings/TransportHubBuildingDefinition.tres"),
|
"transport_hub": preload("res://buildings/TransportHubBuildingDefinition.tres"),
|
||||||
"ship_lab": preload("res://buildings/ShipLabBuildingDefinition.tres"),
|
"ship_lab": preload("res://buildings/ShipLabBuildingDefinition.tres"),
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,22 @@ const TRANSPORT_HUB_COLOR: Color = Color(0.95, 0.55, 0.85)
|
||||||
const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9)
|
const SHIP_LAB_COLOR: Color = Color(0.55, 0.95, 0.9)
|
||||||
const GAS_BURNER_COLOR: Color = Color(0.95, 0.35, 0.15)
|
const GAS_BURNER_COLOR: Color = Color(0.95, 0.35, 0.15)
|
||||||
const GAS_BURNER_ENERGY_COLOR: Color = Color(0.85, 0.95, 0.15)
|
const GAS_BURNER_ENERGY_COLOR: Color = Color(0.85, 0.95, 0.15)
|
||||||
|
# #102. Olive rather than another blue or amber: every existing structure is
|
||||||
|
# a place you go to DO something, and this is the one you leave people at.
|
||||||
|
const OUTPOST_COLOR: Color = Color(0.72, 0.86, 0.45)
|
||||||
|
|
||||||
|
# Outpost: a watch mast on a tripod footing, with two open berths beside it
|
||||||
|
# where a garrison sits. Deliberately the least machinery of any structure
|
||||||
|
# here - it makes nothing and refines nothing, it just holds a position -
|
||||||
|
# and deliberately not radial, so it cannot be mistaken for a small hub.
|
||||||
|
const OUTPOST_MAST_TOP: float = -84.0
|
||||||
|
const OUTPOST_MAST_FOOT: float = 46.0
|
||||||
|
const OUTPOST_HEAD_RADIUS: float = 20.0
|
||||||
|
const OUTPOST_FOOT_SPAN: float = 62.0
|
||||||
|
const OUTPOST_FOOT_DROP: float = 26.0
|
||||||
|
const OUTPOST_BERTH_X: float = 74.0
|
||||||
|
const OUTPOST_BERTH_RADIUS: float = 22.0
|
||||||
|
const OUTPOST_VANE_COUNT: int = 3
|
||||||
|
|
||||||
# Ship lab: a test frame with a vessel held in it and instruments sweeping
|
# Ship lab: a test frame with a vessel held in it and instruments sweeping
|
||||||
# over it. Reads as a workshop rather than a factory - the crafter already
|
# over it. Reads as a workshop rather than a factory - the crafter already
|
||||||
|
|
@ -359,6 +375,47 @@ func _draw_command_center() -> void:
|
||||||
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9)
|
NeonDraw.draw_glowing_polygon(self, _diamond(Vector2.ZERO, CMD_SCREEN_RADIUS), readout, 0.9)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_outpost() -> void:
|
||||||
|
var c: Color = OUTPOST_COLOR
|
||||||
|
var lit: Color = Color(0.94, 0.99, 0.82)
|
||||||
|
|
||||||
|
# Tripod footing.
|
||||||
|
var foot: Vector2 = Vector2(0.0, OUTPOST_MAST_FOOT)
|
||||||
|
for side in [-1.0, 0.0, 1.0]:
|
||||||
|
var toe: Vector2 = Vector2(OUTPOST_FOOT_SPAN * side, OUTPOST_MAST_FOOT + OUTPOST_FOOT_DROP)
|
||||||
|
NeonDraw.draw_glowing_line(self, foot, toe, c, 0.7, 0.42)
|
||||||
|
NeonDraw.draw_glowing_line(self,
|
||||||
|
Vector2(-OUTPOST_FOOT_SPAN, OUTPOST_MAST_FOOT + OUTPOST_FOOT_DROP),
|
||||||
|
Vector2(OUTPOST_FOOT_SPAN, OUTPOST_MAST_FOOT + OUTPOST_FOOT_DROP), c, 0.55, 0.34)
|
||||||
|
|
||||||
|
# The mast, with rungs so it reads as climbable structure rather than a
|
||||||
|
# drawn line.
|
||||||
|
var top: Vector2 = Vector2(0.0, OUTPOST_MAST_TOP)
|
||||||
|
NeonDraw.draw_glowing_line(self, foot, top, c, 0.9, 0.5)
|
||||||
|
for i in range(4):
|
||||||
|
var y: float = lerpf(OUTPOST_MAST_FOOT, OUTPOST_MAST_TOP + OUTPOST_HEAD_RADIUS,
|
||||||
|
float(i + 1) / 5.0)
|
||||||
|
NeonDraw.draw_glowing_line(self, Vector2(-11.0, y), Vector2(11.0, y), c, 0.45, 0.28)
|
||||||
|
|
||||||
|
# Sensor head: a lit diamond inside a cage, with vanes looking outward.
|
||||||
|
NeonDraw.draw_glowing_polygon(self, _ring(top, OUTPOST_HEAD_RADIUS, 6, PI / 6.0), c, 1.0)
|
||||||
|
NeonDraw.draw_glowing_polygon(self, _diamond(top, OUTPOST_HEAD_RADIUS * 0.5), lit, 0.95)
|
||||||
|
for i in range(OUTPOST_VANE_COUNT):
|
||||||
|
var a: float = PI + (float(i) / float(OUTPOST_VANE_COUNT - 1)) * PI
|
||||||
|
var dir: Vector2 = Vector2(cos(a), sin(a))
|
||||||
|
NeonDraw.draw_glowing_line(self, top + dir * OUTPOST_HEAD_RADIUS,
|
||||||
|
top + dir * (OUTPOST_HEAD_RADIUS + 16.0), c, 0.7, 0.3)
|
||||||
|
|
||||||
|
# Two open berths. Open, not enclosed: what sits in them belongs to the
|
||||||
|
# player and flies away, so they are cradles rather than hangars.
|
||||||
|
for side in [-1.0, 1.0]:
|
||||||
|
var berth: Vector2 = Vector2(OUTPOST_BERTH_X * side, 4.0)
|
||||||
|
NeonDraw.draw_glowing_path(self, _arc(berth, OUTPOST_BERTH_RADIUS,
|
||||||
|
-PI * 0.5, PI * 0.5, 10), c, 0.75, 0.36)
|
||||||
|
NeonDraw.draw_glowing_line(self, Vector2(12.0 * side, 4.0),
|
||||||
|
berth - Vector2(OUTPOST_BERTH_RADIUS * side, 0.0), c, 0.6, 0.34)
|
||||||
|
|
||||||
|
|
||||||
# --- mothership: a radial flagship ----------------------------------------
|
# --- mothership: a radial flagship ----------------------------------------
|
||||||
# Not a big fighter. A mothership is the thing everything else orbits, so it
|
# Not a big fighter. A mothership is the thing everything else orbits, so it
|
||||||
# is built radially around a reactor core rather than pointed in a direction:
|
# is built radially around a reactor core rather than pointed in a direction:
|
||||||
|
|
|
||||||
20
buildings/OutpostBuildingDefinition.tres
Normal file
20
buildings/OutpostBuildingDefinition.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[gd_resource type="Resource" script_class="BuildingDefinition" load_steps=3 format=3]
|
||||||
|
[ext_resource type="Script" path="res://buildings/BuildingDefinition.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" path="res://buildings/OutpostBuildingVisual.tscn" id="2"]
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
definition_id = "outpost"
|
||||||
|
display_name = "Outpost"
|
||||||
|
description = "A forward position for the fleet to garrison. Vessels stationed here hold station and fight what comes at them, without you flying back."
|
||||||
|
cost = 30
|
||||||
|
action_icon = 40
|
||||||
|
menu_icon = 40
|
||||||
|
visual_scene = ExtResource("2")
|
||||||
|
capabilities = PackedStringArray("garrison")
|
||||||
|
map_color = Color(0.72, 0.86, 0.45, 1)
|
||||||
|
map_sides = 3
|
||||||
|
map_radius = 7.0
|
||||||
|
menu_button_name = "OutpostButton"
|
||||||
|
menu_label_name = "OutpostLabel"
|
||||||
|
menu_cost_name = "OutpostCost"
|
||||||
|
max_hp = 120
|
||||||
13
buildings/OutpostBuildingVisual.gd
Normal file
13
buildings/OutpostBuildingVisual.gd
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
extends "res://buildings/BuildingVisualBase.gd"
|
||||||
|
|
||||||
|
# Drawn once and never redrawn, like the command centre: nothing about a
|
||||||
|
# watch mast animates, and per-frame redraws of art that never changes were
|
||||||
|
# a real framerate cost earlier on.
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_process(false)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
register_draw()
|
||||||
|
_draw_outpost()
|
||||||
11
buildings/OutpostBuildingVisual.tscn
Normal file
11
buildings/OutpostBuildingVisual.tscn
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_scene load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://buildings/OutpostBuildingVisual.gd" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://buildings/BuildingActionProvider.gd" id="2"]
|
||||||
|
|
||||||
|
[node name="Visual" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="ActionProvider" type="Node" parent="."]
|
||||||
|
script = ExtResource("2")
|
||||||
|
actions = PackedStringArray("garrison", "transfer")
|
||||||
|
|
@ -156,6 +156,16 @@ var lab_building_id: int = 0
|
||||||
get_node("../UI/LabMenu/Bar"),
|
get_node("../UI/LabMenu/Bar"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# #103: the outpost's own panel. garrison_building_id is remembered for the
|
||||||
|
# same reason craft_building_id is - _process() clears current_building while
|
||||||
|
# a menu is open, so by the next frame the panel would not know which outpost
|
||||||
|
# it belongs to and every button in it would bail out at its `== 0` guard.
|
||||||
|
@onready var garrison_menu: Control = get_node("../UI/GarrisonMenu")
|
||||||
|
@onready var garrison_panel: Control = get_node("../UI/GarrisonMenu/Panel")
|
||||||
|
@onready var garrison_status: Label = get_node("../UI/GarrisonMenu/StatusLabel")
|
||||||
|
@onready var garrison_close_button: IconButton = get_node("../UI/GarrisonMenu/CloseButton")
|
||||||
|
var garrison_building_id: int = 0
|
||||||
|
|
||||||
@onready var skill_menu: Control = get_node("../UI/SkillMenu")
|
@onready var skill_menu: Control = get_node("../UI/SkillMenu")
|
||||||
@onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree")
|
@onready var skill_tree: Control = get_node("../UI/SkillMenu/Tree")
|
||||||
@onready var skill_detail: Label = get_node("../UI/SkillMenu/DetailLabel")
|
@onready var skill_detail: Label = get_node("../UI/SkillMenu/DetailLabel")
|
||||||
|
|
@ -251,6 +261,8 @@ func _ready() -> void:
|
||||||
_layout_hangar()
|
_layout_hangar()
|
||||||
build_button.pressed.connect(_on_build_pressed)
|
build_button.pressed.connect(_on_build_pressed)
|
||||||
build_close_button.pressed.connect(_close_build_menu)
|
build_close_button.pressed.connect(_close_build_menu)
|
||||||
|
garrison_close_button.pressed.connect(_close_garrison_menu)
|
||||||
|
garrison_panel.changed.connect(_refresh_garrison)
|
||||||
_configure_build_menu_entries()
|
_configure_build_menu_entries()
|
||||||
lab_close_button.pressed.connect(func(): lab_menu.visible = false)
|
lab_close_button.pressed.connect(func(): lab_menu.visible = false)
|
||||||
lab_buy_button.pressed.connect(_on_lab_buy)
|
lab_buy_button.pressed.connect(_on_lab_buy)
|
||||||
|
|
@ -586,6 +598,11 @@ func _on_action_pressed() -> void:
|
||||||
elif action == "formation":
|
elif action == "formation":
|
||||||
formation_menu.visible = true
|
formation_menu.visible = true
|
||||||
formation_editor.queue_redraw()
|
formation_editor.queue_redraw()
|
||||||
|
elif action == "garrison":
|
||||||
|
garrison_building_id = current_building.building_id
|
||||||
|
garrison_panel.building_id = garrison_building_id
|
||||||
|
garrison_menu.visible = true
|
||||||
|
_refresh_garrison()
|
||||||
|
|
||||||
|
|
||||||
func _on_build_pressed() -> void:
|
func _on_build_pressed() -> void:
|
||||||
|
|
@ -691,6 +708,29 @@ func _close_build_menu() -> void:
|
||||||
build_menu.visible = false
|
build_menu.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
# --- the outpost's garrison panel (#103) -----------------------------------
|
||||||
|
|
||||||
|
func _close_garrison_menu() -> void:
|
||||||
|
garrison_menu.visible = false
|
||||||
|
garrison_building_id = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_garrison() -> void:
|
||||||
|
if garrison_building_id == 0:
|
||||||
|
return
|
||||||
|
garrison_panel.building_id = garrison_building_id
|
||||||
|
garrison_panel.refresh()
|
||||||
|
var ordered: int = GameState.garrison_desired_total(garrison_building_id)
|
||||||
|
var crewed: int = GameState.garrison_crew(garrison_building_id).size()
|
||||||
|
if ordered == 0:
|
||||||
|
garrison_status.text = "No vessels ordered. Only armed vessels can hold a position."
|
||||||
|
elif crewed >= ordered:
|
||||||
|
garrison_status.text = "Garrison at full strength"
|
||||||
|
else:
|
||||||
|
garrison_status.text = "%d of %d berths crewed - vessels are sent from the mothership as they become available" \
|
||||||
|
% [crewed, ordered]
|
||||||
|
|
||||||
|
|
||||||
func cost_of(type: String) -> int:
|
func cost_of(type: String) -> int:
|
||||||
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
var definition: Resource = BUILDING_DEFINITIONS.get_definition(type)
|
||||||
return int(definition.cost) if definition != null else 0
|
return int(definition.cost) if definition != null else 0
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,9 @@ const SHIP_COST: int = 6
|
||||||
const BUILDING_MOTHERSHIP: String = "mothership"
|
const BUILDING_MOTHERSHIP: String = "mothership"
|
||||||
const BUILDING_SHIP_CRAFTER: String = "ship_crafter"
|
const BUILDING_SHIP_CRAFTER: String = "ship_crafter"
|
||||||
const BUILDING_COMMAND_CENTER: String = "command_center"
|
const BUILDING_COMMAND_CENTER: String = "command_center"
|
||||||
|
# #102: a forward position the fleet can garrison, researched early because
|
||||||
|
# it is the answer to waves going after what you have built.
|
||||||
|
const BUILDING_OUTPOST: String = "outpost"
|
||||||
# Extractors are not placed in space like the others - they are built ON a
|
# Extractors are not placed in space like the others - they are built ON a
|
||||||
# planet's surface and tracked separately (see `extractors`).
|
# planet's surface and tracked separately (see `extractors`).
|
||||||
const BUILDING_EXTRACTOR: String = "extractor"
|
const BUILDING_EXTRACTOR: String = "extractor"
|
||||||
|
|
@ -362,6 +365,23 @@ const SKILL_TREE: Array = [
|
||||||
"x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "research_points": 1,
|
"x": 0.50, "unlocks": BUILDING_COMMAND_CENTER, "research_points": 1,
|
||||||
"desc": "Build command centers, where the fleet formation is arranged.",
|
"desc": "Build command centers, where the fleet formation is arranged.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
# Early on purpose (#97: "pretty early in the tech tree"). It hangs off
|
||||||
|
# coordination rather than off a deep branch, so the answer to waves
|
||||||
|
# attacking your structures is available at about the time the waves
|
||||||
|
# start being able to hurt them.
|
||||||
|
"id": "forward_bases", "name": "Forward Bases", "cost": 30, "requires": "coordination",
|
||||||
|
"x": 0.66, "unlocks": BUILDING_OUTPOST, "research_points": 1,
|
||||||
|
"desc": "Build outposts: forward positions vessels can be stationed at, to hold ground you are not standing on.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# The same shape as freight_yards for hubs: a permanent bonus that
|
||||||
|
# applies to outposts already standing, not only to new ones, so
|
||||||
|
# researching it never means demolishing and rebuilding.
|
||||||
|
"id": "garrison_bays", "name": "Garrison Bays", "cost": 110, "requires": "forward_bases",
|
||||||
|
"x": 0.66, "outpost_bonus": 1, "research_points": 2,
|
||||||
|
"desc": "Permanent: +1 berth at every outpost, including ones already built.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
"id": "freight", "name": "Freight Handling", "cost": 40, "requires": "logistics",
|
||||||
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1,
|
"x": 0.18, "unlocks": BUILDING_TRANSPORT_HUB, "research_points": 1,
|
||||||
|
|
@ -728,6 +748,138 @@ func destroy_building(id: int) -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
# --- outpost garrisons (#103) ----------------------------------------------
|
||||||
|
#
|
||||||
|
# Two separate things live on an outpost's record, and conflating them was
|
||||||
|
# the first thing to get wrong here:
|
||||||
|
#
|
||||||
|
# garrison what the player ASKED for - {vessel_type: count}. Set in
|
||||||
|
# the outpost's own menu and otherwise untouched.
|
||||||
|
# garrison_crew who is actually THERE - a list of vessel types. #104
|
||||||
|
# fills it from the mothership; nothing else writes it.
|
||||||
|
#
|
||||||
|
# Keeping the order as a wish rather than as an inventory is what lets an
|
||||||
|
# outpost ask for a vessel nobody has yet and simply wait, instead of the
|
||||||
|
# request evaporating.
|
||||||
|
|
||||||
|
const GARRISON_BASE_SLOTS: int = 2
|
||||||
|
# Which vessels can hold a position. #97: "Only ships that attack can be
|
||||||
|
# assigned" - so a shield vessel, a hauler or an engine block cannot, however
|
||||||
|
# much the player might want the shield out there. Written as capabilities
|
||||||
|
# rather than as a list of types, so a new armed vessel is eligible the day
|
||||||
|
# it is added and an unarmed one never is by accident.
|
||||||
|
const GARRISON_CAPABILITIES: PackedStringArray = ["weapon", "laser"]
|
||||||
|
|
||||||
|
|
||||||
|
func outpost_slot_capacity() -> int:
|
||||||
|
var n: int = GARRISON_BASE_SLOTS
|
||||||
|
for id in unlocked:
|
||||||
|
n += int(skill_node(id).get("outpost_bonus", 0))
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
func vessel_can_garrison(vessel_type: String) -> bool:
|
||||||
|
var definition: Resource = vessel_definition(vessel_type)
|
||||||
|
if definition == null or not definition.fleet_eligible:
|
||||||
|
return false
|
||||||
|
for capability in GARRISON_CAPABILITIES:
|
||||||
|
if capability in definition.capabilities:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func garrison_vessel_types() -> PackedStringArray:
|
||||||
|
var out: PackedStringArray = PackedStringArray()
|
||||||
|
for definition in VESSEL_DEFINITIONS.all():
|
||||||
|
if vessel_can_garrison(String(definition.definition_id)):
|
||||||
|
out.append(String(definition.definition_id))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func is_outpost(building: Dictionary) -> bool:
|
||||||
|
return building_has_capability(building, "garrison")
|
||||||
|
|
||||||
|
|
||||||
|
func garrison_desired(id: int) -> Dictionary:
|
||||||
|
var b: Dictionary = find_building(id)
|
||||||
|
if b.is_empty() or not is_outpost(b):
|
||||||
|
return {}
|
||||||
|
if not b.has("garrison"):
|
||||||
|
b.garrison = {}
|
||||||
|
return b.garrison
|
||||||
|
|
||||||
|
|
||||||
|
func garrison_desired_total(id: int) -> int:
|
||||||
|
var n: int = 0
|
||||||
|
for count in garrison_desired(id).values():
|
||||||
|
n += int(count)
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
func garrison_desired_of(id: int, vessel_type: String) -> int:
|
||||||
|
return int(garrison_desired(id).get(vessel_type, 0))
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the count actually set, so the caller can show what happened when
|
||||||
|
# the request was clamped rather than silently doing something else.
|
||||||
|
func set_garrison_desired(id: int, vessel_type: String, count: int) -> int:
|
||||||
|
var b: Dictionary = find_building(id)
|
||||||
|
if b.is_empty() or not is_outpost(b) or not vessel_can_garrison(vessel_type):
|
||||||
|
return 0
|
||||||
|
var wanted: Dictionary = garrison_desired(id)
|
||||||
|
var others: int = garrison_desired_total(id) - int(wanted.get(vessel_type, 0))
|
||||||
|
var room: int = maxi(outpost_slot_capacity() - others, 0)
|
||||||
|
var final_count: int = clampi(count, 0, room)
|
||||||
|
if final_count <= 0:
|
||||||
|
wanted.erase(vessel_type)
|
||||||
|
else:
|
||||||
|
wanted[vessel_type] = final_count
|
||||||
|
return final_count
|
||||||
|
|
||||||
|
|
||||||
|
func adjust_garrison_desired(id: int, vessel_type: String, delta: int) -> int:
|
||||||
|
return set_garrison_desired(id, vessel_type,
|
||||||
|
garrison_desired_of(id, vessel_type) + delta)
|
||||||
|
|
||||||
|
|
||||||
|
func garrison_crew(id: int) -> Array:
|
||||||
|
var b: Dictionary = find_building(id)
|
||||||
|
if b.is_empty() or not is_outpost(b):
|
||||||
|
return []
|
||||||
|
if not b.has("garrison_crew"):
|
||||||
|
b.garrison_crew = []
|
||||||
|
return b.garrison_crew
|
||||||
|
|
||||||
|
|
||||||
|
func garrison_crew_of(id: int, vessel_type: String) -> int:
|
||||||
|
var n: int = 0
|
||||||
|
for crewed in garrison_crew(id):
|
||||||
|
if String(crewed) == vessel_type:
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
# What this outpost is still short of, as {vessel_type: count}. The one
|
||||||
|
# question #104 has to answer every tick, so it is asked here rather than
|
||||||
|
# recomputed by whoever needs it.
|
||||||
|
func garrison_shortfall(id: int) -> Dictionary:
|
||||||
|
var out: Dictionary = {}
|
||||||
|
for vessel_type in garrison_desired(id):
|
||||||
|
var missing: int = int(garrison_desired(id)[vessel_type]) \
|
||||||
|
- garrison_crew_of(id, String(vessel_type))
|
||||||
|
if missing > 0:
|
||||||
|
out[vessel_type] = missing
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func outpost_ids() -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
for b in buildings:
|
||||||
|
if is_outpost(b):
|
||||||
|
out.append(int(b.id))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
func building_has_capability(building: Dictionary, capability: String) -> bool:
|
func building_has_capability(building: Dictionary, capability: String) -> bool:
|
||||||
if building.is_empty():
|
if building.is_empty():
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
146
tests/test_outpost_building.gd
Normal file
146
tests/test_outpost_building.gd
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Headless test: run with
|
||||||
|
# godot --headless -s res://tests/test_outpost_building.gd
|
||||||
|
# Forgejo #102 (#97d): "A new building should be available, pretty early in
|
||||||
|
# the tech tree, to create outposts."
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. the outpost is in the building catalog, with its own visual, its own
|
||||||
|
# icon and its own map colour - not a reused one
|
||||||
|
# B. a skill node unlocks it, and it is EARLY: shallower than the deep
|
||||||
|
# branches, and reachable for less than the mid-game structures cost
|
||||||
|
# C. it is hidden from the build menu until that node is researched, and
|
||||||
|
# appears afterwards, like every other gated structure
|
||||||
|
# D. it can be placed, it stands up in the world, and it has enough health
|
||||||
|
# to be worth attacking - it exists to be shot at
|
||||||
|
|
||||||
|
const MAX_EARLY_DEPTH: int = 2
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var scene: Node
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
# GameState is an autoload: a bare GameState.<instance member> does not
|
||||||
|
# compile in a SceneTree script, so the node is fetched by name instead.
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
game_state.buildings.clear()
|
||||||
|
game_state.extractors.clear()
|
||||||
|
game_state.transporters.clear()
|
||||||
|
game_state.reset_progression()
|
||||||
|
game_state.reset_waves()
|
||||||
|
game_state.rocks = 5000
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_OUTPOST_BUILDING: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _catalog_cases() -> bool:
|
||||||
|
var definitions: Script = load("res://buildings/BuildingDefinitions.gd")
|
||||||
|
var outpost: Resource = definitions.get_definition(GameState.BUILDING_OUTPOST)
|
||||||
|
if outpost == null:
|
||||||
|
return _fail("no \"outpost\" definition in the catalog")
|
||||||
|
if outpost.visual_scene == null:
|
||||||
|
return _fail("the outpost has no visual - it would be an invisible structure")
|
||||||
|
if outpost.menu_button_name == "":
|
||||||
|
return _fail("the outpost declares no build-menu entry, so it could never be built")
|
||||||
|
# --- A. its own look -------------------------------------------------
|
||||||
|
for other in definitions.all():
|
||||||
|
if other.definition_id == GameState.BUILDING_OUTPOST:
|
||||||
|
continue
|
||||||
|
if other.menu_icon == outpost.menu_icon:
|
||||||
|
return _fail("the outpost shares its icon with the %s" % other.definition_id)
|
||||||
|
if other.visual_scene == outpost.visual_scene:
|
||||||
|
return _fail("the outpost shares its visual with the %s" % other.definition_id)
|
||||||
|
if other.map_color.is_equal_approx(outpost.map_color):
|
||||||
|
return _fail("the outpost shares its map colour with the %s" % other.definition_id)
|
||||||
|
print("the outpost has its own icon, visual and map colour")
|
||||||
|
|
||||||
|
# --- B. researched, and early ----------------------------------------
|
||||||
|
var node_name: String = game_state.research_for_building(GameState.BUILDING_OUTPOST)
|
||||||
|
if node_name == "":
|
||||||
|
return _fail("nothing in the skill tree unlocks the outpost")
|
||||||
|
var node_id: String = ""
|
||||||
|
for node in GameState.SKILL_TREE:
|
||||||
|
if node.get("unlocks", "") == GameState.BUILDING_OUTPOST:
|
||||||
|
node_id = String(node.id)
|
||||||
|
var depth: int = game_state.skill_depth(node_id)
|
||||||
|
if depth > MAX_EARLY_DEPTH:
|
||||||
|
return _fail("\"%s\" sits %d prerequisites deep - that is not early" % [node_name, depth])
|
||||||
|
var cost: int = int(game_state.skill_node(node_id).cost)
|
||||||
|
for node in GameState.SKILL_TREE:
|
||||||
|
if String(node.get("unlocks", "")) in [GameState.BUILDING_TRANSPORT_HUB,
|
||||||
|
GameState.BUILDING_SHIP_LAB, GameState.BUILDING_GAS_BURNER]:
|
||||||
|
if int(node.cost) <= cost:
|
||||||
|
return _fail("\"%s\" costs %d, no less than the mid-game %s at %d" \
|
||||||
|
% [node_name, cost, node.name, node.cost])
|
||||||
|
print("\"%s\" is %d deep and costs %d ore - earlier and cheaper than the mid-game structures" \
|
||||||
|
% [node_name, depth, cost])
|
||||||
|
|
||||||
|
# --- C. gated until researched ---------------------------------------
|
||||||
|
if game_state.is_building_unlocked(GameState.BUILDING_OUTPOST):
|
||||||
|
return _fail("the outpost is buildable before its node is researched")
|
||||||
|
game_state.unlocked.append(node_id)
|
||||||
|
if not game_state.is_building_unlocked(GameState.BUILDING_OUTPOST):
|
||||||
|
return _fail("researching \"%s\" did not unlock the outpost" % node_name)
|
||||||
|
print("hidden until \"%s\" is researched, then offered" % node_name)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
if _catalog_cases():
|
||||||
|
return true
|
||||||
|
scene = load("res://world/Main.tscn").instantiate()
|
||||||
|
root.add_child(scene)
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 1
|
||||||
|
return false
|
||||||
|
1:
|
||||||
|
if elapsed < 0.3:
|
||||||
|
return false
|
||||||
|
scene.get_node("AsteroidField").set_process(false)
|
||||||
|
scene.get_node("EnemySpawner").set_process(false)
|
||||||
|
# --- C (continued). the menu shows it ------------------------
|
||||||
|
var controller: Node = scene.get_node("BuildController")
|
||||||
|
var button: Control = scene.get_node("UI/BuildMenu/OutpostButton")
|
||||||
|
controller._layout_build_menu()
|
||||||
|
if not button.visible:
|
||||||
|
return _fail("the outpost is researched but its build-menu button is hidden")
|
||||||
|
scene.get_node("BuildingField").place(GameState.BUILDING_OUTPOST, Vector2(500.0, 0.0))
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 2
|
||||||
|
return false
|
||||||
|
2:
|
||||||
|
if elapsed < 0.2:
|
||||||
|
return false
|
||||||
|
# --- D. it stands up, and can be shot at ---------------------
|
||||||
|
var node: Node2D = null
|
||||||
|
for b in scene.get_node("BuildingField").spawned:
|
||||||
|
if is_instance_valid(b) and b.building_type == GameState.BUILDING_OUTPOST:
|
||||||
|
node = b
|
||||||
|
if node == null:
|
||||||
|
return _fail("placing an outpost produced no node")
|
||||||
|
if node.get_node_or_null("VisualHost").get_child_count() == 0:
|
||||||
|
return _fail("the outpost node has no visual under it")
|
||||||
|
if node.get_node_or_null("Hitbox") == null:
|
||||||
|
return _fail("the outpost has no hitbox - a wave could not touch it")
|
||||||
|
var hp: int = game_state.building_hp(node.building_id)
|
||||||
|
var crafter_hp: int = game_state.building_max_hp(GameState.BUILDING_SHIP_CRAFTER)
|
||||||
|
if hp < crafter_hp:
|
||||||
|
return _fail("the outpost has %d hp, less than a crafter's %d - it is meant to take hits" \
|
||||||
|
% [hp, crafter_hp])
|
||||||
|
print("an outpost stands at %d hp, against a crafter's %d" % [hp, crafter_hp])
|
||||||
|
|
||||||
|
print("TEST_OUTPOST_BUILDING: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
206
tests/test_outpost_garrison_menu.gd
Normal file
206
tests/test_outpost_garrison_menu.gd
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Headless test: run with
|
||||||
|
# godot --headless -s res://tests/test_outpost_garrison_menu.gd
|
||||||
|
# Forgejo #103 (#97e): "Outposts can be occupied by ships. Only ships that
|
||||||
|
# attack can be assigned. A dedicated menu on each outpost should let you
|
||||||
|
# compose how you want the outpost to be filled. There's a default of 2
|
||||||
|
# vessel per outpost, upgrades can increase this number."
|
||||||
|
#
|
||||||
|
# Two things live on an outpost's record and conflating them is the first
|
||||||
|
# thing to get wrong: the ORDER (what the player asked for) and the CREW
|
||||||
|
# (who is actually there). This menu edits the order only; #104 fills the
|
||||||
|
# crew. Keeping the order as a wish is what lets an outpost ask for a hull
|
||||||
|
# nobody has built yet and simply wait.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. only armed vessels can be assigned - a shield, a hauler or an engine
|
||||||
|
# block cannot hold a position however much the player wants it there
|
||||||
|
# B. two berths by default, and the order cannot exceed them
|
||||||
|
# C. researching the berth upgrade raises the ceiling at outposts ALREADY
|
||||||
|
# standing, not only at new ones
|
||||||
|
# D. the menu opens from the outpost, drives the real order through the
|
||||||
|
# real buttons, and reads back "here / ordered" rather than one number
|
||||||
|
# E. an outpost with an order nobody can fill reports it as waiting, and
|
||||||
|
# the order survives - it is not silently dropped
|
||||||
|
|
||||||
|
var game_state: Node
|
||||||
|
var scene: Node
|
||||||
|
var controller: Node
|
||||||
|
var menu: Control
|
||||||
|
var panel: Control
|
||||||
|
var outpost_id: int = 0
|
||||||
|
var stage: int = 0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
# GameState is an autoload: a bare GameState.<instance member> does not
|
||||||
|
# compile in a SceneTree script, so the node is fetched by name instead.
|
||||||
|
game_state = root.get_node("GameState")
|
||||||
|
game_state.buildings.clear()
|
||||||
|
game_state.extractors.clear()
|
||||||
|
game_state.transporters.clear()
|
||||||
|
game_state.reset_progression()
|
||||||
|
game_state.reset_waves()
|
||||||
|
game_state.rocks = 5000
|
||||||
|
game_state.unlocked.append("forward_bases")
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_OUTPOST_GARRISON_MENU: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _state_cases() -> bool:
|
||||||
|
# --- A. only armed vessels ------------------------------------------
|
||||||
|
var eligible: PackedStringArray = game_state.garrison_vessel_types()
|
||||||
|
if eligible.is_empty():
|
||||||
|
return _fail("no vessel at all may be garrisoned")
|
||||||
|
if not (GameState.VESSEL_INTERCEPTOR in eligible):
|
||||||
|
return _fail("the interceptor, which carries the fleet's gun, cannot be garrisoned")
|
||||||
|
for unarmed in [GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER,
|
||||||
|
GameState.VESSEL_CARRIER, GameState.VESSEL_DISTORDER]:
|
||||||
|
if game_state.vessel_can_garrison(unarmed):
|
||||||
|
return _fail("%s can be garrisoned - it does not attack" % unarmed)
|
||||||
|
print("garrisonable: %s" % str(eligible))
|
||||||
|
|
||||||
|
var id: int = game_state.add_building(GameState.BUILDING_OUTPOST, Vector2(500.0, 0.0))
|
||||||
|
outpost_id = id
|
||||||
|
if not game_state.is_outpost(game_state.find_building(id)):
|
||||||
|
return _fail("the outpost does not carry the garrison capability")
|
||||||
|
if game_state.set_garrison_desired(id, GameState.VESSEL_SHIELD, 1) != 0:
|
||||||
|
return _fail("a shield vessel was accepted into a garrison")
|
||||||
|
|
||||||
|
# --- B. two berths, and the ceiling bites ---------------------------
|
||||||
|
if GameState.GARRISON_BASE_SLOTS != 2:
|
||||||
|
return _fail("the default is %d berths, not the 2 the ticket asks for" \
|
||||||
|
% GameState.GARRISON_BASE_SLOTS)
|
||||||
|
if game_state.outpost_slot_capacity() != 2:
|
||||||
|
return _fail("an unresearched outpost offers %d berths" % game_state.outpost_slot_capacity())
|
||||||
|
if game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 5) != 2:
|
||||||
|
return _fail("an order of 5 was not clamped to the 2 berths available")
|
||||||
|
if game_state.garrison_desired_total(id) != 2:
|
||||||
|
return _fail("the outpost holds an order of %d against 2 berths" \
|
||||||
|
% game_state.garrison_desired_total(id))
|
||||||
|
print("2 berths by default; an order of 5 clamps to 2")
|
||||||
|
|
||||||
|
# --- C. the upgrade reaches existing outposts -----------------------
|
||||||
|
var upgrade_id: String = ""
|
||||||
|
for node in GameState.SKILL_TREE:
|
||||||
|
if int(node.get("outpost_bonus", 0)) > 0:
|
||||||
|
upgrade_id = String(node.id)
|
||||||
|
if upgrade_id == "":
|
||||||
|
return _fail("no skill node raises the berth count")
|
||||||
|
game_state.unlocked.append(upgrade_id)
|
||||||
|
if game_state.outpost_slot_capacity() != 3:
|
||||||
|
return _fail("researching \"%s\" left the capacity at %d" \
|
||||||
|
% [upgrade_id, game_state.outpost_slot_capacity()])
|
||||||
|
# The outpost was placed BEFORE the research, which is the case that
|
||||||
|
# matters: a bonus that only reached new buildings would mean demolishing
|
||||||
|
# and rebuilding to collect it.
|
||||||
|
if game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 3) != 3:
|
||||||
|
return _fail("an outpost built before the upgrade did not gain the berth")
|
||||||
|
print("\"%s\" raises an already-standing outpost to 3 berths" % upgrade_id)
|
||||||
|
game_state.set_garrison_desired(id, GameState.VESSEL_INTERCEPTOR, 0)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> bool:
|
||||||
|
elapsed += delta
|
||||||
|
match stage:
|
||||||
|
0:
|
||||||
|
if _state_cases():
|
||||||
|
return true
|
||||||
|
game_state.buildings.clear()
|
||||||
|
scene = load("res://world/Main.tscn").instantiate()
|
||||||
|
root.add_child(scene)
|
||||||
|
controller = scene.get_node("BuildController")
|
||||||
|
menu = scene.get_node("UI/GarrisonMenu")
|
||||||
|
panel = scene.get_node("UI/GarrisonMenu/Panel")
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 1
|
||||||
|
return false
|
||||||
|
1:
|
||||||
|
if elapsed < 0.3:
|
||||||
|
return false
|
||||||
|
scene.get_node("AsteroidField").set_process(false)
|
||||||
|
scene.get_node("EnemySpawner").set_process(false)
|
||||||
|
scene.get_node("BuildingField").place(GameState.BUILDING_OUTPOST, Vector2(500.0, 0.0))
|
||||||
|
elapsed = 0.0
|
||||||
|
stage = 2
|
||||||
|
return false
|
||||||
|
2:
|
||||||
|
if elapsed < 0.2:
|
||||||
|
return false
|
||||||
|
# --- D. the real menu, through the real buttons --------------
|
||||||
|
var outpost: Node2D = null
|
||||||
|
for b in scene.get_node("BuildingField").spawned:
|
||||||
|
if is_instance_valid(b) and b.building_type == GameState.BUILDING_OUTPOST:
|
||||||
|
outpost = b
|
||||||
|
if outpost == null:
|
||||||
|
return _fail("no outpost node was placed")
|
||||||
|
outpost_id = int(outpost.building_id)
|
||||||
|
if outpost.primary_action() != "garrison":
|
||||||
|
return _fail("the outpost's primary action is \"%s\", not \"garrison\"" \
|
||||||
|
% outpost.primary_action())
|
||||||
|
controller.current_building = outpost
|
||||||
|
controller._on_action_pressed()
|
||||||
|
if not menu.visible:
|
||||||
|
return _fail("the outpost did not open the garrison menu")
|
||||||
|
if panel.building_id != outpost_id:
|
||||||
|
return _fail("the panel opened without knowing which outpost it belongs to")
|
||||||
|
|
||||||
|
var row: Dictionary = {}
|
||||||
|
for candidate in panel._rows:
|
||||||
|
if String(candidate.type) == GameState.VESSEL_INTERCEPTOR:
|
||||||
|
row = candidate
|
||||||
|
if row.is_empty():
|
||||||
|
return _fail("the panel has no row for the interceptor")
|
||||||
|
for unarmed in [GameState.VESSEL_SHIELD, GameState.VESSEL_TRANSPORTER]:
|
||||||
|
for candidate in panel._rows:
|
||||||
|
if String(candidate.type) == unarmed:
|
||||||
|
return _fail("the panel offers a row for the unarmed %s" % unarmed)
|
||||||
|
|
||||||
|
row.plus.pressed.emit()
|
||||||
|
row.plus.pressed.emit()
|
||||||
|
if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 2:
|
||||||
|
return _fail("two presses of + ordered %d vessels" \
|
||||||
|
% game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR))
|
||||||
|
if row.count.text != "0/2":
|
||||||
|
return _fail("the row reads \"%s\" - it should show crew over order" % row.count.text)
|
||||||
|
row.minus.pressed.emit()
|
||||||
|
if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 1:
|
||||||
|
return _fail("- did not take the order back down")
|
||||||
|
print("the menu drives the real order: + + - leaves 1, shown as \"%s\"" % row.count.text)
|
||||||
|
|
||||||
|
# The ceiling is enforced through the buttons too, not only in
|
||||||
|
# GameState - a + that silently did nothing would be worse than a
|
||||||
|
# disabled one.
|
||||||
|
row.plus.pressed.emit()
|
||||||
|
row.plus.pressed.emit()
|
||||||
|
row.plus.pressed.emit()
|
||||||
|
var capacity: int = game_state.outpost_slot_capacity()
|
||||||
|
if game_state.garrison_desired_total(outpost_id) != capacity:
|
||||||
|
return _fail("pressing + past the ceiling ordered %d against %d berths" \
|
||||||
|
% [game_state.garrison_desired_total(outpost_id), capacity])
|
||||||
|
if row.plus.enabled:
|
||||||
|
return _fail("+ is still offered with every berth already spoken for")
|
||||||
|
print("+ stops at %d berths and greys itself out" % capacity)
|
||||||
|
|
||||||
|
# --- E. an unfillable order waits ---------------------------
|
||||||
|
if game_state.garrison_crew(outpost_id).size() != 0:
|
||||||
|
return _fail("the outpost is crewed already, so waiting proves nothing")
|
||||||
|
controller._refresh_garrison()
|
||||||
|
var status: String = scene.get_node("UI/GarrisonMenu/StatusLabel").text
|
||||||
|
if status.strip_edges() == "":
|
||||||
|
return _fail("an outpost waiting on vessels says nothing at all")
|
||||||
|
if game_state.garrison_desired_total(outpost_id) != capacity:
|
||||||
|
return _fail("the order was dropped because it could not be filled")
|
||||||
|
print("waiting, and saying so: \"%s\"" % status)
|
||||||
|
|
||||||
|
print("TEST_OUTPOST_GARRISON_MENU: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
134
ui/GarrisonPanel.gd
Normal file
134
ui/GarrisonPanel.gd
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
# #103 (#97e): "A dedicated menu on each outpost should let you compose how
|
||||||
|
# you want the outpost to be filled."
|
||||||
|
#
|
||||||
|
# Builds its own rows from GameState.garrison_vessel_types() rather than
|
||||||
|
# from nodes placed in Main.tscn. Which vessels can hold a position is
|
||||||
|
# decided by capability, not by a list of types, so the day an armed vessel
|
||||||
|
# is added it gets a row here and an unarmed one never does by accident -
|
||||||
|
# and neither change touches the scene.
|
||||||
|
#
|
||||||
|
# The panel edits the ORDER, not the crew. What the player sets here is what
|
||||||
|
# they want stationed; #104 is what actually gets vessels there. The two are
|
||||||
|
# shown side by side ("1/2") so an outpost that is waiting on a hull it does
|
||||||
|
# not have yet reads as waiting rather than as broken.
|
||||||
|
|
||||||
|
# Emitted when the player changes the order, so whatever hosts the panel can
|
||||||
|
# update its own readout rather than polling.
|
||||||
|
signal changed
|
||||||
|
|
||||||
|
const ROW_HEIGHT: float = 96.0
|
||||||
|
const ROW_GAP: float = 12.0
|
||||||
|
const BUTTON_SIZE: float = 76.0
|
||||||
|
const NAME_FONT_SIZE: int = 30
|
||||||
|
const COUNT_FONT_SIZE: int = 34
|
||||||
|
const HEADER_FONT_SIZE: int = 22
|
||||||
|
|
||||||
|
var building_id: int = 0
|
||||||
|
|
||||||
|
var _rows: Array = []
|
||||||
|
var _capacity_label: Label
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
# _make_label already parents what it builds.
|
||||||
|
_capacity_label = _make_label("", HEADER_FONT_SIZE, Color(0.62, 0.78, 0.86))
|
||||||
|
_capacity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
|
||||||
|
for vessel_type in GameState.garrison_vessel_types():
|
||||||
|
var row: Dictionary = _make_row(vessel_type)
|
||||||
|
_rows.append(row)
|
||||||
|
resized.connect(_layout)
|
||||||
|
_layout()
|
||||||
|
|
||||||
|
|
||||||
|
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = text_value
|
||||||
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
label.add_theme_font_size_override("font_size", font_size)
|
||||||
|
label.add_theme_color_override("font_color", colour)
|
||||||
|
label.add_theme_color_override("font_outline_color", Color(0.01, 0.01, 0.035, 0.95))
|
||||||
|
label.add_theme_constant_override("outline_size", 2)
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
add_child(label)
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
|
func _make_row(vessel_type: String) -> Dictionary:
|
||||||
|
var definition: Resource = GameState.vessel_definition(vessel_type)
|
||||||
|
var colour: Color = definition.glow_color if definition != null else Color.WHITE
|
||||||
|
|
||||||
|
var name_label: Label = _make_label(GameState.vessel_name(vessel_type), NAME_FONT_SIZE, colour)
|
||||||
|
var count_label: Label = _make_label("0", COUNT_FONT_SIZE, Color(0.94, 0.98, 1.0))
|
||||||
|
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
|
||||||
|
var minus := IconButton.new()
|
||||||
|
minus.icon = IconButton.Icon.MINUS
|
||||||
|
minus.glow_color = colour
|
||||||
|
minus.pressed.connect(_on_adjust.bind(vessel_type, -1))
|
||||||
|
add_child(minus)
|
||||||
|
|
||||||
|
var plus := IconButton.new()
|
||||||
|
plus.icon = IconButton.Icon.PLUS
|
||||||
|
plus.glow_color = colour
|
||||||
|
plus.pressed.connect(_on_adjust.bind(vessel_type, 1))
|
||||||
|
add_child(plus)
|
||||||
|
|
||||||
|
return {"type": vessel_type, "name": name_label, "count": count_label,
|
||||||
|
"minus": minus, "plus": plus}
|
||||||
|
|
||||||
|
|
||||||
|
func _on_adjust(vessel_type: String, delta: int) -> void:
|
||||||
|
if building_id == 0:
|
||||||
|
return
|
||||||
|
GameState.adjust_garrison_desired(building_id, vessel_type, delta)
|
||||||
|
refresh()
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _layout() -> void:
|
||||||
|
if _rows.is_empty():
|
||||||
|
return
|
||||||
|
_capacity_label.position = Vector2.ZERO
|
||||||
|
_capacity_label.size = Vector2(size.x, 44.0)
|
||||||
|
var y: float = 52.0
|
||||||
|
for row in _rows:
|
||||||
|
row.name.position = Vector2(0.0, y)
|
||||||
|
row.name.size = Vector2(size.x * 0.46, ROW_HEIGHT)
|
||||||
|
var right: float = size.x
|
||||||
|
row.plus.position = Vector2(right - BUTTON_SIZE, y + (ROW_HEIGHT - BUTTON_SIZE) * 0.5)
|
||||||
|
row.plus.size = Vector2(BUTTON_SIZE, BUTTON_SIZE)
|
||||||
|
row.count.position = Vector2(right - BUTTON_SIZE * 3.0, y)
|
||||||
|
row.count.size = Vector2(BUTTON_SIZE * 2.0, ROW_HEIGHT)
|
||||||
|
row.minus.position = Vector2(right - BUTTON_SIZE * 4.0,
|
||||||
|
y + (ROW_HEIGHT - BUTTON_SIZE) * 0.5)
|
||||||
|
row.minus.size = Vector2(BUTTON_SIZE, BUTTON_SIZE)
|
||||||
|
y += ROW_HEIGHT + ROW_GAP
|
||||||
|
|
||||||
|
|
||||||
|
func natural_height() -> float:
|
||||||
|
return 52.0 + float(_rows.size()) * (ROW_HEIGHT + ROW_GAP)
|
||||||
|
|
||||||
|
|
||||||
|
func refresh() -> void:
|
||||||
|
if _rows.is_empty():
|
||||||
|
return
|
||||||
|
var capacity: int = GameState.outpost_slot_capacity()
|
||||||
|
var ordered: int = GameState.garrison_desired_total(building_id)
|
||||||
|
_capacity_label.text = "BERTHS %d / %d" % [ordered, capacity]
|
||||||
|
for row in _rows:
|
||||||
|
var vessel_type: String = String(row.type)
|
||||||
|
var want: int = GameState.garrison_desired_of(building_id, vessel_type)
|
||||||
|
var here: int = GameState.garrison_crew_of(building_id, vessel_type)
|
||||||
|
# "here / ordered", so an outpost waiting on a hull nobody has built
|
||||||
|
# yet reads as waiting rather than as having lost one.
|
||||||
|
row.count.text = "%d/%d" % [here, want]
|
||||||
|
var unlocked: bool = GameState.vessel_is_unlocked(vessel_type)
|
||||||
|
row.name.modulate.a = 1.0 if unlocked else 0.45
|
||||||
|
row.minus.enabled = unlocked and want > 0
|
||||||
|
row.plus.enabled = unlocked and ordered < capacity
|
||||||
|
row.minus.queue_redraw()
|
||||||
|
row.plus.queue_redraw()
|
||||||
|
|
@ -50,6 +50,9 @@ enum Icon {
|
||||||
# inserting anywhere earlier renumbers every icon int already stored in a
|
# inserting anywhere earlier renumbers every icon int already stored in a
|
||||||
# scene file.
|
# scene file.
|
||||||
PAUSE, RESUME,
|
PAUSE, RESUME,
|
||||||
|
# #102: appended, like everything above - inserting anywhere earlier
|
||||||
|
# renumbers every icon int already stored in every scene.
|
||||||
|
OUTPOST,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Size the glyphs were originally drawn against. Every glyph dimension is
|
# Size the glyphs were originally drawn against. Every glyph dimension is
|
||||||
|
|
@ -291,6 +294,8 @@ func _draw() -> void:
|
||||||
_draw_laser_shot(center, pulse, s)
|
_draw_laser_shot(center, pulse, s)
|
||||||
Icon.DISTORDER_VESSEL:
|
Icon.DISTORDER_VESSEL:
|
||||||
_draw_distorder_vessel(center, pulse, s)
|
_draw_distorder_vessel(center, pulse, s)
|
||||||
|
Icon.OUTPOST:
|
||||||
|
_draw_outpost_icon(center, pulse, s)
|
||||||
|
|
||||||
|
|
||||||
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
|
func _draw_close(center: Vector2, pulse: float, s: float) -> void:
|
||||||
|
|
@ -659,6 +664,30 @@ func _draw_ship_lab_icon(center: Vector2, pulse: float, s: float) -> void:
|
||||||
_active_color(), pulse, 0.35 * s, core_color)
|
_active_color(), pulse, 0.35 * s, core_color)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_outpost_icon(center: Vector2, pulse: float, s: float) -> void:
|
||||||
|
# A watchtower with two vessels holding station off it. The hub's
|
||||||
|
# octagon already means "things are stationed here and go out from
|
||||||
|
# here"; this has to mean "things stay here and watch", so it is a
|
||||||
|
# narrow mast with a sensor head rather than a ring, and the two darts
|
||||||
|
# beside it are the garrison.
|
||||||
|
var mast_top: Vector2 = center + Vector2(0.0, -11.0) * s
|
||||||
|
var mast_foot: Vector2 = center + Vector2(0.0, 9.0) * s
|
||||||
|
NeonDraw.draw_glowing_line(self, mast_foot, mast_top, _active_color(), pulse, 0.5 * s, core_color)
|
||||||
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
||||||
|
mast_top + Vector2(0.0, -4.0) * s, mast_top + Vector2(4.0, 0.0) * s,
|
||||||
|
mast_top + Vector2(0.0, 4.0) * s, mast_top + Vector2(-4.0, 0.0) * s,
|
||||||
|
]), _active_color(), pulse, core_color)
|
||||||
|
# Footing, so the mast stands on something rather than floating.
|
||||||
|
NeonDraw.draw_glowing_line(self, mast_foot + Vector2(-6.0, 0.0) * s,
|
||||||
|
mast_foot + Vector2(6.0, 0.0) * s, _active_color(), pulse, 0.4 * s, core_color)
|
||||||
|
for side in [-1.0, 1.0]:
|
||||||
|
var at: Vector2 = center + Vector2(11.0 * side, -1.0) * s
|
||||||
|
NeonDraw.draw_glowing_polygon(self, PackedVector2Array([
|
||||||
|
at + Vector2(0.0, -4.0) * s, at + Vector2(2.6, 3.0) * s,
|
||||||
|
at + Vector2(-2.6, 3.0) * s,
|
||||||
|
]), _active_color(), pulse, core_color)
|
||||||
|
|
||||||
|
|
||||||
func _draw_transport_hub_icon(center: Vector2, pulse: float, s: float) -> void:
|
func _draw_transport_hub_icon(center: Vector2, pulse: float, s: float) -> void:
|
||||||
# A ring of docking cradles with a freight box in the middle: reads as
|
# A ring of docking cradles with a freight box in the middle: reads as
|
||||||
# "things are stationed here and go out from here", which is what the hub
|
# "things are stationed here and go out from here", which is what the hub
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
[ext_resource type="Script" path="res://ui/NeonPanel.gd" id="20"]
|
[ext_resource type="Script" path="res://ui/NeonPanel.gd" id="20"]
|
||||||
[ext_resource type="Script" path="res://ui/SkillTreeView.gd" id="21"]
|
[ext_resource type="Script" path="res://ui/SkillTreeView.gd" id="21"]
|
||||||
[ext_resource type="Script" path="res://ui/WaveAlert.gd" id="32"]
|
[ext_resource type="Script" path="res://ui/WaveAlert.gd" id="32"]
|
||||||
|
[ext_resource type="Script" path="res://ui/GarrisonPanel.gd" id="33"]
|
||||||
[ext_resource type="Script" path="res://logistics/TransporterField.gd" id="23"]
|
[ext_resource type="Script" path="res://logistics/TransporterField.gd" id="23"]
|
||||||
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
|
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
|
||||||
[ext_resource type="Script" path="res://logistics/VesselDeliveryField.gd" id="26"]
|
[ext_resource type="Script" path="res://logistics/VesselDeliveryField.gd" id="26"]
|
||||||
|
|
@ -719,6 +720,48 @@ script = ExtResource("19")
|
||||||
amount = 30
|
amount = 30
|
||||||
digit_height = 40.0
|
digit_height = 40.0
|
||||||
|
|
||||||
|
[node name="OutpostButton" type="Control" parent="UI/BuildMenu"]
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -64
|
||||||
|
offset_right = 64
|
||||||
|
offset_top = 830
|
||||||
|
offset_bottom = 958
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 40
|
||||||
|
glow_color = Color(0.72, 0.86, 0.45, 1)
|
||||||
|
|
||||||
|
[node name="OutpostLabel" type="Label" parent="UI/BuildMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -150
|
||||||
|
offset_right = 150
|
||||||
|
offset_top = 964
|
||||||
|
offset_bottom = 1030
|
||||||
|
theme_override_font_sizes/font_size = 32
|
||||||
|
autowrap_mode = 2
|
||||||
|
text = "Outpost"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="OutpostCost" type="Control" parent="UI/BuildMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -60
|
||||||
|
offset_right = 60
|
||||||
|
offset_top = 1034
|
||||||
|
offset_bottom = 1086
|
||||||
|
script = ExtResource("19")
|
||||||
|
amount = 30
|
||||||
|
digit_height = 40.0
|
||||||
|
|
||||||
[node name="CloseButton" type="Control" parent="UI/BuildMenu"]
|
[node name="CloseButton" type="Control" parent="UI/BuildMenu"]
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
|
|
@ -2060,6 +2103,62 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
color = Color(0, 0, 0, 0)
|
color = Color(0, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="GarrisonMenu" type="Control" parent="UI" groups=["modal_menus"]]
|
||||||
|
visible = false
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
|
||||||
|
[node name="Backdrop" type="ColorRect" parent="UI/GarrisonMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
color = Color(0.03, 0.03, 0.08, 0.94)
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="UI/GarrisonMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = 24.0
|
||||||
|
offset_top = 20.0
|
||||||
|
offset_right = -200.0
|
||||||
|
offset_bottom = 82.0
|
||||||
|
text = "Outpost Garrison"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="Panel" type="Control" parent="UI/GarrisonMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = 40.0
|
||||||
|
offset_top = 120.0
|
||||||
|
offset_right = -40.0
|
||||||
|
offset_bottom = 620.0
|
||||||
|
script = ExtResource("33")
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="UI/GarrisonMenu"]
|
||||||
|
mouse_filter = 2
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 32.0
|
||||||
|
offset_top = -160.0
|
||||||
|
offset_right = -32.0
|
||||||
|
offset_bottom = -24.0
|
||||||
|
theme_override_font_sizes/font_size = 30
|
||||||
|
autowrap_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="CloseButton" type="Control" parent="UI/GarrisonMenu"]
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -140.0
|
||||||
|
offset_top = 20.0
|
||||||
|
offset_right = -24.0
|
||||||
|
offset_bottom = 136.0
|
||||||
|
script = ExtResource("10")
|
||||||
|
icon = 1
|
||||||
|
glow_color = Color(0.72, 0.86, 0.45, 1)
|
||||||
|
|
||||||
[node name="SkillMenu" type="Control" parent="UI" groups=["modal_menus"]]
|
[node name="SkillMenu" type="Control" parent="UI" groups=["modal_menus"]]
|
||||||
visible = false
|
visible = false
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue