Initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var health_restore: float = 25.0
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_health"):
|
||||
player.add_health(health_restore)
|
||||
print("You rest for a while. Health restored.")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2cfgmuwrsr3y
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Beer Can"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://4dys8ga8ap02
|
||||
@@ -0,0 +1,16 @@
|
||||
extends Camera3D
|
||||
|
||||
@onready var capsule: Node3D = get_node("/root/Node3D/EscapeCapsule")
|
||||
|
||||
func _physics_process(_delta):
|
||||
if not capsule:
|
||||
return
|
||||
|
||||
var nose_dir = capsule.global_transform.basis.y
|
||||
var up_dir = -capsule.global_transform.basis.z
|
||||
|
||||
global_position = capsule.global_position + nose_dir * 4.0
|
||||
look_at(capsule.global_position + nose_dir * 30.0, up_dir)
|
||||
|
||||
# Fix upside-down
|
||||
rotate_object_local(Vector3.FORWARD, deg_to_rad(180))
|
||||
@@ -0,0 +1 @@
|
||||
uid://di78d53y38p67
|
||||
@@ -0,0 +1,66 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
# How fast the player moves in meters per second.
|
||||
@export var speed = 14
|
||||
# The downward acceleration when in the air, in meters per second squared.
|
||||
@export var fall_acceleration = 75
|
||||
@export var mouse_sensitivity = 0.002
|
||||
const JUMP_VELOCITY = 25
|
||||
|
||||
var target_velocity = Vector3.ZERO
|
||||
# Reference to your camera pivot or camera
|
||||
@onready var pivot = $Pivot
|
||||
|
||||
func _ready():
|
||||
# Captures the mouse cursor so it stays in the game window
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventMouseMotion:
|
||||
rotate_y(-event.relative.x * mouse_sensitivity)
|
||||
pivot.rotate_x(-event.relative.y * mouse_sensitivity)
|
||||
pivot.rotation.x = clamp(pivot.rotation.x, deg_to_rad(-60), deg_to_rad(60))
|
||||
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
|
||||
func _physics_process(delta):
|
||||
var direction = Vector3.ZERO
|
||||
|
||||
# need to map to WASD to make it work with WASD
|
||||
if Input.is_action_pressed("ui_right"):
|
||||
direction.x += 1
|
||||
if Input.is_action_pressed("ui_left"):
|
||||
direction.x -= 1
|
||||
if Input.is_action_pressed("ui_down"):
|
||||
direction.z += 1
|
||||
if Input.is_action_pressed("ui_up"):
|
||||
direction.z -= 1
|
||||
|
||||
if direction != Vector3.ZERO:
|
||||
direction = direction.normalized()
|
||||
direction = (transform.basis * direction).normalized()
|
||||
|
||||
# Ground Velocity
|
||||
target_velocity.x = direction.x * speed
|
||||
target_velocity.z = direction.z * speed
|
||||
|
||||
# Vertical Velocity
|
||||
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
|
||||
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
|
||||
|
||||
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
|
||||
target_velocity.y = JUMP_VELOCITY
|
||||
|
||||
# Moving the Character
|
||||
velocity = target_velocity
|
||||
move_and_slide()
|
||||
for i in get_slide_collision_count():
|
||||
var collision = get_slide_collision(i)
|
||||
var collided_object = collision.get_collider()
|
||||
if collided_object.is_in_group("flag_group"):
|
||||
$win_label.set_text("You Win!")
|
||||
@@ -0,0 +1 @@
|
||||
uid://flm6ps54u0c4
|
||||
@@ -0,0 +1,459 @@
|
||||
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")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxrxrk0qt15dl
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Circuit Board"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0oppy8q64xry
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Cooking Knife"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cley0rs8im7v3
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Dried Noodles"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5j656olsdtjs
|
||||
@@ -0,0 +1,68 @@
|
||||
extends Node3D
|
||||
## Earth impostor — from Luna you are always far; show a disc inside far plane.
|
||||
|
||||
@export var real_earth_path: NodePath = ^".."
|
||||
@export var surface_path: NodePath = ^"../EarthSurface"
|
||||
@export var impostor_mesh_path: NodePath = ^"ImposterSphere"
|
||||
|
||||
@export var switch_distance: float = 150000.0
|
||||
@export var visual_distance: float = 120000.0
|
||||
@export var real_radius: float = 637100.0
|
||||
|
||||
var _real: Node3D
|
||||
var _surface: Node3D
|
||||
var _mesh: MeshInstance3D
|
||||
var _base_radius: float = 0.5
|
||||
|
||||
func _ready() -> void:
|
||||
_real = get_node_or_null(real_earth_path) as Node3D
|
||||
_surface = get_node_or_null(surface_path) as Node3D
|
||||
_mesh = get_node_or_null(impostor_mesh_path) as MeshInstance3D
|
||||
|
||||
if _mesh and _mesh.mesh is SphereMesh:
|
||||
_base_radius = maxf((_mesh.mesh as SphereMesh).radius, 0.5)
|
||||
|
||||
if _mesh:
|
||||
_mesh.visible = false
|
||||
# Keep YOUR material/texture; only force unshaded so it reads in space
|
||||
var src_mat: Material = _mesh.get_active_material(0)
|
||||
if src_mat is StandardMaterial3D:
|
||||
var mat: StandardMaterial3D = (src_mat as StandardMaterial3D).duplicate()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_mesh.material_override = mat
|
||||
|
||||
print("EarthImposter ready | real=", _real, " surface=", _surface, " mesh=", _mesh)
|
||||
set_process(true)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _real == null or _mesh == null:
|
||||
return
|
||||
|
||||
var viewer: Node3D = FloatingOrigin.focus
|
||||
if viewer == null or not is_instance_valid(viewer):
|
||||
_mesh.visible = false
|
||||
return
|
||||
|
||||
var to_body: Vector3 = _real.global_position - viewer.global_position
|
||||
var dist: float = to_body.length()
|
||||
if dist < 1.0:
|
||||
_mesh.visible = false
|
||||
return
|
||||
|
||||
# Close approach only: real surface
|
||||
if dist < switch_distance:
|
||||
_mesh.visible = false
|
||||
if _surface:
|
||||
_surface.visible = true
|
||||
return
|
||||
|
||||
# Far (normal from Luna): impostor on
|
||||
# Leave EarthSurface visible until you confirm the disc — then you can hide it
|
||||
var dir: Vector3 = to_body / dist
|
||||
_mesh.global_position = viewer.global_position + dir * visual_distance
|
||||
|
||||
var visual_radius: float = real_radius * (visual_distance / dist)
|
||||
visual_radius = clampf(visual_radius, 8000.0, 150000.0)
|
||||
_mesh.scale = Vector3.ONE * (visual_radius / _base_radius)
|
||||
_mesh.visible = true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ikbkmt7sby7a
|
||||
@@ -0,0 +1,632 @@
|
||||
extends CharacterBody3D
|
||||
## Kestrel EC-01 capsule — flight, cameras, enter/exit.
|
||||
## Hull clips + plumes live on child "Kestrel" via kestrel_driver.gd.
|
||||
|
||||
@onready var interaction_area: Area3D = $InteractionArea
|
||||
@onready var camera_pivot: Node3D = get_node_or_null("CameraPivot")
|
||||
@onready var capsule_camera: Camera3D = get_node_or_null("CameraPivot/CapsuleCamera")
|
||||
@onready var third_person_camera: Camera3D = $ThirdPersonCamera
|
||||
@onready var interact_prompt: Label = get_node_or_null("/root/Node3D/HUD/InteractPrompt")
|
||||
@onready var pause_menu: Control = get_node_or_null("/root/Node3D/HUD/PauseMenu")
|
||||
@onready var body_collision: CollisionShape3D = get_node_or_null("CollisionShape3D")
|
||||
@onready var flight_label: Label = get_node_or_null("/root/Node3D/HUD/FlightLabel")
|
||||
@onready var kestrel: Node3D = get_node_or_null("Kestrel")
|
||||
|
||||
var player_inside: bool = false
|
||||
var player_ref: CharacterBody3D = null
|
||||
var using_third_person: bool = false
|
||||
var f_was_down: bool = false
|
||||
|
||||
var mass: float = 6500.0
|
||||
var thrust_force: float = 180000.0
|
||||
var rotation_speed: float = 1.8
|
||||
var surface_friction: float = 6.0
|
||||
|
||||
const MOON_RADIUS: float = 173700.0
|
||||
const MOON_G: float = 1.62
|
||||
const MOON_SOI: float = 400000.0
|
||||
const EARTH_RADIUS: float = 637100.0
|
||||
const EARTH_G: float = 9.81
|
||||
const EARTH_SOI: float = 20000000.0
|
||||
const CAM_FAR: float = 1000000.0
|
||||
const NEAR_SURFACE_MARGIN: float = 300.0
|
||||
|
||||
var _moon_colliders: Array[CollisionObject3D] = []
|
||||
var _crust_exception: bool = false
|
||||
|
||||
var free_look: bool = false
|
||||
var look_sensitivity: float = 0.003
|
||||
var orbit_yaw: float = 0.0
|
||||
var orbit_pitch: float = 0.4
|
||||
var orbit_distance: float = 14.0
|
||||
var orbit_pitch_min: float = -1.2
|
||||
var orbit_pitch_max: float = 1.2
|
||||
var cockpit_yaw: float = 0.0
|
||||
var cockpit_pitch: float = 0.0
|
||||
var cockpit_look_limit_yaw: float = 1.45
|
||||
var cockpit_look_limit_up: float = 1.0
|
||||
var cockpit_look_limit_down: float = 0.55
|
||||
var head_local: Transform3D = Transform3D.IDENTITY
|
||||
|
||||
var telem_body: String = "—"
|
||||
var telem_alt: float = 0.0
|
||||
var telem_speed: float = 0.0
|
||||
var telem_vspeed: float = 0.0
|
||||
var telem_hspeed: float = 0.0
|
||||
var telem_vcirc: float = 0.0
|
||||
var telem_dh: float = 0.0
|
||||
var telem_earth_rng: float = 0.0
|
||||
var telem_thrust: bool = false
|
||||
var telem_has_alt: bool = false
|
||||
|
||||
var _gear_down: bool = true
|
||||
var _hatch_crew_open: bool = false
|
||||
var _hatch_dock_open: bool = false
|
||||
var _ladder_out: bool = false
|
||||
var _cabin_dim: bool = false
|
||||
var _v_was: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_ensure_actions()
|
||||
if interaction_area:
|
||||
interaction_area.monitoring = true
|
||||
interaction_area.monitorable = true
|
||||
interaction_area.body_entered.connect(_on_body_entered)
|
||||
interaction_area.body_exited.connect(_on_body_exited)
|
||||
if camera_pivot:
|
||||
camera_pivot.top_level = false
|
||||
head_local = camera_pivot.transform
|
||||
if capsule_camera:
|
||||
capsule_camera.near = 0.05
|
||||
capsule_camera.far = CAM_FAR
|
||||
capsule_camera.current = false
|
||||
if third_person_camera:
|
||||
third_person_camera.near = 0.1
|
||||
third_person_camera.far = CAM_FAR
|
||||
third_person_camera.current = false
|
||||
if flight_label:
|
||||
flight_label.visible = false
|
||||
_bind_kestrel()
|
||||
_collect_moon_colliders()
|
||||
call_deferred("_update_crust_exception")
|
||||
|
||||
|
||||
func _bind_kestrel() -> void:
|
||||
if kestrel == null:
|
||||
kestrel = get_node_or_null("Kestrel")
|
||||
print("Kestrel hull=", kestrel, " driver=", kestrel.get_script() if kestrel else null)
|
||||
|
||||
|
||||
func _hull() -> Node:
|
||||
return kestrel if kestrel else null
|
||||
|
||||
|
||||
func _set_hull(prop: String, value: Variant) -> void:
|
||||
var h := _hull()
|
||||
if h and prop in h:
|
||||
h.set(prop, value)
|
||||
|
||||
|
||||
func _moon() -> Node3D:
|
||||
return get_node_or_null("/root/Node3D/SolarSystem/Moon") as Node3D
|
||||
|
||||
|
||||
func _earth() -> Node3D:
|
||||
return get_node_or_null("/root/Node3D/SolarSystem/Earth") as Node3D
|
||||
|
||||
|
||||
func _habitat() -> Node3D:
|
||||
return get_node_or_null("/root/Node3D/SolarSystem/Moon/LovellAnchor/Station/Habitat") as Node3D
|
||||
|
||||
|
||||
func _collect_moon_colliders() -> void:
|
||||
_moon_colliders.clear()
|
||||
var moon := _moon()
|
||||
if moon == null:
|
||||
return
|
||||
for n in moon.find_children("*", "PhysicsBody3D", true, false):
|
||||
if n == self:
|
||||
continue
|
||||
if str(n.name).begins_with("Shell"):
|
||||
continue
|
||||
if str(n.name).begins_with("Moon") and n is CollisionObject3D:
|
||||
_moon_colliders.append(n as CollisionObject3D)
|
||||
|
||||
|
||||
func _inside_young() -> bool:
|
||||
var habitat := _habitat()
|
||||
if habitat == null:
|
||||
return false
|
||||
var lp: Vector3 = habitat.to_local(global_position)
|
||||
var radial: float = Vector2(lp.x, lp.z).length()
|
||||
return radial < 2000.0 and lp.y < 80.0 and lp.y > -120.0
|
||||
|
||||
|
||||
func _update_crust_exception() -> void:
|
||||
var want: bool = _inside_young()
|
||||
if want == _crust_exception:
|
||||
return
|
||||
_crust_exception = want
|
||||
for body in _moon_colliders:
|
||||
if not is_instance_valid(body):
|
||||
continue
|
||||
if want:
|
||||
add_collision_exception_with(body)
|
||||
else:
|
||||
remove_collision_exception_with(body)
|
||||
|
||||
|
||||
func _active_gravity() -> Dictionary:
|
||||
var moon := _moon()
|
||||
if moon:
|
||||
var d_m: float = global_position.distance_to(moon.global_position)
|
||||
if d_m >= 1.0 and d_m <= MOON_SOI:
|
||||
var to_c: Vector3 = moon.global_position - global_position
|
||||
return {
|
||||
"name": "Luna",
|
||||
"center": moon.global_position,
|
||||
"radius": MOON_RADIUS,
|
||||
"g": MOON_G,
|
||||
"accel": to_c.normalized() * MOON_G,
|
||||
"dist": d_m
|
||||
}
|
||||
var earth := _earth()
|
||||
if earth:
|
||||
var d_e: float = global_position.distance_to(earth.global_position)
|
||||
if d_e >= 1.0 and d_e <= EARTH_SOI:
|
||||
var to_c2: Vector3 = earth.global_position - global_position
|
||||
return {
|
||||
"name": "Earth",
|
||||
"center": earth.global_position,
|
||||
"radius": EARTH_RADIUS,
|
||||
"g": EARTH_G,
|
||||
"accel": to_c2.normalized() * EARTH_G,
|
||||
"dist": d_e
|
||||
}
|
||||
return {"name": "—", "center": Vector3.ZERO, "radius": 0.0, "g": 0.0, "accel": Vector3.ZERO, "dist": 0.0}
|
||||
|
||||
|
||||
func _gravity_accel() -> Vector3:
|
||||
return _active_gravity()["accel"]
|
||||
|
||||
|
||||
func _update_telemetry() -> void:
|
||||
telem_speed = velocity.length()
|
||||
telem_thrust = Input.is_key_pressed(KEY_SPACE)
|
||||
var earth := _earth()
|
||||
if earth:
|
||||
telem_earth_rng = maxf(0.0, global_position.distance_to(earth.global_position) - EARTH_RADIUS)
|
||||
else:
|
||||
telem_earth_rng = 0.0
|
||||
var body: Dictionary = _active_gravity()
|
||||
telem_body = str(body["name"])
|
||||
if telem_body == "—":
|
||||
telem_has_alt = false
|
||||
telem_alt = 0.0
|
||||
telem_vspeed = 0.0
|
||||
telem_hspeed = telem_speed
|
||||
telem_vcirc = 0.0
|
||||
telem_dh = 0.0
|
||||
return
|
||||
telem_has_alt = true
|
||||
var r: float = float(body["dist"])
|
||||
var rad: float = float(body["radius"])
|
||||
var g: float = float(body["g"])
|
||||
telem_alt = r - rad
|
||||
var radial: Vector3 = (global_position - body["center"]).normalized()
|
||||
telem_vspeed = velocity.dot(radial)
|
||||
telem_hspeed = (velocity - radial * telem_vspeed).length()
|
||||
telem_vcirc = sqrt(g * rad * rad / r) if r > 1.0 else 0.0
|
||||
telem_dh = telem_hspeed - telem_vcirc
|
||||
|
||||
|
||||
func _update_flight_hud() -> void:
|
||||
if flight_label == null or not player_inside:
|
||||
return
|
||||
flight_label.visible = true
|
||||
var mode := "3P" if using_third_person else "1P"
|
||||
var thr := "THRUST" if telem_thrust else "IDLE"
|
||||
var vs_sign := "+" if telem_vspeed >= 0.0 else ""
|
||||
var alt_line: String
|
||||
var vcirc_line: String
|
||||
var dh_line: String
|
||||
if telem_has_alt:
|
||||
alt_line = "ALT %8.0f m\n" % telem_alt
|
||||
vcirc_line = "VCIRC %8.1f m/s\n" % telem_vcirc
|
||||
var dh_sign := "+" if telem_dh >= 0.0 else ""
|
||||
dh_line = "ΔH %s%7.1f m/s\n" % [dh_sign, telem_dh]
|
||||
else:
|
||||
alt_line = "ALT —\n"
|
||||
vcirc_line = "VCIRC —\n"
|
||||
dh_line = "ΔH —\n"
|
||||
flight_label.text = (
|
||||
"[%s] %s | BODY %s\n" % [mode, thr, telem_body]
|
||||
+ alt_line
|
||||
+ "SPD %8.1f m/s\n" % telem_speed
|
||||
+ "VS %s%7.1f m/s\n" % [vs_sign, telem_vspeed]
|
||||
+ "H %8.1f m/s\n" % telem_hspeed
|
||||
+ vcirc_line
|
||||
+ dh_line
|
||||
+ "EARTH %8.0f m" % telem_earth_rng
|
||||
)
|
||||
|
||||
|
||||
func _clear_flight_hud() -> void:
|
||||
if flight_label:
|
||||
flight_label.visible = false
|
||||
flight_label.text = ""
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if body.is_in_group("player"):
|
||||
player_ref = body
|
||||
if interact_prompt and not player_inside:
|
||||
interact_prompt.text = "Press F to Enter"
|
||||
interact_prompt.visible = true
|
||||
|
||||
|
||||
func _on_body_exited(body: Node3D) -> void:
|
||||
if body.is_in_group("player") and not player_inside:
|
||||
player_ref = null
|
||||
if interact_prompt:
|
||||
interact_prompt.visible = false
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not player_inside:
|
||||
return
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
if pause_menu and pause_menu.has_method("open_pause") and player_ref:
|
||||
pause_menu.open_pause(player_ref)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _event_toggles(event, "capsule_gear", KEY_G):
|
||||
_gear_down = not _gear_down
|
||||
_set_hull("gear_deploy", 1.0 if _gear_down else 0.0)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _event_toggles(event, "capsule_hatch_crew", KEY_H):
|
||||
_hatch_crew_open = not _hatch_crew_open
|
||||
_set_hull("hatch_crew_open", 1.0 if _hatch_crew_open else 0.0)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _event_toggles(event, "capsule_hatch_dock", KEY_J):
|
||||
_hatch_dock_open = not _hatch_dock_open
|
||||
_set_hull("hatch_dock_open", 1.0 if _hatch_dock_open else 0.0)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _event_toggles(event, "capsule_ladder", KEY_L):
|
||||
_ladder_out = not _ladder_out
|
||||
_set_hull("hatch_ladder_deploy", 1.0 if _ladder_out else 0.0)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _event_toggles(event, "capsule_instruments", KEY_I):
|
||||
_set_hull("instruments_demo", 1.0)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _event_toggles(event, "capsule_cabin_light", KEY_K):
|
||||
_cabin_dim = not _cabin_dim
|
||||
_set_hull("cabin_dim", _cabin_dim)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _ensure_actions() -> void:
|
||||
var binds := {
|
||||
"interact": KEY_F,
|
||||
"toggle_first_person": KEY_V,
|
||||
"capsule_gear": KEY_G,
|
||||
"capsule_hatch_crew": KEY_H,
|
||||
"capsule_hatch_dock": KEY_J,
|
||||
"capsule_ladder": KEY_L,
|
||||
"capsule_instruments": KEY_I,
|
||||
"capsule_cabin_light": KEY_K,
|
||||
}
|
||||
for action in binds.keys():
|
||||
if InputMap.has_action(action):
|
||||
continue
|
||||
InputMap.add_action(action)
|
||||
var ev := InputEventKey.new()
|
||||
ev.physical_keycode = binds[action]
|
||||
InputMap.action_add_event(action, ev)
|
||||
|
||||
|
||||
func _event_toggles(event: InputEvent, action: String, key: Key) -> bool:
|
||||
if InputMap.has_action(action) and event.is_action_pressed(action):
|
||||
return true
|
||||
if event is InputEventKey and event.pressed and not event.echo and event.physical_keycode == key:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _act_pressed(action: String) -> bool:
|
||||
return InputMap.has_action(action) and Input.is_action_pressed(action)
|
||||
|
||||
|
||||
func _act_just(action: String) -> bool:
|
||||
return InputMap.has_action(action) and Input.is_action_just_pressed(action)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var interact_now := _act_just("interact") or _just_key(KEY_F)
|
||||
if not player_inside and player_ref and interact_now:
|
||||
f_was_down = true
|
||||
enter_capsule()
|
||||
return
|
||||
if not player_inside:
|
||||
f_was_down = Input.is_physical_key_pressed(KEY_F)
|
||||
_update_crust_exception()
|
||||
return
|
||||
if interact_now and not f_was_down:
|
||||
f_was_down = true
|
||||
exit_capsule()
|
||||
return
|
||||
f_was_down = _act_pressed("interact") or Input.is_physical_key_pressed(KEY_F)
|
||||
if _act_just("toggle_first_person"):
|
||||
toggle_camera()
|
||||
elif not InputMap.has_action("toggle_first_person") and _just_key(KEY_V):
|
||||
toggle_camera()
|
||||
var was_free = free_look
|
||||
free_look = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
|
||||
if was_free and not free_look and not using_third_person:
|
||||
cockpit_yaw = 0.0
|
||||
cockpit_pitch = 0.0
|
||||
if Input.is_key_pressed(KEY_ALT):
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
else:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
handle_flight(delta)
|
||||
_update_telemetry()
|
||||
_update_flight_hud()
|
||||
if using_third_person:
|
||||
update_third_person_camera()
|
||||
if third_person_camera and not third_person_camera.current:
|
||||
third_person_camera.make_current()
|
||||
else:
|
||||
update_cockpit_camera()
|
||||
if capsule_camera and not capsule_camera.current:
|
||||
capsule_camera.make_current()
|
||||
|
||||
|
||||
func _just_key(key: Key) -> bool:
|
||||
var down := Input.is_physical_key_pressed(key)
|
||||
if key == KEY_V:
|
||||
var edge := down and not _v_was
|
||||
_v_was = down
|
||||
return edge
|
||||
return false
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not player_inside:
|
||||
return
|
||||
if Input.is_key_pressed(KEY_ALT):
|
||||
return
|
||||
if event is InputEventMouseMotion:
|
||||
if using_third_person:
|
||||
orbit_yaw -= event.relative.x * look_sensitivity
|
||||
orbit_pitch += event.relative.y * look_sensitivity
|
||||
orbit_pitch = clampf(orbit_pitch, orbit_pitch_min, orbit_pitch_max)
|
||||
elif free_look:
|
||||
cockpit_yaw -= event.relative.x * look_sensitivity
|
||||
cockpit_pitch -= event.relative.y * look_sensitivity
|
||||
cockpit_yaw = clamp(cockpit_yaw, -cockpit_look_limit_yaw, cockpit_look_limit_yaw)
|
||||
cockpit_pitch = clamp(cockpit_pitch, -cockpit_look_limit_down, cockpit_look_limit_up)
|
||||
|
||||
|
||||
func enter_capsule() -> void:
|
||||
if player_inside or player_ref == null:
|
||||
return
|
||||
player_inside = true
|
||||
if interact_prompt:
|
||||
interact_prompt.visible = false
|
||||
player_ref.reparent(self)
|
||||
player_ref.position = Vector3(0, 0.5, 0)
|
||||
player_ref.rotation = Vector3.ZERO
|
||||
player_ref.set_physics_process(false)
|
||||
player_ref.set_process(true)
|
||||
player_ref.set_process_input(false)
|
||||
player_ref.set_process_unhandled_input(false)
|
||||
if player_ref.has_method("set_in_vehicle"):
|
||||
player_ref.set_in_vehicle(true)
|
||||
var player_col = player_ref.get_node_or_null("CollisionShape3D")
|
||||
if player_col:
|
||||
player_col.disabled = true
|
||||
var player_cam = player_ref.get_node_or_null("CameraPivot/Camera3D")
|
||||
if player_cam:
|
||||
player_cam.current = false
|
||||
player_ref.visible = false
|
||||
cockpit_yaw = 0.0
|
||||
cockpit_pitch = 0.0
|
||||
orbit_yaw = 0.0
|
||||
orbit_pitch = 0.4
|
||||
using_third_person = false
|
||||
f_was_down = true
|
||||
free_look = false
|
||||
if camera_pivot:
|
||||
camera_pivot.top_level = false
|
||||
camera_pivot.transform = head_local
|
||||
if third_person_camera:
|
||||
third_person_camera.current = false
|
||||
if capsule_camera:
|
||||
capsule_camera.near = 0.05
|
||||
capsule_camera.far = CAM_FAR
|
||||
capsule_camera.current = true
|
||||
capsule_camera.make_current()
|
||||
velocity = Vector3.ZERO
|
||||
FloatingOrigin.set_focus(self)
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
if flight_label:
|
||||
flight_label.visible = true
|
||||
_update_crust_exception()
|
||||
|
||||
|
||||
func exit_capsule() -> void:
|
||||
if not player_inside or player_ref == null:
|
||||
return
|
||||
var exiting_player: CharacterBody3D = player_ref
|
||||
player_inside = false
|
||||
velocity = Vector3.ZERO
|
||||
free_look = false
|
||||
f_was_down = false
|
||||
using_third_person = false
|
||||
_set_hull("abort_burn", false)
|
||||
_set_hull("rcs_direction", 0)
|
||||
_clear_flight_hud()
|
||||
if body_collision:
|
||||
body_collision.disabled = false
|
||||
if is_instance_valid(camera_pivot):
|
||||
camera_pivot.top_level = false
|
||||
camera_pivot.transform = head_local
|
||||
if is_instance_valid(capsule_camera):
|
||||
capsule_camera.current = false
|
||||
if is_instance_valid(third_person_camera):
|
||||
third_person_camera.current = false
|
||||
var station := get_node_or_null("/root/Node3D/SolarSystem/Moon/LovellAnchor/Station")
|
||||
var dest: Node = station if station else get_tree().current_scene
|
||||
exiting_player.reparent(dest)
|
||||
exiting_player.global_position = global_position + global_transform.basis.y * 4.0
|
||||
var player_cam_pivot = exiting_player.get_node_or_null("CameraPivot")
|
||||
if player_cam_pivot:
|
||||
player_cam_pivot.rotation = Vector3.ZERO
|
||||
exiting_player.visible = true
|
||||
exiting_player.set_physics_process(true)
|
||||
exiting_player.set_process(true)
|
||||
exiting_player.set_process_input(true)
|
||||
exiting_player.set_process_unhandled_input(true)
|
||||
exiting_player.process_mode = Node.PROCESS_MODE_INHERIT
|
||||
var player_col = exiting_player.get_node_or_null("CollisionShape3D")
|
||||
if player_col:
|
||||
player_col.disabled = false
|
||||
if exiting_player.has_method("set_in_vehicle"):
|
||||
exiting_player.set_in_vehicle(false)
|
||||
var player_cam = exiting_player.get_node_or_null("CameraPivot/Camera3D")
|
||||
if player_cam:
|
||||
player_cam.current = true
|
||||
player_cam.make_current()
|
||||
if exiting_player.has_method("restore_from_vehicle"):
|
||||
exiting_player.restore_from_vehicle()
|
||||
FloatingOrigin.set_focus(exiting_player)
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
player_ref = null
|
||||
|
||||
|
||||
func toggle_camera() -> void:
|
||||
using_third_person = not using_third_person
|
||||
if using_third_person:
|
||||
if capsule_camera:
|
||||
capsule_camera.current = false
|
||||
if third_person_camera:
|
||||
third_person_camera.near = 0.1
|
||||
third_person_camera.far = CAM_FAR
|
||||
third_person_camera.make_current()
|
||||
else:
|
||||
if third_person_camera:
|
||||
third_person_camera.current = false
|
||||
if camera_pivot:
|
||||
camera_pivot.top_level = false
|
||||
camera_pivot.transform = head_local
|
||||
if capsule_camera:
|
||||
capsule_camera.make_current()
|
||||
|
||||
|
||||
func update_cockpit_camera() -> void:
|
||||
if not camera_pivot:
|
||||
return
|
||||
camera_pivot.top_level = false
|
||||
var b: Basis = head_local.basis
|
||||
b = b.rotated(b.x, cockpit_pitch)
|
||||
b = b.rotated(b.y, cockpit_yaw)
|
||||
b = b.orthonormalized()
|
||||
if abs(b.determinant()) < 0.5:
|
||||
camera_pivot.transform = head_local
|
||||
return
|
||||
camera_pivot.transform = Transform3D(b, head_local.origin)
|
||||
|
||||
|
||||
func update_third_person_camera() -> void:
|
||||
if not third_person_camera:
|
||||
return
|
||||
var cy := cos(orbit_yaw)
|
||||
var sy := sin(orbit_yaw)
|
||||
var cp := cos(orbit_pitch)
|
||||
var sp := sin(orbit_pitch)
|
||||
var offset := Vector3(sy * cp, sp, cy * cp) * orbit_distance
|
||||
var local_off := Vector3(offset.x, -offset.z, offset.y)
|
||||
var cam_pos := global_position + global_transform.basis * local_off
|
||||
third_person_camera.global_position = cam_pos
|
||||
var target := global_position + global_transform.basis.y * 1.5
|
||||
var forward := target - cam_pos
|
||||
if forward.length_squared() < 0.0001:
|
||||
return
|
||||
forward = forward.normalized()
|
||||
var ref_up := global_transform.basis.z
|
||||
if abs(forward.dot(ref_up)) > 0.92:
|
||||
ref_up = global_transform.basis.x
|
||||
var right := ref_up.cross(forward)
|
||||
if right.length_squared() < 0.0001:
|
||||
return
|
||||
right = right.normalized()
|
||||
var up := forward.cross(right).normalized()
|
||||
var cam_basis := Basis(-right, up, -forward).orthonormalized()
|
||||
if abs(cam_basis.determinant()) < 0.5:
|
||||
return
|
||||
third_person_camera.global_basis = cam_basis
|
||||
|
||||
|
||||
func handle_flight(delta: float) -> void:
|
||||
_update_crust_exception()
|
||||
var input_pitch := 0.0
|
||||
var input_roll := 0.0
|
||||
var input_yaw := 0.0
|
||||
if Input.is_key_pressed(KEY_W):
|
||||
input_pitch -= 1.0
|
||||
if Input.is_key_pressed(KEY_S):
|
||||
input_pitch += 1.0
|
||||
if Input.is_key_pressed(KEY_A):
|
||||
input_yaw += 1.0
|
||||
if Input.is_key_pressed(KEY_D):
|
||||
input_yaw -= 1.0
|
||||
if Input.is_key_pressed(KEY_Q):
|
||||
input_roll -= 1.0
|
||||
if Input.is_key_pressed(KEY_E):
|
||||
input_roll += 1.0
|
||||
rotate_object_local(Vector3.RIGHT, input_pitch * rotation_speed * delta)
|
||||
rotate_object_local(Vector3.FORWARD, -input_yaw * rotation_speed * delta)
|
||||
rotate_object_local(Vector3.UP, input_roll * rotation_speed * delta)
|
||||
var thrusting := Input.is_key_pressed(KEY_SPACE)
|
||||
_set_hull("abort_burn", thrusting)
|
||||
_set_hull("att_pitch", input_pitch)
|
||||
_set_hull("att_yaw", input_yaw)
|
||||
_set_hull("att_roll", input_roll)
|
||||
_set_hull("rcs_direction", 0)
|
||||
var acceleration: Vector3 = _gravity_accel()
|
||||
if thrusting:
|
||||
acceleration += global_transform.basis.y * (thrust_force / mass)
|
||||
velocity += acceleration * delta
|
||||
var near_surface := is_on_floor() or _crust_exception
|
||||
if not near_surface:
|
||||
var moon := _moon()
|
||||
if moon and global_position.distance_to(moon.global_position) < MOON_RADIUS + NEAR_SURFACE_MARGIN:
|
||||
near_surface = true
|
||||
var earth := _earth()
|
||||
if earth and global_position.distance_to(earth.global_position) < EARTH_RADIUS + NEAR_SURFACE_MARGIN:
|
||||
near_surface = true
|
||||
if near_surface:
|
||||
if body_collision:
|
||||
body_collision.disabled = false
|
||||
move_and_slide()
|
||||
if is_on_floor():
|
||||
velocity.x = move_toward(velocity.x, 0.0, surface_friction * delta)
|
||||
velocity.z = move_toward(velocity.z, 0.0, surface_friction * delta)
|
||||
if velocity.length() < 0.08:
|
||||
velocity = Vector3.ZERO
|
||||
else:
|
||||
if body_collision:
|
||||
body_collision.disabled = true
|
||||
global_position += velocity * delta
|
||||
PhysicsServer3D.body_set_state(
|
||||
get_rid(),
|
||||
PhysicsServer3D.BODY_STATE_TRANSFORM,
|
||||
global_transform
|
||||
)
|
||||
PhysicsServer3D.body_set_state(
|
||||
get_rid(),
|
||||
PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY,
|
||||
velocity
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://1udl42sp42sp
|
||||
@@ -0,0 +1,94 @@
|
||||
extends Node
|
||||
## Sibling FO. Shift registered world roots.
|
||||
## Move focus only when it is NOT already under a root (capsule = sibling).
|
||||
## Never zero velocity.
|
||||
|
||||
const THRESHOLD: float = 50000.0
|
||||
const COOLDOWN: float = 0.25
|
||||
|
||||
var focus: Node3D = null
|
||||
var _shift_roots: Array[Node3D] = []
|
||||
var _cooldown: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
|
||||
func set_focus(node: Node3D) -> void:
|
||||
focus = node
|
||||
|
||||
|
||||
func register_root(node: Node3D) -> void:
|
||||
if node == null:
|
||||
return
|
||||
if not _shift_roots.has(node):
|
||||
_shift_roots.append(node)
|
||||
print("FloatingOrigin registered: ", node.name)
|
||||
|
||||
|
||||
func unregister_root(node: Node3D) -> void:
|
||||
_shift_roots.erase(node)
|
||||
|
||||
|
||||
func shift(offset: Vector3) -> void:
|
||||
if offset.length_squared() < 1.0:
|
||||
return
|
||||
_apply_shift(offset)
|
||||
_cooldown = COOLDOWN
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if _cooldown > 0.0:
|
||||
_cooldown -= delta
|
||||
return
|
||||
if focus == null or not is_instance_valid(focus) or not focus.is_inside_tree():
|
||||
return
|
||||
if _shift_roots.is_empty():
|
||||
return
|
||||
var fp: Vector3 = focus.global_position
|
||||
if fp.length() < THRESHOLD:
|
||||
return
|
||||
_apply_shift(-fp)
|
||||
_cooldown = COOLDOWN
|
||||
|
||||
|
||||
func _apply_shift(offset: Vector3) -> void:
|
||||
for root in _shift_roots:
|
||||
if is_instance_valid(root) and root.is_inside_tree():
|
||||
root.global_position += offset
|
||||
if is_instance_valid(focus) and focus.is_inside_tree() and not _focus_under_a_root():
|
||||
focus.global_position += offset
|
||||
_sync_focus_physics()
|
||||
print("FloatingOrigin shift: ", offset, " focus now: ", focus.global_position if focus else Vector3.ZERO)
|
||||
|
||||
|
||||
func _focus_under_a_root() -> bool:
|
||||
var n: Node = focus
|
||||
while n != null:
|
||||
if n is Node3D and _shift_roots.has(n as Node3D):
|
||||
return true
|
||||
n = n.get_parent()
|
||||
return false
|
||||
|
||||
|
||||
func _sync_focus_physics() -> void:
|
||||
if focus == null or not is_instance_valid(focus):
|
||||
return
|
||||
if not (focus is CharacterBody3D):
|
||||
return
|
||||
var body := focus as CharacterBody3D
|
||||
var saved_vel: Vector3 = body.velocity
|
||||
PhysicsServer3D.body_set_state(
|
||||
body.get_rid(),
|
||||
PhysicsServer3D.BODY_STATE_TRANSFORM,
|
||||
body.global_transform
|
||||
)
|
||||
PhysicsServer3D.body_set_state(
|
||||
body.get_rid(),
|
||||
PhysicsServer3D.BODY_STATE_LINEAR_VELOCITY,
|
||||
saved_vel
|
||||
)
|
||||
body.velocity = saved_vel
|
||||
if body.has_method("reset_physics_interpolation"):
|
||||
body.reset_physics_interpolation()
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckl5inm8ugypo
|
||||
@@ -0,0 +1,152 @@
|
||||
extends Node
|
||||
|
||||
const SAVE_PATH := "user://save_slot_1.json"
|
||||
var has_save: bool = false
|
||||
var play_elapsed: float = 0.0
|
||||
var demo_limit: float = 36000.0
|
||||
var scramble_at: float = 35280.0
|
||||
var scramble_started: bool = false
|
||||
var clock_paused: bool = false
|
||||
var game_year: int = 2066
|
||||
var game_month: int = 9
|
||||
var game_day: int = 14
|
||||
var game_hour: float = 8.0
|
||||
var time_scale: float = 1.0
|
||||
signal scramble_begun
|
||||
signal time_scale_changed(new_scale: float)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
has_save = FileAccess.file_exists(SAVE_PATH)
|
||||
process_mode = Node.PROCESS_MODE_PAUSABLE
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if clock_paused:
|
||||
return
|
||||
var stepped: float = delta * time_scale
|
||||
play_elapsed += stepped
|
||||
_advance_game_clock(stepped)
|
||||
if not scramble_started and play_elapsed >= scramble_at:
|
||||
scramble_started = true
|
||||
scramble_begun.emit()
|
||||
print("SCRAMBLE: Unknown Vessels Inbound")
|
||||
|
||||
|
||||
func set_time_scale(scale: float) -> void:
|
||||
time_scale = clampf(scale, 0.0, 4.0)
|
||||
time_scale_changed.emit(time_scale)
|
||||
print("Time scale: x", time_scale)
|
||||
|
||||
|
||||
func _advance_game_clock(game_seconds: float) -> void:
|
||||
game_hour += game_seconds / 3600.0
|
||||
while game_hour >= 24.0:
|
||||
game_hour -= 24.0
|
||||
game_day += 1
|
||||
var days_in_month: int = _days_in_month(game_year, game_month)
|
||||
if game_day > days_in_month:
|
||||
game_day = 1
|
||||
game_month += 1
|
||||
if game_month > 12:
|
||||
game_month = 1
|
||||
game_year += 1
|
||||
|
||||
|
||||
func _days_in_month(_year: int, month: int) -> int:
|
||||
match month:
|
||||
1, 3, 5, 7, 8, 10, 12:
|
||||
return 31
|
||||
4, 6, 9, 11:
|
||||
return 30
|
||||
2:
|
||||
return 28
|
||||
_:
|
||||
return 30
|
||||
|
||||
|
||||
func get_time_string() -> String:
|
||||
var h: int = int(game_hour)
|
||||
var m: int = int((game_hour - float(h)) * 60.0)
|
||||
var s: int = int((game_hour - float(h) - float(m) / 60.0) * 3600.0)
|
||||
var month_names := ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
||||
var mon: String = month_names[game_month]
|
||||
var scale_txt := ""
|
||||
if time_scale == 0.0:
|
||||
scale_txt = " PAUSED"
|
||||
elif time_scale > 1.0:
|
||||
scale_txt = " x%d" % int(time_scale)
|
||||
return "%d %s %d %02d:%02d:%02d%s" % [game_day, mon, game_year, h, m, s, scale_txt]
|
||||
|
||||
|
||||
func get_play_remaining_string() -> String:
|
||||
var left: float = max(demo_limit - play_elapsed, 0.0)
|
||||
var h: int = int(left / 3600.0)
|
||||
var m: int = int((left - float(h) * 3600.0) / 60.0)
|
||||
var sec: int = int(left) % 60
|
||||
return "%02d:%02d:%02d" % [h, m, sec]
|
||||
|
||||
|
||||
func save_game(player: Node) -> void:
|
||||
if player == null:
|
||||
return
|
||||
var look = {}
|
||||
if "appearance" in player:
|
||||
look = player.appearance
|
||||
var data := {
|
||||
"player_name": player.player_name,
|
||||
"background_id": player.background_id,
|
||||
"background_name": player.background_name,
|
||||
"culture_id": player.culture_id if "culture_id" in player else "",
|
||||
"culture_name": player.culture_name if "culture_name" in player else "",
|
||||
"appearance": look,
|
||||
"money": player.money,
|
||||
"food": player.food,
|
||||
"health": player.health,
|
||||
"oxygen": player.oxygen,
|
||||
"inventory": player.inventory.duplicate(),
|
||||
"position": {
|
||||
"x": player.global_position.x,
|
||||
"y": player.global_position.y,
|
||||
"z": player.global_position.z
|
||||
},
|
||||
"play_elapsed": play_elapsed,
|
||||
"scramble_started": scramble_started,
|
||||
"game_year": game_year,
|
||||
"game_month": game_month,
|
||||
"game_day": game_day,
|
||||
"game_hour": game_hour,
|
||||
"time_scale": time_scale
|
||||
}
|
||||
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_string(JSON.stringify(data))
|
||||
file.close()
|
||||
has_save = true
|
||||
print("Game saved")
|
||||
|
||||
|
||||
func load_game_data() -> Dictionary:
|
||||
if not FileAccess.file_exists(SAVE_PATH):
|
||||
return {}
|
||||
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
return {}
|
||||
var text := file.get_as_text()
|
||||
file.close()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) == TYPE_DICTIONARY:
|
||||
return parsed
|
||||
return {}
|
||||
|
||||
|
||||
func apply_loaded_clock(data: Dictionary) -> void:
|
||||
play_elapsed = float(data.get("play_elapsed", 0.0))
|
||||
scramble_started = bool(data.get("scramble_started", false))
|
||||
game_year = int(data.get("game_year", 2066))
|
||||
game_month = int(data.get("game_month", 9))
|
||||
game_day = int(data.get("game_day", 14))
|
||||
game_hour = float(data.get("game_hour", 8.0))
|
||||
time_scale = float(data.get("time_scale", 1.0))
|
||||
clock_paused = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3o7ua8cv55tq
|
||||
@@ -0,0 +1 @@
|
||||
uid://br0vxsoinqwe1
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Dried Noodles"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bigf78nt5im5v
|
||||
@@ -0,0 +1,646 @@
|
||||
extends Node3D
|
||||
## Player/Body look loader. PASTE 2026-09-12 22:54 — 1P always gun-out eye ride
|
||||
## 8-way = yaw CreatedBody toward the stick + Walk/Run.
|
||||
## No AnimationTree. Waist fold AFTER clip via SkeletonModifier3D.
|
||||
|
||||
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 CLIP_ALIASES := {
|
||||
"Idle": ["Idle"],
|
||||
"Walk": ["Walk"],
|
||||
"Run": ["Run"],
|
||||
"Run_Back": ["Run_Back", "RunBack"],
|
||||
"Run_Left": ["Run_Left", "RunLeft"],
|
||||
"Run_Right": ["Run_Right", "RunRight"],
|
||||
"Jump": ["Jump"],
|
||||
"Interact": ["Interact", "Pickup", "PickUp"],
|
||||
"Idle_Gun": ["Idle_Gun", "Idle_Gun_Pointing", "Gun_Idle"],
|
||||
"AimIdle": ["Idle_Gun_Pointing", "Idle_Gun", "Gun_Idle", "Aim_Idle"],
|
||||
"AimWalk": ["Walk_Shoot", "WalkShoot", "Gun_Walk", "Walk_Gun", "Walk"],
|
||||
"Walk_Shoot": ["Walk_Shoot", "WalkShoot", "Gun_Walk", "Walk"],
|
||||
"AimRun": ["Run_Shoot", "RunShoot", "Gun_Run", "Run_Gun", "Run"],
|
||||
"Run_Shoot": ["Run_Shoot", "RunShoot", "Run"],
|
||||
"Gun_Shoot": ["Gun_Shoot", "Pistol_Shoot", "Shoot", "Idle_Gun_Shoot"],
|
||||
"Idle_Gun_Shoot": ["Idle_Gun_Shoot", "Gun_Shoot", "Idle_Gun_Pointing"],
|
||||
}
|
||||
|
||||
var current: Node3D = null
|
||||
var anim: AnimationPlayer = null
|
||||
var last_keep: Array[String] = []
|
||||
var _logged_names: bool = false
|
||||
var hide_head: bool = false
|
||||
var show_pistol: bool = false
|
||||
|
||||
var _skel: Skeleton3D = null
|
||||
var _bone_abdomen: int = -1
|
||||
var _bone_torso: int = -1
|
||||
var _bone_chest: int = -1
|
||||
var _bone_head: int = -1
|
||||
var _bone_rarm: int = -1
|
||||
var _bone_larm: int = -1
|
||||
var _bone_rfore: int = -1
|
||||
var _bone_lfore: int = -1
|
||||
var _aim_pitch: float = 0.0
|
||||
var _aim_enabled: bool = false
|
||||
var _logged_bones: bool = false
|
||||
var _mod: SkeletonModifier3D = null
|
||||
var _dbg: float = 0.0
|
||||
|
||||
var tree: AnimationTree = null
|
||||
var _walk_bs: AnimationNodeBlendSpace2D = null
|
||||
var _run_bs: AnimationNodeBlendSpace2D = null
|
||||
var _aim_walk_bs: AnimationNodeBlendSpace2D = null
|
||||
var _aim_run_bs: AnimationNodeBlendSpace2D = null
|
||||
var _mode: String = ""
|
||||
var _blend_pos: Vector2 = Vector2.ZERO
|
||||
var _run_amt: float = 0.0
|
||||
var _aiming: bool = false
|
||||
var _fire_lock: float = 0.0
|
||||
var _ride_fp: bool = false
|
||||
var _fp_cam: Camera3D = null
|
||||
var _fp_pivot: Node3D = null
|
||||
var arm_axis_id: int = 2
|
||||
|
||||
|
||||
class AimBend extends SkeletonModifier3D:
|
||||
var host: Node
|
||||
func _process_modification() -> void:
|
||||
if host and host.has_method("_apply_waist_now"):
|
||||
host._apply_waist_now()
|
||||
|
||||
|
||||
func apply_look(appearance: Dictionary) -> AnimationPlayer:
|
||||
_clear_old()
|
||||
var sex := str(appearance.get("sex", "male"))
|
||||
var path := MALE_FBX if sex == "male" else FEMALE_FBX
|
||||
if not ResourceLoader.exists(path):
|
||||
push_warning("Look file missing: " + path)
|
||||
return null
|
||||
var packed = load(path)
|
||||
if packed == null:
|
||||
return null
|
||||
var inst: Node = packed.instantiate()
|
||||
if not (inst is Node3D):
|
||||
return null
|
||||
current = inst as Node3D
|
||||
current.name = "CreatedBody"
|
||||
current.position = Vector3.ZERO
|
||||
current.rotation_degrees = Vector3.ZERO
|
||||
current.scale = Vector3.ONE * 1.3
|
||||
add_child(current)
|
||||
last_keep = [
|
||||
str(appearance.get("head", "Casual_Head")),
|
||||
str(appearance.get("body", "Casual_Body")),
|
||||
str(appearance.get("legs", "Casual_Legs")),
|
||||
str(appearance.get("feet", "Casual_Feet")),
|
||||
]
|
||||
anim = current.find_child("AnimationPlayer", true, false) as AnimationPlayer
|
||||
if anim:
|
||||
anim.speed_scale = 1.0
|
||||
anim.root_motion_track = NodePath()
|
||||
_loop_locomotion(anim)
|
||||
print("Look clips: ", anim.get_animation_list())
|
||||
_cache_skeleton()
|
||||
_build_loco_tree()
|
||||
_apply_keep(current, last_keep)
|
||||
call_deferred("_apply_keep_deferred")
|
||||
return anim
|
||||
|
||||
|
||||
func _resolve_clip(logical: String) -> String:
|
||||
if anim == null:
|
||||
return ""
|
||||
var names: Array = CLIP_ALIASES.get(logical, [logical])
|
||||
var candidates: Array[String] = []
|
||||
for n in names:
|
||||
candidates.append("CharacterArmature|%s" % n)
|
||||
candidates.append(str(n))
|
||||
for clip in candidates:
|
||||
if anim.has_animation(clip):
|
||||
return clip
|
||||
for clip in anim.get_animation_list():
|
||||
for n in names:
|
||||
if clip.ends_with(str(n)) or clip.contains(str(n)):
|
||||
return clip
|
||||
return ""
|
||||
|
||||
|
||||
func play_clip(logical: String, speed_scale: float = 1.0) -> void:
|
||||
if anim == null:
|
||||
return
|
||||
if tree:
|
||||
tree.active = false
|
||||
var clip := _resolve_clip(logical)
|
||||
if clip.is_empty():
|
||||
return
|
||||
var a: Animation = anim.get_animation(clip)
|
||||
if a:
|
||||
var looped := logical in [
|
||||
"Idle", "Walk", "Run", "Run_Back", "Run_Left", "Run_Right",
|
||||
"Idle_Gun", "AimIdle", "AimWalk", "AimRun", "Walk_Shoot", "Run_Shoot"
|
||||
]
|
||||
a.loop_mode = Animation.LOOP_LINEAR if looped else Animation.LOOP_NONE
|
||||
anim.speed_scale = speed_scale
|
||||
if anim.current_animation != clip:
|
||||
anim.play(clip)
|
||||
elif not anim.is_playing():
|
||||
anim.play(clip)
|
||||
_apply_keep(current, last_keep)
|
||||
|
||||
|
||||
func _loop_locomotion(player: AnimationPlayer) -> void:
|
||||
for logical in [
|
||||
"Idle", "Walk", "Run", "Run_Back", "Run_Left", "Run_Right",
|
||||
"Idle_Gun", "AimIdle", "AimWalk", "AimRun", "Walk_Shoot", "Run_Shoot"
|
||||
]:
|
||||
var clip := _resolve_clip(logical)
|
||||
if clip.is_empty():
|
||||
continue
|
||||
var a: Animation = player.get_animation(clip)
|
||||
if a:
|
||||
a.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
|
||||
func _clip_node(logical: String) -> AnimationNodeAnimation:
|
||||
var n := AnimationNodeAnimation.new()
|
||||
var clip := _resolve_clip(logical)
|
||||
if clip.is_empty():
|
||||
clip = _resolve_clip("Idle")
|
||||
if clip.is_empty():
|
||||
clip = logical
|
||||
n.animation = StringName(clip)
|
||||
n.resource_name = logical
|
||||
return n
|
||||
|
||||
|
||||
func _add_pt(bs: AnimationNodeBlendSpace2D, logical: String, pos: Vector2, label: String) -> void:
|
||||
var node := _clip_node(logical)
|
||||
bs.add_blend_point(node, pos)
|
||||
var idx: int = bs.get_blend_point_count() - 1
|
||||
if bs.has_method("set_blend_point_name"):
|
||||
bs.call("set_blend_point_name", idx, StringName(label))
|
||||
|
||||
|
||||
func _make_space(_idle: String, fwd: String, back: String, left: String, right: String) -> AnimationNodeBlendSpace2D:
|
||||
var bs := AnimationNodeBlendSpace2D.new()
|
||||
# Discrete = one whole clip. Interpolating Walk with Run_Left is clown shoes.
|
||||
bs.blend_mode = AnimationNodeBlendSpace2D.BLEND_MODE_DISCRETE_CARRY
|
||||
bs.min_space = Vector2(-1.0, -1.0)
|
||||
bs.max_space = Vector2(1.0, 1.0)
|
||||
bs.snap = Vector2(0.0, 0.0)
|
||||
_add_pt(bs, fwd, Vector2(0.0, -1.0), "fwd")
|
||||
_add_pt(bs, back, Vector2(0.0, 1.0), "back")
|
||||
_add_pt(bs, left, Vector2(-1.0, 0.0), "left")
|
||||
_add_pt(bs, right, Vector2(1.0, 0.0), "right")
|
||||
_add_pt(bs, fwd, Vector2(-0.71, -0.71), "fwd_left")
|
||||
_add_pt(bs, fwd, Vector2(0.71, -0.71), "fwd_right")
|
||||
_add_pt(bs, back, Vector2(-0.71, 0.71), "back_left")
|
||||
_add_pt(bs, back, Vector2(0.71, 0.71), "back_right")
|
||||
return bs
|
||||
|
||||
|
||||
func _build_loco_tree() -> void:
|
||||
# No AnimationTree. Quaternius has no Walk_Left/Walk_Right — blending
|
||||
# Walk with Run_Left is clown shoes. One clip per octant instead.
|
||||
if tree and is_instance_valid(tree):
|
||||
tree.queue_free()
|
||||
tree = null
|
||||
_walk_bs = null
|
||||
_run_bs = null
|
||||
_aim_walk_bs = null
|
||||
_aim_run_bs = null
|
||||
_mode = "idle"
|
||||
_blend_pos = Vector2.ZERO
|
||||
_run_amt = 0.0
|
||||
print("Loco clips walk=", _resolve_clip("Walk"),
|
||||
" left=", _resolve_clip("Run_Left"),
|
||||
" right=", _resolve_clip("Run_Right"),
|
||||
" back=", _resolve_clip("Run_Back"),
|
||||
" run=", _resolve_clip("Run"),
|
||||
" aimIdle=", _resolve_clip("AimIdle"))
|
||||
|
||||
|
||||
func play_fire(aiming_now: bool = false) -> void:
|
||||
var logical: String = "Idle_Gun_Shoot" if aiming_now else "Gun_Shoot"
|
||||
play_clip(logical)
|
||||
var clip := _resolve_clip(logical)
|
||||
var dur: float = 0.26
|
||||
if anim and not clip.is_empty() and anim.has_animation(clip):
|
||||
dur = maxf(0.16, anim.get_animation(clip).length * 0.82)
|
||||
_fire_lock = dur
|
||||
|
||||
|
||||
func set_loco(strafe: float, back: float, run_amount: float, delta: float = 0.016, aiming: bool = false) -> void:
|
||||
_aiming = aiming
|
||||
if tree and tree.active:
|
||||
tree.active = false
|
||||
if _fire_lock > 0.0:
|
||||
_fire_lock = maxf(_fire_lock - delta, 0.0)
|
||||
if aiming:
|
||||
_face_move(0.0, delta, 14.0)
|
||||
return
|
||||
if aiming:
|
||||
_face_move(0.0, delta, 14.0)
|
||||
var aim_stick := Vector2(clampf(strafe, -1.0, 1.0), clampf(back, -1.0, 1.0))
|
||||
_blend_pos = aim_stick.normalized() if aim_stick.length() >= 0.16 else Vector2.ZERO
|
||||
_run_amt = run_amount if aim_stick.length() >= 0.16 else 0.0
|
||||
if aim_stick.length() < 0.16:
|
||||
_mode = "aim_idle"
|
||||
play_clip("AimIdle")
|
||||
elif run_amount > 0.5:
|
||||
_mode = "aim_run"
|
||||
play_clip("AimRun")
|
||||
else:
|
||||
_mode = "aim_walk"
|
||||
play_clip("AimWalk")
|
||||
return
|
||||
var target := Vector2(clampf(strafe, -1.0, 1.0), clampf(back, -1.0, 1.0))
|
||||
if target.length() < 0.16:
|
||||
_face_move(0.0, delta, 10.0)
|
||||
_blend_pos = Vector2.ZERO
|
||||
_run_amt = 0.0
|
||||
if show_pistol or hide_head:
|
||||
_mode = "idle_gun"
|
||||
play_clip("Idle_Gun")
|
||||
else:
|
||||
_mode = "idle"
|
||||
play_clip("Idle")
|
||||
return
|
||||
_blend_pos = target.normalized()
|
||||
_run_amt = 1.0 if run_amount > 0.5 else 0.0
|
||||
# S / SA / SD — backpedal, no 180 spin
|
||||
if _blend_pos.y > 0.2:
|
||||
_face_move(0.0, delta, 12.0)
|
||||
_mode = "Run_Back"
|
||||
play_clip("Run_Back")
|
||||
return
|
||||
# Pure A / D — sidestep, keep facing camera-forward
|
||||
if absf(_blend_pos.x) >= 0.62 and absf(_blend_pos.y) <= 0.42:
|
||||
_face_move(0.0, delta, 14.0)
|
||||
var side: String = "Run_Left" if _blend_pos.x < 0.0 else "Run_Right"
|
||||
_mode = side
|
||||
play_clip(side)
|
||||
return
|
||||
# W / WA / WD — face the stick + Walk/Run
|
||||
var yaw := atan2(-_blend_pos.x, maxf(-_blend_pos.y, 0.001))
|
||||
yaw = clampf(yaw, deg_to_rad(-90.0), deg_to_rad(90.0))
|
||||
_face_move(yaw, delta, 16.0)
|
||||
var logical: String = "Run" if _run_amt > 0.5 else "Walk"
|
||||
_mode = logical
|
||||
var pace: float = 0.92 + _run_amt * 0.18 if logical == "Run" else 0.88 + target.length() * 0.14
|
||||
play_clip(logical, pace)
|
||||
|
||||
|
||||
func _face_move(yaw: float, delta: float, rate: float) -> void:
|
||||
if current == null or not is_instance_valid(current):
|
||||
return
|
||||
current.rotation.y = lerp_angle(current.rotation.y, yaw, clampf(rate * delta, 0.0, 1.0))
|
||||
|
||||
|
||||
func set_first_person(fp: bool) -> void:
|
||||
hide_head = fp
|
||||
print("Look 1P=", fp, " idle=Idle_Gun" if fp else " idle=Idle")
|
||||
if current != null and is_instance_valid(current):
|
||||
_apply_keep(current, last_keep)
|
||||
|
||||
|
||||
func set_pistol_drawn(drawn: bool) -> void:
|
||||
show_pistol = drawn
|
||||
if current != null and is_instance_valid(current):
|
||||
_apply_keep(current, last_keep)
|
||||
|
||||
|
||||
func set_meshes_visible(v: bool) -> void:
|
||||
if current == null:
|
||||
return
|
||||
if v:
|
||||
_apply_keep(current, last_keep)
|
||||
else:
|
||||
_hide_all_visuals(current)
|
||||
|
||||
|
||||
func set_aim_pitch(radians: float) -> void:
|
||||
if not _aim_enabled:
|
||||
_aim_pitch = 0.0
|
||||
return
|
||||
_aim_pitch = clampf(radians, deg_to_rad(-50.0), deg_to_rad(65.0))
|
||||
|
||||
|
||||
func get_head_global_pos() -> Vector3:
|
||||
if _skel == null or not is_instance_valid(_skel) or _bone_head < 0:
|
||||
return Vector3.INF
|
||||
var pose: Transform3D = _skel.get_bone_global_pose(_bone_head)
|
||||
return _skel.to_global(pose.origin)
|
||||
|
||||
|
||||
func set_fp_ride(cam: Camera3D, pivot: Node3D, on: bool) -> void:
|
||||
_fp_cam = cam
|
||||
_fp_pivot = pivot
|
||||
_ride_fp = on and cam != null
|
||||
|
||||
|
||||
func set_aim_enabled(on: bool) -> void:
|
||||
_aim_enabled = on
|
||||
if not on:
|
||||
_aim_pitch = 0.0
|
||||
if _mod:
|
||||
_mod.active = on
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_dbg += delta
|
||||
if _dbg < 4.0:
|
||||
return
|
||||
_dbg = 0.0
|
||||
print("Loco dbg mode=", _mode,
|
||||
" pos=", snapped(_blend_pos, Vector2(0.01, 0.01)),
|
||||
" run=", snappedf(_run_amt, 0.01),
|
||||
" aim=", _aiming,
|
||||
" player=", anim.current_animation if anim else "?")
|
||||
if _aim_enabled:
|
||||
print("AimFold on pitch=", snappedf(rad_to_deg(_aim_pitch), 0.1),
|
||||
" abd=", _bone_name(_bone_abdomen),
|
||||
" torso=", _bone_name(_bone_torso),
|
||||
" chest=", _bone_name(_bone_chest),
|
||||
" mod=", _mod != null, " active=", _mod.active if _mod else false)
|
||||
|
||||
|
||||
func _cache_skeleton() -> void:
|
||||
_skel = null
|
||||
_bone_abdomen = -1
|
||||
_bone_torso = -1
|
||||
_bone_chest = -1
|
||||
_bone_head = -1
|
||||
_bone_rarm = -1
|
||||
_bone_larm = -1
|
||||
_bone_rfore = -1
|
||||
_bone_lfore = -1
|
||||
_mod = null
|
||||
if current == null:
|
||||
return
|
||||
_skel = current.find_child("Skeleton3D", true, false) as Skeleton3D
|
||||
if _skel == null:
|
||||
push_warning("Look: no Skeleton3D on CreatedBody")
|
||||
return
|
||||
_bone_abdomen = _find_exact(["Abdomen", "mixamorig_Spine", "mixamorig:Spine", "Spine"])
|
||||
_bone_torso = _find_exact(["Torso", "mixamorig_Spine1", "mixamorig:Spine1", "Spine1"])
|
||||
_bone_chest = _find_exact(["Chest", "mixamorig_Spine2", "mixamorig:Spine2", "Spine2", "UpperChest"])
|
||||
_bone_head = _find_exact(["Head", "mixamorig_Head", "mixamorig:Head"])
|
||||
_bone_rarm = _find_limb(["mixamorig_RightArm", "mixamorig:RightArm", "RightArm", "Arm.R"])
|
||||
_bone_larm = _find_limb(["mixamorig_LeftArm", "mixamorig:LeftArm", "LeftArm", "Arm.L"])
|
||||
_bone_rfore = _find_limb(["mixamorig_RightForeArm", "mixamorig:RightForeArm", "RightForeArm", "ForeArm.R"])
|
||||
_bone_lfore = _find_limb(["mixamorig_LeftForeArm", "mixamorig:LeftForeArm", "LeftForeArm", "ForeArm.L"])
|
||||
_mod = AimBend.new()
|
||||
_mod.name = "AimBend"
|
||||
_mod.host = self
|
||||
_mod.active = false
|
||||
_skel.add_child(_mod)
|
||||
if not _logged_bones:
|
||||
_logged_bones = true
|
||||
print("Look skeleton=", _skel.name,
|
||||
" abd=", _bone_name(_bone_abdomen),
|
||||
" torso=", _bone_name(_bone_torso),
|
||||
" chest=", _bone_name(_bone_chest),
|
||||
" head=", _bone_name(_bone_head),
|
||||
" rarm=", _bone_name(_bone_rarm),
|
||||
" larm=", _bone_name(_bone_larm),
|
||||
" modifier=AimBend")
|
||||
|
||||
|
||||
func _find_exact(names: Array[String]) -> int:
|
||||
if _skel == null:
|
||||
return -1
|
||||
for bone_key in names:
|
||||
var idx: int = _skel.find_bone(bone_key)
|
||||
if idx >= 0:
|
||||
var low := _skel.get_bone_name(idx).to_lower()
|
||||
var want_head := "head" in bone_key.to_lower()
|
||||
if "shoulder" in low or "arm" in low or "neck" in low:
|
||||
continue
|
||||
if "head" in low and not want_head:
|
||||
continue
|
||||
return idx
|
||||
return -1
|
||||
|
||||
|
||||
func _find_limb(names: Array[String]) -> int:
|
||||
if _skel == null:
|
||||
return -1
|
||||
for bone_key in names:
|
||||
var idx: int = _skel.find_bone(bone_key)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
for i in range(_skel.get_bone_count()):
|
||||
var raw: String = _skel.get_bone_name(i)
|
||||
var low: String = raw.to_lower()
|
||||
for bone_key in names:
|
||||
var want: String = bone_key.to_lower()
|
||||
if low == want or low.contains(want):
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
func _bone_name(idx: int) -> String:
|
||||
if _skel == null or idx < 0:
|
||||
return str(idx)
|
||||
return "%s(%d)" % [_skel.get_bone_name(idx), idx]
|
||||
|
||||
|
||||
func _apply_waist_now() -> void:
|
||||
if _skel == null:
|
||||
return
|
||||
if _aim_enabled:
|
||||
var pitch: float = -_aim_pitch
|
||||
if hide_head:
|
||||
# 1P: lean the whole torso so the gun and shoes enter the lens.
|
||||
_set_fold(_bone_abdomen, pitch * 0.40)
|
||||
_set_fold(_bone_torso, pitch * 0.22)
|
||||
_set_fold(_bone_chest, pitch * 0.08)
|
||||
var axis: Vector3 = Vector3.FORWARD
|
||||
match arm_axis_id:
|
||||
0:
|
||||
axis = Vector3.RIGHT
|
||||
1:
|
||||
axis = Vector3.UP
|
||||
_:
|
||||
axis = Vector3.FORWARD
|
||||
var arm_amt: float = 0.22
|
||||
if _aim_pitch > 0.0:
|
||||
arm_amt = 0.36
|
||||
_set_fold_axis(_bone_rarm, pitch * arm_amt, axis)
|
||||
_set_fold_axis(_bone_larm, pitch * arm_amt * 0.55, axis)
|
||||
else:
|
||||
_set_fold(_bone_abdomen, pitch * 0.42)
|
||||
_set_fold(_bone_torso, pitch * 0.38)
|
||||
_set_fold(_bone_chest, pitch * 0.28)
|
||||
_snap_fp_eye()
|
||||
|
||||
|
||||
func _snap_fp_eye() -> void:
|
||||
if not _ride_fp or _fp_pivot == null or _skel == null or _bone_head < 0:
|
||||
return
|
||||
var pose: Transform3D = _skel.get_bone_global_pose(_bone_head)
|
||||
var xf: Transform3D = _skel.global_transform * pose
|
||||
var body: Node3D = _fp_pivot.get_parent() as Node3D
|
||||
var face := Vector3.FORWARD
|
||||
var up := Vector3.UP
|
||||
if body:
|
||||
face = -body.global_transform.basis.z
|
||||
face.y = 0.0
|
||||
if face.length_squared() < 0.0001:
|
||||
face = -body.global_transform.basis.z
|
||||
face = face.normalized()
|
||||
up = body.global_transform.basis.y.normalized()
|
||||
else:
|
||||
face = xf.basis.z.normalized()
|
||||
up = xf.basis.y.normalized()
|
||||
_fp_pivot.global_position = xf.origin + face * 0.03 + up * 0.12
|
||||
|
||||
|
||||
func _set_fold(idx: int, angle: float) -> void:
|
||||
_set_fold_axis(idx, angle, Vector3.RIGHT)
|
||||
|
||||
|
||||
func _set_fold_axis(idx: int, angle: float, axis: Vector3) -> void:
|
||||
if idx < 0 or _skel == null:
|
||||
return
|
||||
if absf(angle) < 0.0005:
|
||||
return
|
||||
var pose: Quaternion = _skel.get_bone_pose_rotation(idx)
|
||||
_skel.set_bone_pose_rotation(idx, pose * Quaternion(axis.normalized(), angle))
|
||||
|
||||
|
||||
func cycle_arm_axis() -> void:
|
||||
arm_axis_id = (arm_axis_id + 1) % 3
|
||||
print("ARM AXIS ", ["X (RIGHT)", "Y (UP)", "Z (FORWARD)"][arm_axis_id])
|
||||
|
||||
|
||||
func _apply_keep_deferred() -> void:
|
||||
if current != null and is_instance_valid(current):
|
||||
_apply_keep(current, last_keep)
|
||||
if _skel == null:
|
||||
_cache_skeleton()
|
||||
|
||||
|
||||
func _clear_old() -> void:
|
||||
current = null
|
||||
anim = null
|
||||
if tree and is_instance_valid(tree):
|
||||
tree.queue_free()
|
||||
tree = null
|
||||
_walk_bs = null
|
||||
_run_bs = null
|
||||
_aim_walk_bs = null
|
||||
_aim_run_bs = null
|
||||
_mode = ""
|
||||
_blend_pos = Vector2.ZERO
|
||||
_run_amt = 0.0
|
||||
_aiming = false
|
||||
_fire_lock = 0.0
|
||||
_skel = null
|
||||
_mod = null
|
||||
_bone_abdomen = -1
|
||||
_bone_torso = -1
|
||||
_bone_chest = -1
|
||||
_bone_head = -1
|
||||
_bone_rarm = -1
|
||||
_bone_larm = -1
|
||||
_bone_rfore = -1
|
||||
_bone_lfore = -1
|
||||
_logged_bones = false
|
||||
_aim_enabled = false
|
||||
_aim_pitch = 0.0
|
||||
_ride_fp = false
|
||||
_fp_cam = null
|
||||
_fp_pivot = null
|
||||
for child in get_children():
|
||||
child.hide()
|
||||
child.queue_free()
|
||||
|
||||
|
||||
func _hide_all_visuals(root: Node) -> void:
|
||||
var stack: Array[Node] = [root]
|
||||
while stack.size() > 0:
|
||||
var n: Node = stack.pop_back()
|
||||
if n is GeometryInstance3D:
|
||||
n.visible = false
|
||||
for c in n.get_children():
|
||||
stack.append(c)
|
||||
|
||||
|
||||
func _apply_keep(root: Node, keep: Array[String]) -> void:
|
||||
if root == null or keep.is_empty():
|
||||
return
|
||||
var shown: PackedStringArray = []
|
||||
var hidden: PackedStringArray = []
|
||||
var stack: Array[Node] = [root]
|
||||
while stack.size() > 0:
|
||||
var n: Node = stack.pop_back()
|
||||
if n is GeometryInstance3D:
|
||||
var lab := _label(n)
|
||||
var keep_this := _matches_keep(n, keep) and not _is_banned(lab)
|
||||
if show_pistol and _is_pistol_label(lab):
|
||||
keep_this = true
|
||||
if hide_head and _is_head_label(lab):
|
||||
keep_this = false
|
||||
n.visible = keep_this
|
||||
if keep_this:
|
||||
shown.append(_label(n))
|
||||
else:
|
||||
hidden.append(_label(n))
|
||||
for c in n.get_children():
|
||||
stack.append(c)
|
||||
if not _logged_names:
|
||||
_logged_names = true
|
||||
print("Look keep=", keep)
|
||||
print("Look SHOWN (", shown.size(), "): ", shown)
|
||||
print("Look HIDDEN (", hidden.size(), "): ", hidden)
|
||||
|
||||
|
||||
func _label(n: Node) -> String:
|
||||
var label := n.name
|
||||
if n is MeshInstance3D:
|
||||
var mi := n as MeshInstance3D
|
||||
if mi.mesh and not mi.mesh.resource_name.is_empty():
|
||||
label = "%s|%s" % [n.name, mi.mesh.resource_name]
|
||||
return label
|
||||
|
||||
|
||||
func _matches_keep(n: Node, keep: Array[String]) -> bool:
|
||||
var labels: Array[String] = [n.name]
|
||||
if n is MeshInstance3D:
|
||||
var mi := n as MeshInstance3D
|
||||
if mi.mesh and not mi.mesh.resource_name.is_empty():
|
||||
labels.append(mi.mesh.resource_name)
|
||||
for label in labels:
|
||||
for k in keep:
|
||||
if k.is_empty():
|
||||
continue
|
||||
if label == k or label.begins_with(k):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _is_head_label(mesh_name: String) -> bool:
|
||||
var low := mesh_name.to_lower()
|
||||
if "pistol" in low:
|
||||
return false
|
||||
return ("_head" in low) or low.ends_with("head") or ("hair" in low) or ("hat" in low) or ("neck" in low)
|
||||
|
||||
|
||||
func _is_pistol_label(mesh_name: String) -> bool:
|
||||
return mesh_name.contains("Pistol") or mesh_name.contains("pistol")
|
||||
|
||||
|
||||
func _is_banned(mesh_name: String) -> bool:
|
||||
var n := mesh_name
|
||||
return (
|
||||
n.begins_with("Horse")
|
||||
or n.contains("Horse")
|
||||
or n.begins_with("King")
|
||||
or n.begins_with("Witch")
|
||||
or n.begins_with("Medieval")
|
||||
or n.begins_with("Beach")
|
||||
or n.begins_with("Sword")
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bh5kr1365v1bq
|
||||
@@ -0,0 +1,30 @@
|
||||
extends Control
|
||||
|
||||
@onready var new_game_button: Button = $CenterRoot/MenuColumn/NewGameButton
|
||||
@onready var continue_button: Button = $CenterRoot/MenuColumn/ContinueButton
|
||||
@onready var quit_button: Button = $CenterRoot/MenuColumn/QuitButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
if new_game_button:
|
||||
new_game_button.pressed.connect(_on_new_game)
|
||||
if continue_button:
|
||||
continue_button.pressed.connect(_on_continue)
|
||||
if quit_button:
|
||||
quit_button.pressed.connect(_on_quit)
|
||||
if continue_button:
|
||||
continue_button.disabled = not GameState.has_save
|
||||
|
||||
|
||||
func _on_new_game() -> void:
|
||||
get_tree().change_scene_to_file("res://Scenes/character_creation.tscn")
|
||||
|
||||
|
||||
func _on_continue() -> void:
|
||||
Engine.set_meta("load_save", true)
|
||||
get_tree().change_scene_to_file("res://Scenes/main.tscn")
|
||||
|
||||
|
||||
func _on_quit() -> void:
|
||||
get_tree().quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cb6r3wpwg73jg
|
||||
@@ -0,0 +1,10 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Marijuana Seeds"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://03eo5b1lwlm7
|
||||
@@ -0,0 +1,87 @@
|
||||
extends Node3D
|
||||
## Swap only when the real Moon is about to leave the far plane.
|
||||
## At handover the disc matches the real angular size. It shrinks with distance.
|
||||
## A late boost only kicks in when the disc would become a speck (Earth).
|
||||
|
||||
@export var real_moon_path: NodePath = ^".."
|
||||
@export var surface_high_path: NodePath = ^"../SurfaceHigh"
|
||||
@export var impostor_mesh_path: NodePath = ^"ImposterSphere"
|
||||
@export var switch_distance: float = 850000.0
|
||||
@export var visual_distance: float = 280000.0
|
||||
@export var real_radius: float = 173700.0
|
||||
@export var far_boost_start: float = 2500000.0
|
||||
@export var far_boost_end: float = 20000000.0
|
||||
@export var far_boost: float = 5.0
|
||||
|
||||
var _real_moon: Node3D
|
||||
var _surface_high: Node3D
|
||||
var _surface_low: Node3D
|
||||
var _mesh: MeshInstance3D
|
||||
var _base_radius: float = 0.5
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_real_moon = get_node_or_null(real_moon_path) as Node3D
|
||||
_surface_high = get_node_or_null(surface_high_path) as Node3D
|
||||
if _surface_high == null and _real_moon:
|
||||
_surface_high = _real_moon.get_node_or_null("SurfaceHigh") as Node3D
|
||||
if _real_moon:
|
||||
_surface_low = _real_moon.get_node_or_null("SurfaceLow") as Node3D
|
||||
_mesh = get_node_or_null(impostor_mesh_path) as MeshInstance3D
|
||||
if _mesh == null:
|
||||
_mesh = find_child("ImposterSphere", true, false) as MeshInstance3D
|
||||
if _mesh:
|
||||
_mesh.top_level = true
|
||||
_mesh.visible = false
|
||||
_mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
if _mesh.mesh is SphereMesh:
|
||||
var sm := _mesh.mesh as SphereMesh
|
||||
_base_radius = maxf(sm.radius, 0.5)
|
||||
sm.radial_segments = maxi(sm.radial_segments, 32)
|
||||
sm.rings = maxi(sm.rings, 16)
|
||||
var mat := _mesh.get_active_material(0)
|
||||
if mat == null:
|
||||
mat = _mesh.material_override
|
||||
if mat is StandardMaterial3D:
|
||||
var copy := (mat as StandardMaterial3D).duplicate() as StandardMaterial3D
|
||||
copy.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_mesh.material_override = copy
|
||||
print("MoonImposter ready switch=", switch_distance, " mesh=", _mesh)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _hide_real(hide: bool) -> void:
|
||||
if _surface_high:
|
||||
_surface_high.visible = not hide
|
||||
if _surface_low:
|
||||
_surface_low.visible = not hide
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _real_moon == null or _mesh == null:
|
||||
return
|
||||
var viewer: Node3D = FloatingOrigin.focus
|
||||
if viewer == null or not is_instance_valid(viewer):
|
||||
_mesh.visible = false
|
||||
return
|
||||
if _real_moon.has_meta("hide_crust") and bool(_real_moon.get_meta("hide_crust")):
|
||||
_mesh.visible = false
|
||||
_hide_real(true)
|
||||
return
|
||||
var to_moon: Vector3 = _real_moon.global_position - viewer.global_position
|
||||
var dist: float = to_moon.length()
|
||||
if dist < switch_distance:
|
||||
_mesh.visible = false
|
||||
_hide_real(false)
|
||||
return
|
||||
_hide_real(true)
|
||||
var dir: Vector3 = to_moon / maxf(dist, 1.0)
|
||||
_mesh.global_position = viewer.global_position + dir * visual_distance
|
||||
var exact: float = real_radius * (visual_distance / dist)
|
||||
var t: float = 0.0
|
||||
if dist > far_boost_start:
|
||||
t = clampf((dist - far_boost_start) / maxf(far_boost_end - far_boost_start, 1.0), 0.0, 1.0)
|
||||
var visual_radius: float = exact * lerpf(1.0, far_boost, t)
|
||||
visual_radius = clampf(visual_radius, 200.0, visual_distance * 0.85)
|
||||
_mesh.scale = Vector3.ONE * (visual_radius / _base_radius)
|
||||
_mesh.visible = true
|
||||
@@ -0,0 +1 @@
|
||||
uid://db5dujh3xfuky
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Multitool"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dc246e7164y2f
|
||||
@@ -0,0 +1,63 @@
|
||||
extends Control
|
||||
|
||||
@onready var resume_button: Button = $PauseButtons/ResumeButton
|
||||
@onready var save_button: Button = $PauseButtons/SaveButton
|
||||
@onready var quit_button: Button = $PauseButtons/QuitToMenuButton
|
||||
var player: Node = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visible = false
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
if resume_button:
|
||||
resume_button.pressed.connect(_on_resume)
|
||||
if save_button:
|
||||
save_button.pressed.connect(_on_save)
|
||||
if quit_button:
|
||||
quit_button.pressed.connect(_on_quit_to_menu)
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not event.is_action_pressed("ui_cancel"):
|
||||
return
|
||||
var banner := get_node_or_null("/root/Node3D/HUD/ScrambleBanner")
|
||||
if banner and banner.visible:
|
||||
return
|
||||
if visible or get_tree().paused:
|
||||
_on_resume()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var p := get_tree().get_first_node_in_group("player")
|
||||
if p == null:
|
||||
p = get_node_or_null("/root/Node3D/Player")
|
||||
if p:
|
||||
open_pause(p)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func open_pause(player_ref: Node) -> void:
|
||||
player = player_ref
|
||||
visible = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
get_tree().paused = true
|
||||
|
||||
|
||||
func _on_resume() -> void:
|
||||
visible = false
|
||||
get_tree().paused = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
|
||||
func _on_save() -> void:
|
||||
if player:
|
||||
GameState.save_game(player)
|
||||
else:
|
||||
var p := get_tree().get_first_node_in_group("player")
|
||||
if p:
|
||||
GameState.save_game(p)
|
||||
|
||||
|
||||
func _on_quit_to_menu() -> void:
|
||||
get_tree().paused = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
get_tree().change_scene_to_file("res://Scenes/main_menu.tscn")
|
||||
@@ -0,0 +1 @@
|
||||
uid://soef5g0d1u0a
|
||||
@@ -0,0 +1,10 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Beer Can"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfqr8db8q1y4b
|
||||
@@ -0,0 +1,134 @@
|
||||
@tool
|
||||
extends Node3D
|
||||
## Hollow planet collision. Attach to Moon or Earth.
|
||||
## SphereShape3D is a SOLID ball — anything inside the radius gets ejected.
|
||||
## This builds a concave surface (triangles only) with an optional station hole.
|
||||
## Visual mesh stays on SurfaceHigh. Collision matches that radius.
|
||||
|
||||
@export var surface_mesh_path: NodePath = ^"SurfaceHigh"
|
||||
@export var old_body_path: NodePath = ^"MoonBody"
|
||||
@export var hole_anchor_path: NodePath = ^"LovellAnchor"
|
||||
@export var hole_radius_m: float = 205.0
|
||||
@export var segments: int = 48
|
||||
@export var rings: int = 32
|
||||
@export var rebuild_shell: bool = false:
|
||||
set(v):
|
||||
rebuild_shell = false
|
||||
if v:
|
||||
build()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
build()
|
||||
|
||||
|
||||
func build() -> void:
|
||||
var radius: float = _visual_radius()
|
||||
if radius < 10.0:
|
||||
push_warning("PlanetShell: no radius from SurfaceHigh")
|
||||
return
|
||||
_disable_solid_body()
|
||||
var hole_dir := Vector3.ZERO
|
||||
var hole_cos := 1.0
|
||||
var anchor := get_node_or_null(hole_anchor_path) as Node3D
|
||||
if anchor and hole_radius_m > 1.0:
|
||||
var to_a: Vector3 = anchor.global_position - global_position
|
||||
if to_a.length() > 1.0:
|
||||
hole_dir = global_transform.basis.inverse() * to_a.normalized()
|
||||
hole_dir = hole_dir.normalized()
|
||||
var ang: float = atan(hole_radius_m / radius)
|
||||
hole_cos = cos(ang)
|
||||
var faces := _make_faces(radius, hole_dir, hole_cos)
|
||||
var body := get_node_or_null("ShellBody") as StaticBody3D
|
||||
if body == null:
|
||||
body = StaticBody3D.new()
|
||||
body.name = "ShellBody"
|
||||
add_child(body)
|
||||
body.owner = _scene_root()
|
||||
body.transform = Transform3D()
|
||||
body.collision_layer = 1
|
||||
body.collision_mask = 1
|
||||
var col := body.get_node_or_null("CollisionShape3D") as CollisionShape3D
|
||||
if col == null:
|
||||
col = CollisionShape3D.new()
|
||||
col.name = "CollisionShape3D"
|
||||
body.add_child(col)
|
||||
col.owner = body.owner
|
||||
var shape := ConcavePolygonShape3D.new()
|
||||
shape.set_faces(faces)
|
||||
col.shape = shape
|
||||
col.transform = Transform3D()
|
||||
print("PlanetShell ", name, " radius=", radius, " faces=", faces.size() / 3, " hole=", hole_dir != Vector3.ZERO, " anchor=", anchor)
|
||||
|
||||
|
||||
func _visual_radius() -> float:
|
||||
var high := get_node_or_null(surface_mesh_path) as MeshInstance3D
|
||||
if high and high.mesh is SphereMesh:
|
||||
var sm := high.mesh as SphereMesh
|
||||
var rad: float = sm.radius
|
||||
if sm.height > rad * 1.5:
|
||||
rad = sm.height * 0.5
|
||||
var scl: float = maxf(high.scale.x, maxf(high.scale.y, high.scale.z))
|
||||
rad *= scl
|
||||
if rad > 100.0:
|
||||
return rad
|
||||
return 173700.0
|
||||
|
||||
|
||||
func _disable_solid_body() -> void:
|
||||
var old := get_node_or_null(old_body_path)
|
||||
if old == null:
|
||||
return
|
||||
var col := old.get_node_or_null("CollisionShape3D") as CollisionShape3D
|
||||
if col:
|
||||
col.disabled = true
|
||||
if old is CollisionObject3D:
|
||||
(old as CollisionObject3D).collision_layer = 0
|
||||
(old as CollisionObject3D).collision_mask = 0
|
||||
print("PlanetShell: MoonBody DISABLED (solid sphere ejects)")
|
||||
|
||||
|
||||
func _make_faces(radius: float, hole_dir: Vector3, hole_cos: float) -> PackedVector3Array:
|
||||
var faces := PackedVector3Array()
|
||||
var segs: int = maxi(12, segments)
|
||||
var rgs: int = maxi(8, rings)
|
||||
var use_hole: bool = hole_dir.length_squared() > 0.5
|
||||
for lat in range(rgs):
|
||||
var t0: float = float(lat) / float(rgs) * PI - PI * 0.5
|
||||
var t1: float = float(lat + 1) / float(rgs) * PI - PI * 0.5
|
||||
var y0: float = sin(t0)
|
||||
var y1: float = sin(t1)
|
||||
var r0: float = cos(t0)
|
||||
var r1: float = cos(t1)
|
||||
for lon in range(segs):
|
||||
var a0: float = float(lon) / float(segs) * TAU
|
||||
var a1: float = float(lon + 1) / float(segs) * TAU
|
||||
var p00 := Vector3(r0 * cos(a0), y0, r0 * sin(a0)) * radius
|
||||
var p10 := Vector3(r0 * cos(a1), y0, r0 * sin(a1)) * radius
|
||||
var p01 := Vector3(r1 * cos(a0), y1, r1 * sin(a0)) * radius
|
||||
var p11 := Vector3(r1 * cos(a1), y1, r1 * sin(a1)) * radius
|
||||
_add_tri(faces, p00, p01, p11, hole_dir, hole_cos, use_hole)
|
||||
_add_tri(faces, p00, p11, p10, hole_dir, hole_cos, use_hole)
|
||||
return faces
|
||||
|
||||
|
||||
func _add_tri(faces: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3, hole_dir: Vector3, hole_cos: float, use_hole: bool) -> void:
|
||||
if use_hole:
|
||||
var mid: Vector3 = (a + b + c) / 3.0
|
||||
if mid.length_squared() < 0.001:
|
||||
return
|
||||
if mid.normalized().dot(hole_dir) >= hole_cos:
|
||||
return
|
||||
# Both windings. Concave shells are one-sided; a flipped face is a hole you fall through.
|
||||
faces.append(a)
|
||||
faces.append(b)
|
||||
faces.append(c)
|
||||
faces.append(a)
|
||||
faces.append(c)
|
||||
faces.append(b)
|
||||
|
||||
|
||||
func _scene_root() -> Node:
|
||||
if Engine.is_editor_hint() and get_tree() and get_tree().edited_scene_root:
|
||||
return get_tree().edited_scene_root
|
||||
return owner if owner else self
|
||||
@@ -0,0 +1 @@
|
||||
uid://2fx6webb7m8d
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Plasma Cutter (Damaged)"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdo20owmglm22
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://bqssr0j8iphme
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Protein Paste"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://ch56mng5ltaqf
|
||||
@@ -0,0 +1,10 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Ration Pack"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cl275qnxinw8q
|
||||
@@ -0,0 +1,26 @@
|
||||
extends Node
|
||||
|
||||
var rng = RandomNumberGenerator.new()
|
||||
var materials = [
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_arid_rocks_1.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_arid_rocks_2.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_arid_rocks_3.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_mossy_rocks_1.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_mossy_rocks_2.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_sand_stone_rocks.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_snow_rocks_1.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_snow_rocks_2.tres",
|
||||
"res://Kyle's Rock Pack/Kyle Fuji/Materials/M_snow_rocks_3.tres"
|
||||
]
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
rng.randomize()
|
||||
var my_random_number = rng.randi_range(0, len(materials) - 1)
|
||||
|
||||
var NEW_MATERIAL = load(materials[my_random_number])
|
||||
var nodes = get_tree().get_nodes_in_group("change_material")
|
||||
for node in nodes:
|
||||
for child in node.get_children():
|
||||
if child is MeshInstance3D:
|
||||
child.material_override = NEW_MATERIAL
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6oobwtp7lnw0
|
||||
@@ -0,0 +1,46 @@
|
||||
extends Control
|
||||
|
||||
var alarm_label: Label
|
||||
var dismiss_button: Button
|
||||
|
||||
func _ready() -> void:
|
||||
visible = false
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
alarm_label = find_child("AlarmLabel", true, false) as Label
|
||||
dismiss_button = find_child("DismissButton", true, false) as Button
|
||||
|
||||
if dismiss_button:
|
||||
dismiss_button.pressed.connect(hide_banner)
|
||||
|
||||
if not GameState.scramble_begun.is_connected(_on_scramble):
|
||||
GameState.scramble_begun.connect(_on_scramble)
|
||||
|
||||
func _on_scramble() -> void:
|
||||
visible = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
if alarm_label == null:
|
||||
alarm_label = find_child("AlarmLabel", true, false) as Label
|
||||
|
||||
if alarm_label:
|
||||
alarm_label.modulate = Color.WHITE
|
||||
alarm_label.self_modulate = Color.WHITE
|
||||
alarm_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
alarm_label.add_theme_font_size_override("font_size", 22)
|
||||
alarm_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
alarm_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
alarm_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
alarm_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
alarm_label.text = "PRIORITY ALERT — PEOPLE'S PRIVATEER FLEET INBOUND\nUnregistered prize crews on approach. All hands to extraction. ~10 minutes."
|
||||
|
||||
print("SCRAMBLE: PPF inbound")
|
||||
|
||||
func hide_banner() -> void:
|
||||
if not visible:
|
||||
return
|
||||
visible = false
|
||||
if not get_tree().paused:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
print("SCRAMBLE banner dismissed")
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1brrhkbw71xg
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Scrap Metal"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://oljblbitud3n
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@export var item_name: String = "Soy Bean Seeds"
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
if player.has_method("add_item"):
|
||||
player.add_item(item_name)
|
||||
print("Picked up: ", item_name)
|
||||
queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dobhysl1n6hi1
|
||||
@@ -0,0 +1,38 @@
|
||||
extends HBoxContainer
|
||||
|
||||
@onready var btn_pause: Button = $BtnPause
|
||||
@onready var btn_x1: Button = $BtnX1
|
||||
@onready var btn_x2: Button = $BtnX2
|
||||
@onready var btn_x4: Button = $BtnX4
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
btn_pause.pressed.connect(_on_pause)
|
||||
btn_x1.pressed.connect(_on_x1)
|
||||
btn_x2.pressed.connect(_on_x2)
|
||||
btn_x4.pressed.connect(_on_x4)
|
||||
|
||||
if GameState.has_signal("time_scale_changed"):
|
||||
GameState.time_scale_changed.connect(_on_scale_changed)
|
||||
|
||||
_on_scale_changed(GameState.time_scale)
|
||||
|
||||
func _on_pause() -> void:
|
||||
GameState.set_time_scale(0.0)
|
||||
|
||||
func _on_x1() -> void:
|
||||
GameState.set_time_scale(1.0)
|
||||
|
||||
func _on_x2() -> void:
|
||||
GameState.set_time_scale(2.0)
|
||||
|
||||
func _on_x4() -> void:
|
||||
GameState.set_time_scale(4.0)
|
||||
|
||||
func _on_scale_changed(new_scale: float) -> void:
|
||||
btn_pause.disabled = is_equal_approx(new_scale, 0.0)
|
||||
btn_x1.disabled = is_equal_approx(new_scale, 1.0)
|
||||
btn_x2.disabled = is_equal_approx(new_scale, 2.0)
|
||||
btn_x4.disabled = is_equal_approx(new_scale, 4.0)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://sxr03xjwmthe
|
||||
@@ -0,0 +1,47 @@
|
||||
extends Node3D
|
||||
## Keep the sun disc inside the cockpit far plane. Aim SunLight.
|
||||
## Never leave the disc sitting on the Moon.
|
||||
|
||||
@export var sun_disc_path: NodePath = ^"SunDisc"
|
||||
@export var sun_light_path: NodePath = ^"../SunLight"
|
||||
@export var sun_direction: Vector3 = Vector3(-0.82, 0.45, 0.35)
|
||||
@export var visual_distance: float = 220000.0
|
||||
@export var visual_radius: float = 9000.0
|
||||
|
||||
var _mesh: MeshInstance3D
|
||||
var _light: DirectionalLight3D
|
||||
var _base_radius: float = 0.5
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_mesh = get_node_or_null(sun_disc_path) as MeshInstance3D
|
||||
if _mesh == null:
|
||||
_mesh = find_child("SunDisc", true, false) as MeshInstance3D
|
||||
if _mesh and _mesh.mesh is SphereMesh:
|
||||
_base_radius = maxf((_mesh.mesh as SphereMesh).radius, 0.5)
|
||||
_light = get_node_or_null(sun_light_path) as DirectionalLight3D
|
||||
if _light == null:
|
||||
_light = get_node_or_null("../SunLight") as DirectionalLight3D
|
||||
sun_direction = sun_direction.normalized()
|
||||
_aim_light()
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _aim_light() -> void:
|
||||
if _light == null:
|
||||
return
|
||||
var target: Vector3 = _light.global_position - sun_direction
|
||||
if target.is_equal_approx(_light.global_position):
|
||||
return
|
||||
_light.look_at(target, Vector3.UP)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _mesh == null:
|
||||
return
|
||||
var viewer: Node3D = FloatingOrigin.focus
|
||||
var origin: Vector3 = viewer.global_position if viewer and is_instance_valid(viewer) else global_position
|
||||
_mesh.global_position = origin + sun_direction * visual_distance
|
||||
_mesh.scale = Vector3.ONE * (visual_radius / _base_radius)
|
||||
_mesh.visible = true
|
||||
_aim_light()
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1o7uwk5jbs5j
|
||||
@@ -0,0 +1,11 @@
|
||||
extends StaticBody3D
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
print("Terminal accessed by player")
|
||||
|
||||
# Simple reaction for now
|
||||
if player.has_method("add_oxygen"):
|
||||
player.add_oxygen(15.0) # restore a little oxygen
|
||||
print("Oxygen partially restored")
|
||||
|
||||
# Later this will open a real menu
|
||||
@@ -0,0 +1 @@
|
||||
uid://drvd2kgxl2l2s
|
||||
@@ -0,0 +1,9 @@
|
||||
extends StaticBody3D
|
||||
|
||||
func on_interact(player: Node) -> void:
|
||||
var menu = get_node_or_null("/root/Node3D/VendorUI/VendorMenu")
|
||||
if menu and menu.has_method("open_menu"):
|
||||
menu.open_menu(player)
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
else:
|
||||
print("Vendor menu not found")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnf3scbnf3qvx
|
||||
@@ -0,0 +1,117 @@
|
||||
extends Control
|
||||
|
||||
var player: Node = null
|
||||
@onready var close_button: Button = $MenuPanel/MainVBox/CloseButton
|
||||
|
||||
# ===== SELL BUTTONS =====
|
||||
@onready var sell_buttons = {
|
||||
"Marijuana Seeds": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_MarijuanaSeeds,
|
||||
"Soy Bean Seeds": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_SoyBeanSeeds,
|
||||
"Ration Pack": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_RationPack,
|
||||
"Dried Noodles": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_DriedNoodles,
|
||||
"Protein Paste": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_ProteinPaste,
|
||||
"Scrap Metal": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_ScrapMetal,
|
||||
"Multitool": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_Multitool,
|
||||
"Plasma Cutter (Damaged)": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_PlasmaCutter,
|
||||
"Cooking Knife": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_CookingKnife,
|
||||
"Circuit Board": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_CircuitBoard,
|
||||
"Beer Can": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_BeerCan,
|
||||
"Hotdog": $MenuPanel/MainVBox/MainColumns/SellVBox/Sell_Hotdog
|
||||
}
|
||||
|
||||
# ===== BUY BUTTONS =====
|
||||
@onready var buy_buttons = {
|
||||
"Ration Pack": $MenuPanel/MainVBox/MainColumns/BuyVBox/Buy_RationPack,
|
||||
"Dried Noodles": $MenuPanel/MainVBox/MainColumns/BuyVBox/Buy_DriedNoodles,
|
||||
"Protein Paste": $MenuPanel/MainVBox/MainColumns/BuyVBox/Buy_ProteinPaste,
|
||||
"Beer Can": $MenuPanel/MainVBox/MainColumns/BuyVBox/Buy_BeerCan,
|
||||
"Hotdog": $MenuPanel/MainVBox/MainColumns/BuyVBox/Buy_Hotdog
|
||||
}
|
||||
|
||||
var sell_prices = {
|
||||
"Marijuana Seeds": 40,
|
||||
"Soy Bean Seeds": 25,
|
||||
"Ration Pack": 18,
|
||||
"Dried Noodles": 10,
|
||||
"Protein Paste": 14,
|
||||
"Scrap Metal": 15,
|
||||
"Multitool": 50,
|
||||
"Plasma Cutter (Damaged)": 65,
|
||||
"Cooking Knife": 28,
|
||||
"Circuit Board": 42,
|
||||
"Beer Can": 8,
|
||||
"Hotdog": 8
|
||||
}
|
||||
|
||||
var buy_prices = {
|
||||
"Ration Pack": 15,
|
||||
"Dried Noodles": 12,
|
||||
"Protein Paste": 16,
|
||||
"Beer Can": 10,
|
||||
"Hotdog": 15
|
||||
}
|
||||
|
||||
func _ready() -> void:
|
||||
for item_name in sell_buttons.keys():
|
||||
var button = sell_buttons[item_name]
|
||||
if button:
|
||||
button.pressed.connect(_sell.bind(item_name, sell_prices[item_name]))
|
||||
else:
|
||||
print("Missing sell button for: ", item_name)
|
||||
|
||||
for item_name in buy_buttons.keys():
|
||||
var button = buy_buttons[item_name]
|
||||
if button:
|
||||
button.pressed.connect(_buy.bind(item_name, buy_prices[item_name]))
|
||||
else:
|
||||
print("Missing buy button for: ", item_name)
|
||||
|
||||
if close_button:
|
||||
close_button.pressed.connect(_on_close_pressed)
|
||||
|
||||
visible = false
|
||||
|
||||
func open_menu(player_ref: Node) -> void:
|
||||
player = player_ref
|
||||
visible = true
|
||||
_update_button_states()
|
||||
print("Vendor menu opened")
|
||||
|
||||
func _update_button_states() -> void:
|
||||
if player == null:
|
||||
return
|
||||
|
||||
for item_name in sell_buttons.keys():
|
||||
var button = sell_buttons[item_name]
|
||||
if button:
|
||||
button.disabled = not player.inventory.has(item_name)
|
||||
|
||||
for item_name in buy_buttons.keys():
|
||||
var button = buy_buttons[item_name]
|
||||
if button:
|
||||
button.disabled = player.money < buy_prices[item_name]
|
||||
|
||||
func _sell(item_name: String, price: int) -> void:
|
||||
if player == null:
|
||||
return
|
||||
if player.remove_item(item_name):
|
||||
player.add_money(price)
|
||||
print("Sold ", item_name, " for ", price, " credits")
|
||||
_update_button_states()
|
||||
else:
|
||||
print("You don't have ", item_name)
|
||||
|
||||
func _buy(item_name: String, price: int) -> void:
|
||||
if player == null:
|
||||
return
|
||||
if player.money >= price:
|
||||
player.add_money(-price)
|
||||
player.add_item(item_name)
|
||||
print("Bought ", item_name)
|
||||
_update_button_states()
|
||||
else:
|
||||
print("Not enough credits")
|
||||
|
||||
func _on_close_pressed() -> void:
|
||||
visible = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
@@ -0,0 +1 @@
|
||||
uid://bbr0judkwyfhb
|
||||
Reference in New Issue
Block a user