Initial commit

This commit is contained in:
scorpion21211-cmd
2026-10-03 23:29:41 -04:00
commit 72ca74c67f
10586 changed files with 1388168 additions and 0 deletions
+9
View File
@@ -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.")
+1
View File
@@ -0,0 +1 @@
uid://d2cfgmuwrsr3y
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://4dys8ga8ap02
+16
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://di78d53y38p67
+66
View File
@@ -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!")
+1
View File
@@ -0,0 +1 @@
uid://flm6ps54u0c4
+459
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://cxrxrk0qt15dl
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://b0oppy8q64xry
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://cley0rs8im7v3
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://c5j656olsdtjs
+68
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://ikbkmt7sby7a
+632
View File
@@ -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
)
+1
View File
@@ -0,0 +1 @@
uid://1udl42sp42sp
+94
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://ckl5inm8ugypo
+152
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://b3o7ua8cv55tq
View File
+1
View File
@@ -0,0 +1 @@
uid://br0vxsoinqwe1
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bigf78nt5im5v
+646
View File
@@ -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")
)
+1
View File
@@ -0,0 +1 @@
uid://bh5kr1365v1bq
+30
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://cb6r3wpwg73jg
+10
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://03eo5b1lwlm7
+87
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://db5dujh3xfuky
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://dc246e7164y2f
+63
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://soef5g0d1u0a
+10
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://dfqr8db8q1y4b
+134
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://2fx6webb7m8d
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://cdo20owmglm22
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
uid://bqssr0j8iphme
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://ch56mng5ltaqf
+10
View File
@@ -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()
+1
View File
@@ -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
+46
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://b1brrhkbw71xg
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://oljblbitud3n
+9
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://dobhysl1n6hi1
+38
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://sxr03xjwmthe
+47
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://d1o7uwk5jbs5j
+11
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://drvd2kgxl2l2s
+9
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
uid://dnf3scbnf3qvx
+117
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://bbr0judkwyfhb