019f434995
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>
322 lines
13 KiB
GDScript
322 lines
13 KiB
GDScript
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
|