Files
client/spacewar/menu/team_select.gd
T
anekdotin 019f434995 Per-ship scale/speed factors and updated race/class doc
Give each ship an explicit sprite scale (tuned per role so an
Interceptor/Gunship/Bomber/Support/Heavy reads the same size across
all 3 races, then reduced another 25% per feedback that ships felt
oversized) and a movement speed_factor pulled from a shared
SPEED_BY_ROLE table, so e.g. every race's Interceptor moves at the
same speed as every other race's. GameConfig carries both through to
ship_movement.gd, which now scales thrust/max_speed by the chosen
ship's factor.

Rewrite overview/racesclasses.md to match what's actually implemented:
real ship names/roles for Terran Republic and Vorg Swarm, fill in the
previously-undocumented Mechanos Sovereignty roster, and correct the
chosen-race list now that Vorg Swarm (bio/acid swarm, matching the
uploaded art) replaced Void Wraiths as the third chosen race.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 23:14:30 -04:00

322 lines
13 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends CanvasLayer
# Movement speed factor by role, shared across all 3 races so e.g. every
# race's Interceptor moves at the same speed as every other race's.
const SPEED_BY_ROLE := {
"Interceptor": 1.3,
"Gunship": 1.15,
"Bomber": 1.0,
"Support": 0.9,
"Heavy": 0.7,
}
const RACES := [
{
"id": 1, "name": "TERRAN REPUBLIC", "sub": "Angular gray-blue military ships, standard balanced fighters",
"color": Color(0.40, 0.60, 0.85),
"ships": [
{
"name": "WARFRAME", "role": "Interceptor",
"desc": "Very fast, rapid weak laser bursts. Built to close distance instantly and chip away at anything in range. Low individual hit strength but a relentless rate of fire punishes anyone who stands still.",
"path": "res://assets/images/ships/terran/warframe.png", "scale": 0.229, "speed_factor": SPEED_BY_ROLE.Interceptor,
},
{
"name": "STRIKER", "role": "Gunship",
"desc": "Super fast fire rate, very low damage lasers. Trades hitting power for volume — the more shots in the air, the more that connect. Best flown aggressively, applying constant pressure rather than picking single shots.",
"path": "res://assets/images/ships/terran/striker.png", "scale": 0.244, "speed_factor": SPEED_BY_ROLE.Gunship,
},
{
"name": "DEPLOYER", "role": "Bomber",
"desc": "Fast lasers plus an EMP mine dropped in your wake. Punishes anyone chasing too closely and disables systems on contact. Strong for area denial and covering a retreat.",
"path": "res://assets/images/ships/terran/deployer.png", "scale": 0.265, "speed_factor": SPEED_BY_ROLE.Bomber,
},
{
"name": "LINK NODE", "role": "Support",
"desc": "Standard lasers plus a small repair pack dropped for allies to pick up. Keeps a team topped off in prolonged fights. Not built to duel alone — stay near the fight, not in the middle of it.",
"path": "res://assets/images/ships/terran/link_node.png", "scale": 0.329, "speed_factor": SPEED_BY_ROLE.Support,
},
{
"name": "BASTION", "role": "Heavy",
"desc": "Tankier and slower, but its laser hits harder than anything else in the fleet. Built to anchor a push and absorb damage while dealing it back. Discourages anyone from engaging head-on.",
"path": "res://assets/images/ships/terran/bastion.png", "scale": 0.283, "speed_factor": SPEED_BY_ROLE.Heavy,
},
],
},
{
"id": 2, "name": "MECHANOS SOVEREIGNTY", "sub": "Industrial war machines built for brute-force firepower",
"color": Color(0.85, 0.28, 0.28),
"ships": [
{
"name": "SENTINEL", "role": "Interceptor",
"desc": "A light scout mech with twin rapid cannons. Fast and maneuverable for a Mechanos hull, built to screen the front line and harass before the heavier machines arrive.",
"path": "res://assets/images/ships/mech/sentinel.png", "scale": 0.205, "speed_factor": SPEED_BY_ROLE.Interceptor,
},
{
"name": "VANGUARD", "role": "Gunship",
"desc": "Dual autocannons built for sustained fire. Sits between the light scouts and the heavy machines — durable enough to hold a line, fast enough to reposition when it breaks.",
"path": "res://assets/images/ships/mech/vanguard.png", "scale": 0.266, "speed_factor": SPEED_BY_ROLE.Gunship,
},
{
"name": "RAVAGER", "role": "Bomber",
"desc": "Drops heavy ordnance and mines from its cargo pods. Slow to turn but devastating to anything caught in the blast radius. Best used to seal off a chokepoint.",
"path": "res://assets/images/ships/mech/ravager.png", "scale": 0.25, "speed_factor": SPEED_BY_ROLE.Bomber,
},
{
"name": "AEGIS", "role": "Support",
"desc": "Projects a short-range shield and deploys a repair drone for nearby allies. Doesn't hit hard on its own — its job is keeping everyone else alive long enough to win the fight.",
"path": "res://assets/images/ships/mech/aegis.png", "scale": 0.248, "speed_factor": SPEED_BY_ROLE.Support,
},
{
"name": "COLOSSUS", "role": "Heavy",
"desc": "A walking fortress bristling with cannons. Slow and telegraphed, but absorbs enormous damage and returns it with interest. Usually the last thing standing in an engagement.",
"path": "res://assets/images/ships/mech/colossus.png", "scale": 0.252, "speed_factor": SPEED_BY_ROLE.Heavy,
},
],
},
{
"id": 3, "name": "VORG SWARM", "sub": "Organic swarm built for chaotic damage-over-time",
"color": Color(0.45, 0.80, 0.35),
"ships": [
{
"name": "STINGER", "role": "Interceptor",
"desc": "Fast, acid shots with light damage-over-time. Built to swarm a target from range, applying just enough acid to wear an enemy down before backing off.",
"path": "res://assets/images/ships/vorg/stinger.png", "scale": 0.254, "speed_factor": SPEED_BY_ROLE.Interceptor,
},
{
"name": "SPITTER", "role": "Gunship",
"desc": "Faster-firing acid with a stronger damage-over-time effect. Trades some speed for sustained corrosive pressure — anything it tags keeps taking damage long after contact.",
"path": "res://assets/images/ships/vorg/spitter.png", "scale": 0.247, "speed_factor": SPEED_BY_ROLE.Gunship,
},
{
"name": "BROODLING", "role": "Bomber",
"desc": "Normal acid shots that also spawn a small slowing swarm cloud. The cloud lingers, fouling movement for anyone caught inside it — ideal for locking down a retreat or a chokepoint.",
"path": "res://assets/images/ships/vorg/broodling.png", "scale": 0.404, "speed_factor": SPEED_BY_ROLE.Bomber,
},
{
"name": "NURSE", "role": "Support",
"desc": "Acid shots plus a small health/regeneration pack dropped for allies. Keeps the swarm alive in extended fights, at the cost of raw offensive power.",
"path": "res://assets/images/ships/vorg/nurse.png", "scale": 0.271, "speed_factor": SPEED_BY_ROLE.Support,
},
{
"name": "BEHEMOTH", "role": "Heavy",
"desc": "Tankier and slower, with heavy acid shots that carry a large damage-over-time effect. The swarm's apex predator — absorbs punishment while its acid keeps eating away at anyone nearby.",
"path": "res://assets/images/ships/vorg/behemoth.png", "scale": 0.171, "speed_factor": SPEED_BY_ROLE.Heavy,
},
],
},
]
# Row layout constants (screen is 1280×800)
const ROW_X1 := 120.0
const ROW_X2 := 1160.0
const ROW_H := 100.0
const ROW_GAP := 8.0
const ROW_START := 195.0
var _root: Control
var _content: Control
var _offered: Array = []
var _chosen_race: Dictionary = {}
func _ready() -> void:
layer = 20
_offered = _pick_two_races()
_build_root()
_show_race_selection()
func _pick_two_races() -> Array:
var pool := RACES.duplicate()
pool.shuffle()
return [pool[0], pool[1]]
func _build_root() -> void:
_root = Control.new()
_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(_root)
var overlay := ColorRect.new()
overlay.color = Color(0.0, 0.0, 0.0, 0.65)
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
overlay.mouse_filter = Control.MOUSE_FILTER_STOP
_root.add_child(overlay)
func _swap_content() -> Control:
if _content:
_content.queue_free()
_content = Control.new()
_content.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_content.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_content)
return _content
# ── Race selection ───────────────────────────────────────────────────────────
func _show_race_selection() -> void:
var c := _swap_content()
_label(c, "CHOOSE YOUR FACTION", 32, Color(0.42, 0.87, 1.0),
0, 105, 1280, 160, HORIZONTAL_ALIGNMENT_CENTER)
_label(c, "This match: %s vs %s" % [_offered[0].name, _offered[1].name],
13, Color(0.45, 0.58, 0.72),
0, 170, 1280, 200, HORIZONTAL_ALIGNMENT_CENTER)
var left_btn := _make_race_btn(_offered[0])
_place(left_btn, 150, 225, 590, 540)
left_btn.pressed.connect(_on_race_chosen.bind(_offered[0]))
c.add_child(left_btn)
var right_btn := _make_race_btn(_offered[1])
_place(right_btn, 690, 225, 1130, 540)
right_btn.pressed.connect(_on_race_chosen.bind(_offered[1]))
c.add_child(right_btn)
func _on_race_chosen(race: Dictionary) -> void:
_chosen_race = race
GameConfig.player_race = race.id
_show_ship_selection()
# ── Ship selection ───────────────────────────────────────────────────────────
func _show_ship_selection() -> void:
var c := _swap_content()
_label(c, "SELECT YOUR SHIP", 30, Color(0.42, 0.87, 1.0),
0, 90, 1280, 145, HORIZONTAL_ALIGNMENT_CENTER)
_label(c, "Playing as — " + _chosen_race.name, 14, _chosen_race.color,
0, 155, 1280, 185, HORIZONTAL_ALIGNMENT_CENTER)
var ships: Array = _chosen_race.ships
for i in ships.size():
var y := ROW_START + i * (ROW_H + ROW_GAP)
var btn := _make_ship_row(ships[i], _chosen_race.color)
_place(btn, ROW_X1, y, ROW_X2, y + ROW_H)
btn.pressed.connect(_on_ship_chosen.bind(ships[i]))
c.add_child(btn)
func _on_ship_chosen(ship: Dictionary) -> void:
GameConfig.player_ship_path = ship.path
GameConfig.player_ship_scale = ship.scale
GameConfig.player_ship_speed_factor = ship.speed_factor
GameConfig.team_selected.emit()
queue_free()
# ── Button builders ──────────────────────────────────────────────────────────
func _make_race_btn(race: Dictionary) -> Button:
var btn := Button.new()
btn.text = ""
var col: Color = race.color
var sn := _flat(Color(col.r * 0.14, col.g * 0.14, col.b * 0.14, 0.96), 12)
sn.set_border_width_all(2)
sn.border_color = Color(col.r * 0.38, col.g * 0.38, col.b * 0.38)
var sh := _flat(Color(col.r * 0.24, col.g * 0.24, col.b * 0.24, 0.96), 12)
sh.set_border_width_all(2)
sh.border_color = col
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sh)
btn.add_theme_stylebox_override("pressed", sh)
var vb := VBoxContainer.new()
vb.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
vb.offset_left = 20; vb.offset_top = 24; vb.offset_right = -20; vb.offset_bottom = -24
vb.alignment = BoxContainer.ALIGNMENT_CENTER
vb.add_theme_constant_override("separation", 16)
vb.mouse_filter = Control.MOUSE_FILTER_IGNORE
btn.add_child(vb)
var name_lbl := Label.new()
name_lbl.text = race.name
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_lbl.add_theme_font_size_override("font_size", 28)
name_lbl.add_theme_color_override("font_color", col)
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(name_lbl)
var sep := HSeparator.new()
sep.add_theme_color_override("color", Color(col.r * 0.4, col.g * 0.4, col.b * 0.4))
sep.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(sep)
var sub_lbl := Label.new()
sub_lbl.text = race.sub
sub_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sub_lbl.add_theme_font_size_override("font_size", 15)
sub_lbl.add_theme_color_override("font_color", Color(0.60, 0.70, 0.82))
sub_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(sub_lbl)
var hint := Label.new()
hint.text = "CLICK TO SELECT"
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hint.add_theme_font_size_override("font_size", 11)
hint.add_theme_color_override("font_color", Color(col.r * 0.5, col.g * 0.5, col.b * 0.5))
hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
vb.add_child(hint)
return btn
func _make_ship_row(ship: Dictionary, accent: Color) -> Button:
var btn := Button.new()
btn.text = ""
var sn := _flat(Color(0.06, 0.09, 0.17, 0.95), 8)
sn.set_border_width_all(1)
sn.border_color = Color(0.18, 0.28, 0.48, 0.6)
var sh := _flat(Color(0.10, 0.16, 0.28, 0.97), 8)
sh.set_border_width_all(2)
sh.border_color = accent
btn.add_theme_stylebox_override("normal", sn)
btn.add_theme_stylebox_override("hover", sh)
btn.add_theme_stylebox_override("pressed", sh)
var tex := TextureRect.new()
tex.texture = load(ship.path)
tex.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
tex.offset_left = 8; tex.offset_top = 6; tex.offset_right = -8; tex.offset_bottom = -6
tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tex.expand_mode = TextureRect.EXPAND_FIT_HEIGHT_PROPORTIONAL
tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
btn.add_child(tex)
return btn
# ── Helpers ──────────────────────────────────────────────────────────────────
func _label(parent: Control, text: String, font_size: int, color: Color,
x1: float, y1: float, x2: float, y2: float,
align: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT) -> void:
var lbl := Label.new()
lbl.text = text
lbl.horizontal_alignment = align
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
lbl.add_theme_font_size_override("font_size", font_size)
lbl.add_theme_color_override("font_color", color)
_place(lbl, x1, y1, x2, y2)
parent.add_child(lbl)
func _place(node: Control, x1: float, y1: float, x2: float, y2: float) -> void:
node.set_anchor_and_offset(SIDE_LEFT, 0.0, x1)
node.set_anchor_and_offset(SIDE_TOP, 0.0, y1)
node.set_anchor_and_offset(SIDE_RIGHT, 0.0, x2)
node.set_anchor_and_offset(SIDE_BOTTOM, 0.0, y2)
func _flat(col: Color, radius: int = 0) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = col
s.set_corner_radius_all(radius)
return s