Files
BadQuanta 1e8ebb7567 Added gameplay_input manager.
Refactored menus, player, and escape_capsule to utilize new system.
Actions defined in Godot project file with default bindings.
Player/Vehicles now react to actions and GameplayInput states instead of polling Input directly.
They respond to commands via `_handle_gameplay_command` and should not implement _unhandled_input directly.
Kept some debugging input handling code in `player.gd` for things like F3/F5/F4/PageUp/PageDown.
We may want to move much of this out of `player.gd` into something that handles the gamestate more generally.
2026-10-06 02:19:50 +00:00

475 lines
18 KiB
GDScript

extends Control
## Look preview + culture/job lore. Compact grid. No Dock Chaplain.
var name_input: LineEdit
var background_list: Control
var confirm_button: Button
var back_button: Button
var description_label: Label
var bonus_label: Label
var culture_list: Control
var look_world: Node3D
var preview_cam: Camera3D
var sex_male: Button
var sex_female: Button
var name_head: Label
var name_body: Label
var name_legs: Label
var name_feet: Label
const MALE_FBX := "res://character assets/All together/FBX/Humans_Master.fbx"
const FEMALE_FBX := "res://character assets/All together/FBX women/All together.fbx"
const MALE_HEADS := ["Casual_Head", "Casual2_Head", "Worker_Head", "Farmer_Head", "Adventurer_Head", "Punk_Head", "Suit_Head", "Swat_Head", "SpaceSuit_Head"]
const MALE_BODIES := ["Casual_Body", "Casual2_Body", "Worker_Body", "Farmer_Body", "Adventurer_Body", "Punk_Body", "Suit_Body", "Swat_Body", "SpaceSuit_Body"]
const MALE_LEGS := ["Casual_Legs", "Casual2_Legs", "Worker_Legs", "Farmer_Pants", "Adventurer_Legs", "Punk_Legs", "Suit_Legs", "Swat_Legs", "SpaceSuit_Legs"]
const MALE_FEET := ["Casual_Feet", "Casual2_Feet", "Worker_Feet", "Farmer_Feet", "Adventurer_Feet", "Punk_Feet", "Suit_Feet", "Swat_Feet", "SpaceSuit_Feet"]
const FEMALE_HEADS := ["Casual_Head", "Worker_Head", "Suit_Head", "Formal_Head", "Formad_Head", "Adventurer_Head", "Punk_Head", "Soldier_Head", "SciFi_Head"]
const FEMALE_BODIES := ["Casual_Body", "Worker_Body", "Suit_Body", "Formal_Body", "Adventurer_Body", "Punk_Body", "Soldier_Body", "SciFi_Body"]
const FEMALE_LEGS := ["Casual_Legs", "Worker_Legs", "Suit_Legs", "Formal_Legs", "Adventurer_Legs", "Punk_Legs", "Soldier_Legs", "SciFi_Legs"]
const FEMALE_FEET := ["Casual_Feet", "Worker_Feet", "Suit_Feet", "Formal_Feet", "Adventurer_Feet", "Punk_Feet", "Soldier_Feet", "SciFi_Feet"]
var sex: String = "male"
var idx := {"head": 0, "body": 0, "legs": 0, "feet": 0}
var preview_male: Node3D = null
var preview_female: Node3D = null
var cultures := [
{"id": "naf", "name": "North American", "desc": "You grew up under the NAF eagle — Mexico to Panama welded into one machine. English and Spanish in the same breath, carriers in the black, and a simple article of faith: the hemisphere holds, or nothing does."},
{"id": "eaa", "name": "East Asian", "desc": "EAA orthodoxy: long plans, short mercy. Beijing's shadow, Korean steel, Vietnamese patience. You learned early that the loudest claim in the room is rarely the one that survives."},
{"id": "european", "name": "European", "desc": "Old flags, thin coalitions, and the sound of Russia testing the locks. Paris, Berlin, London, Warsaw — still arguing while the sky hardens. You left with a ticket and no illusions about unity."},
{"id": "russian", "name": "Russian", "desc": "Victory parades and rationed truth. Ukraine was the lesson; the Baltics are the rumor that keeps generals warm. Orthodox steel, long winters, and a state that still treats the map as unfinished business."},
{"id": "brazilian", "name": "Brazilian", "desc": "Southern empire blood. Industry, orbit yards, and a continent that refuses to kneel to the north. You carry the heat of a country that builds its own guns and names its own heroes."},
{"id": "independent", "name": "Independent", "desc": "No anthem owns you. Station brats, belt orphans, passport ghosts — you answer to contracts, crew, and the thin margin between air and vacuum."}
]
var backgrounds := [
{"id": "dock_worker", "name": "Dock Worker", "desc": "Hard vacuum on one side of the hatch, warehouse stink on the other. You stacked the mass that keeps Young alive and learned every lie a shipping manifest can tell.", "bonuses": {"money": 90}},
{"id": "junior_engineer", "name": "Junior Engineer", "desc": "When the lights brown out, they call you. Breakers, scrubbers, jury-rigged miracles — you keep the arcology's pulse steady with a multitool and bad coffee.", "bonuses": {"money": 100}},
{"id": "med_tech", "name": "Med Tech", "desc": "Blood on the deck plating, timers on the monitors. You stabilize what the station breaks and don't ask whether the patient salutes the same flag you do.", "bonuses": {"money": 95, "health": 10}},
{"id": "smuggler", "name": "Smuggler", "desc": "Customs is a suggestion; the real map is drawn in debts. You moved what official channels refused to name — and you got paid in silence and credits.", "bonuses": {"money": 120}},
{"id": "security_cadet", "name": "Security Cadet", "desc": "Badge, route, rules of engagement written by people who sleep in better modules. You've already learned how fast procedure burns when someone runs.", "bonuses": {"money": 85, "health": 5}},
{"id": "botanist", "name": "Botanist", "desc": "Oxygen has a green face. You worked the trays and towers that turn waste into breath — the quietest strategic asset on the Moon.", "bonuses": {"money": 80, "food": 15}},
{"id": "skiff_pilot", "name": "Skiff Pilot", "desc": "Low and fast over dead regolith. Hop routes, run supplies, and trust the aerospace skills that keep you off the crater floor.", "bonuses": {"money": 105}},
{"id": "life_support_tech", "name": "Life Support Tech", "desc": "Pressure, CO₂, water loops — the boring systems that kill everyone when they fail. You listened to the station the way a doctor listens to a chest.", "bonuses": {"money": 95}},
{"id": "university_aide", "name": "University Aide", "desc": "Tsiolkovsky Hall: fusion on the chalkboard, rumors in the aisle. Dr Ernesto talks brachistochrones like scripture. You fetched the data and heard the future early.", "bonuses": {"money": 75}},
{"id": "unemployed", "name": "Unemployed", "desc": "No contract, no module lease, no one to vouch. Just a name on a transit list and a short window to become necessary.", "bonuses": {"money": 40}}
]
var selected_culture: Dictionary = {}
var selected_background: Dictionary = {}
var background_buttons: Array[Button] = []
var culture_buttons: Array[Button] = []
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
GameplayInput.set_menu_open(self, true)
name_input = _find("NameInput") as LineEdit
background_list = _find("BackgroundL") as Control
if background_list == null:
background_list = _find("BackgroundList") as Control
confirm_button = _find("ConfirmButton") as Button
back_button = _find("BackButton") as Button
description_label = _find("DescriptionL") as Label
if description_label == null:
description_label = _find("DescriptionLabel") as Label
bonus_label = _find("BonusLabel") as Label
culture_list = _find("CultureList") as Control
look_world = _find("LookWorld") as Node3D
preview_cam = _find("LookCam") as Camera3D
sex_male = _find("SexMale") as Button
sex_female = _find("SexFemale") as Button
name_head = _find("Name_Head") as Label
name_body = _find("Name_Body") as Label
name_legs = _find("Name_Legs") as Label
name_feet = _find("Name_Feet") as Label
var vp := _find("SubViewport") as SubViewport
if vp:
vp.own_world_3d = true
vp.transparent_bg = false
vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS
if description_label:
description_label.visible = true
description_label.add_theme_font_size_override("font_size", 14)
description_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description_label.custom_minimum_size = Vector2(0, 88)
description_label.max_lines_visible = 8
description_label.clip_text = false
description_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
if bonus_label:
bonus_label.visible = true
bonus_label.add_theme_font_size_override("font_size", 14)
bonus_label.custom_minimum_size = Vector2(0, 22)
bonus_label.max_lines_visible = 2
bonus_label.clip_text = false
if confirm_button:
confirm_button.pressed.connect(_on_confirm)
if back_button:
back_button.pressed.connect(_on_back)
if sex_male:
sex_male.pressed.connect(func() -> void: _set_sex("male"))
if sex_female:
sex_female.pressed.connect(func() -> void: _set_sex("female"))
_wire_cycle("Prev_Head", "Next_Head", "head")
_wire_cycle("Prev_Body", "Next_Body", "body")
_wire_cycle("Prev_Legs", "Next_Legs", "legs")
_wire_cycle("Prev_Feet", "Next_Feet", "feet")
if culture_list is GridContainer:
(culture_list as GridContainer).columns = 3
if background_list is GridContainer:
(background_list as GridContainer).columns = 2
_wire_culture_buttons()
_spawn_previews()
_build_background_buttons()
if cultures.size() > 0:
_select_culture(cultures[0])
if backgrounds.size() > 0:
_select_background(backgrounds[0])
_set_sex("male")
if sex_male:
sex_male.grab_focus()
## Controller cancel follows the existing back command without changing creation data.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
get_viewport().set_input_as_handled()
_on_back()
## Releases menu ownership when entering gameplay or returning to the main menu.
func _exit_tree() -> void:
GameplayInput.set_menu_open(self, false)
func _find(n: String) -> Node:
return find_child(n, true, false)
func _size_grid_button(btn: Button) -> void:
btn.custom_minimum_size = Vector2(0, 24)
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
btn.size_flags_vertical = Control.SIZE_SHRINK_CENTER
func _wire_cycle(prev_name: String, next_name: String, slot: String) -> void:
var prev_b := _find(prev_name) as Button
var next_b := _find(next_name) as Button
if prev_b:
prev_b.pressed.connect(func() -> void: _cycle(slot, -1))
if next_b:
next_b.pressed.connect(func() -> void: _cycle(slot, 1))
func _wire_culture_buttons() -> void:
culture_buttons.clear()
if culture_list == null:
return
var map := {
"Culture_NA": "naf", "Culture_NAF": "naf",
"Culture_EA": "eaa", "Culture_EAA": "eaa",
"Culture_Eur": "european", "Culture_European": "european",
"Culture_Ru": "russian", "Culture_Russian": "russian",
"Culture_Br": "brazilian", "Culture_Brazilian": "brazilian",
"Culture_In": "independent", "Culture_Independent": "independent",
}
for child in culture_list.get_children():
if not (child is Button):
continue
var btn := child as Button
var cid: String = str(map.get(btn.name, ""))
if cid.is_empty():
continue
var data: Dictionary = {}
for c in cultures:
if c["id"] == cid:
data = c
break
if data.is_empty():
continue
btn.toggle_mode = true
_size_grid_button(btn)
btn.pressed.connect(_on_culture_pressed.bind(data, btn))
culture_buttons.append(btn)
func _on_culture_pressed(data: Dictionary, btn: Button) -> void:
_select_culture(data)
for b in culture_buttons:
b.button_pressed = (b == btn)
func _select_culture(data: Dictionary) -> void:
selected_culture = data
_refresh_desc()
func _build_background_buttons() -> void:
if background_list == null:
push_error("BackgroundL / BackgroundList missing — jobs cannot spawn")
return
for child in background_list.get_children():
child.queue_free()
background_buttons.clear()
for bg in backgrounds:
var btn := Button.new()
btn.text = str(bg["name"])
btn.toggle_mode = true
_size_grid_button(btn)
btn.pressed.connect(_on_background_pressed.bind(bg, btn))
background_list.add_child(btn)
background_buttons.append(btn)
func _on_background_pressed(bg: Dictionary, btn: Button) -> void:
_select_background(bg)
for b in background_buttons:
b.button_pressed = (b == btn)
func _select_background(bg: Dictionary) -> void:
selected_background = bg
_refresh_desc()
if bonus_label:
var bonuses: Dictionary = bg.get("bonuses", {})
var money: int = int(bonuses.get("money", 0))
var extra: PackedStringArray = []
if bonuses.has("food"):
extra.append("Food +%s" % str(bonuses["food"]))
if bonuses.has("health"):
extra.append("Health +%s" % str(bonuses["health"]))
var extra_txt := (", " + ", ".join(extra)) if extra.size() > 0 else ""
bonus_label.text = "Starting bonus: %d credits%s" % [money, extra_txt]
func _refresh_desc() -> void:
var parts: PackedStringArray = []
if not selected_culture.is_empty():
parts.append(str(selected_culture.get("desc", "")))
if not selected_background.is_empty():
parts.append(str(selected_background.get("desc", "")))
var body := "\n\n".join(parts)
if description_label:
description_label.visible = true
description_label.text = body
if bonus_label == null and description_label and not selected_background.is_empty():
var bonuses: Dictionary = selected_background.get("bonuses", {})
var money: int = int(bonuses.get("money", 0))
description_label.text = body + "\n\nStarting bonus: %d credits" % money
func _spawn_previews() -> void:
if look_world == null:
push_warning("LookWorld missing — Look preview will be empty")
return
_ensure_look_stage()
preview_male = _instance_fbx(MALE_FBX)
preview_female = _instance_fbx(FEMALE_FBX)
if preview_male:
preview_male.name = "PreviewMale"
look_world.add_child(preview_male)
if preview_female:
preview_female.name = "PreviewFemale"
look_world.add_child(preview_female)
print("Look spawn male=", preview_male, " female=", preview_female, " cam=", preview_cam)
if preview_cam:
preview_cam.current = true
preview_cam.position = Vector3(0.0, 1.08, 5.4)
preview_cam.rotation_degrees = Vector3(-4.0, 0.0, 0.0)
preview_cam.fov = 34.0
func _ensure_look_stage() -> void:
if look_world.get_node_or_null("LookFill") == null:
var fill := OmniLight3D.new()
fill.name = "LookFill"
fill.light_energy = 1.6
fill.omni_range = 14.0
fill.position = Vector3(0.5, 2.0, 2.2)
look_world.add_child(fill)
if look_world.get_node_or_null("LookEnv") == null:
var we := WorldEnvironment.new()
we.name = "LookEnv"
var env := Environment.new()
env.background_mode = Environment.BG_COLOR
env.background_color = Color(0.10, 0.11, 0.13)
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(0.78, 0.78, 0.82)
env.ambient_light_energy = 1.0
we.environment = env
look_world.add_child(we)
func _instance_fbx(path: String) -> Node3D:
if not ResourceLoader.exists(path):
push_warning("Missing character file: " + path)
return null
var res = load(path)
if res == null:
return null
var inst: Node = res.instantiate()
if inst is Node3D:
(inst as Node3D).position = Vector3.ZERO
(inst as Node3D).rotation_degrees = Vector3.ZERO
(inst as Node3D).scale = Vector3.ONE
return inst as Node3D
return null
func _lists() -> Dictionary:
if sex == "male":
return {"head": MALE_HEADS, "body": MALE_BODIES, "legs": MALE_LEGS, "feet": MALE_FEET}
return {"head": FEMALE_HEADS, "body": FEMALE_BODIES, "legs": FEMALE_LEGS, "feet": FEMALE_FEET}
func _active_preview() -> Node3D:
return preview_male if sex == "male" else preview_female
func _set_sex(s: String) -> void:
sex = s
idx = {"head": 0, "body": 0, "legs": 0, "feet": 0}
if preview_male:
preview_male.visible = (s == "male")
if preview_female:
preview_female.visible = (s == "female")
_apply_look()
func _cycle(slot: String, dir: int) -> void:
var arr: Array = _lists()[slot]
if arr.is_empty():
return
idx[slot] = posmod(idx[slot] + dir, arr.size())
_apply_look()
func _apply_look() -> void:
var root := _active_preview()
if root == null:
return
var lists := _lists()
var keep: Array[String] = [
str(lists["head"][idx["head"]]),
str(lists["body"][idx["body"]]),
str(lists["legs"][idx["legs"]]),
str(lists["feet"][idx["feet"]]),
]
_hide_all_geometry(root)
for mesh_name in keep:
_show_mesh(root, mesh_name)
_hide_other_slots(root, keep)
if name_head:
name_head.text = lists["head"][idx["head"]].replace("_Head", "").replace("Formad", "Formal")
if name_body:
name_body.text = lists["body"][idx["body"]].replace("_Body", "")
if name_legs:
name_legs.text = lists["legs"][idx["legs"]].replace("_Legs", "").replace("_Pants", "")
if name_feet:
name_feet.text = lists["feet"][idx["feet"]].replace("_Feet", "")
_play_idle(root)
func _hide_all_geometry(root: Node) -> void:
var stack: Array[Node] = [root]
while stack.size() > 0:
var n: Node = stack.pop_back()
if n is VisualInstance3D:
n.visible = false
for c in n.get_children():
stack.append(c)
func _show_mesh(root: Node, mesh_name: String) -> void:
var node := root.find_child(mesh_name, true, false)
if node == null:
return
_show_tree(node)
func _show_tree(n: Node) -> void:
n.visible = true
if n is VisualInstance3D:
n.visible = true
for c in n.get_children():
_show_tree(c)
func _hide_other_slots(root: Node, keep: Array[String]) -> void:
var stack: Array[Node] = [root]
while stack.size() > 0:
var n: Node = stack.pop_back()
if _is_slot_root(n.name) and keep.find(n.name) == -1:
_hide_tree(n)
else:
for c in n.get_children():
stack.append(c)
func _hide_tree(n: Node) -> void:
n.visible = false
if n is VisualInstance3D:
n.visible = false
for c in n.get_children():
_hide_tree(c)
func _is_slot_root(mesh_name: String) -> bool:
return (
mesh_name.ends_with("_Head")
or mesh_name.ends_with("_Body")
or mesh_name.ends_with("_Legs")
or mesh_name.ends_with("_Feet")
or mesh_name.ends_with("_Pants")
)
func _play_idle(root: Node) -> void:
var anim: AnimationPlayer = root.find_child("AnimationPlayer", true, false)
if anim == null:
return
for clip in ["CharacterArmature|Idle", "Idle"]:
if anim.has_animation(clip):
anim.play(clip)
return
func _look_payload() -> Dictionary:
var lists := _lists()
return {
"sex": sex,
"head": lists["head"][idx["head"]],
"body": lists["body"][idx["body"]],
"legs": lists["legs"][idx["legs"]],
"feet": lists["feet"][idx["feet"]],
}
func _on_confirm() -> void:
var player_name := "Colonist"
if name_input:
player_name = name_input.text.strip_edges()
if player_name.is_empty():
player_name = "Colonist"
if selected_background.is_empty() and backgrounds.size() > 0:
selected_background = backgrounds[0]
if selected_culture.is_empty() and cultures.size() > 0:
selected_culture = cultures[0]
var data := {
"player_name": player_name,
"appearance": _look_payload(),
"culture_id": selected_culture.get("id", ""),
"culture_name": selected_culture.get("name", ""),
"background_id": selected_background.get("id", ""),
"background_name": selected_background.get("name", ""),
"bonuses": selected_background.get("bonuses", {}),
}
Engine.set_meta("player_creation_data", data)
print("Creation packed: ", data)
get_tree().change_scene_to_file("res://Scenes/main.tscn")
func _on_back() -> void:
get_tree().change_scene_to_file("res://Scenes/main_menu.tscn")