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 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") 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")