b844c1a053
arcology updated ship models in particle fx is broken on kestrel
1286 lines
40 KiB
GDScript
1286 lines
40 KiB
GDScript
extends CharacterBody3D
|
||
## PASTE 2026-09-27 — Station volume includes pads. Habitat trimesh collision + stair step.
|
||
|
||
@export var speed: float = 5.5
|
||
@export var sprint_speed: float = 9.0
|
||
@export var jump_velocity: float = 4.6
|
||
@export var mouse_sensitivity: float = 0.0028
|
||
@export var move_accel: float = 22.0
|
||
@export var move_decel: float = 28.0
|
||
@export var ads_walk_scale: float = 0.72
|
||
@export var ads_sprint_scale: float = 0.84
|
||
|
||
var mag_boots_on: bool = true
|
||
var first_person: bool = false
|
||
var in_vehicle: bool = false
|
||
var pressurized: bool = true
|
||
|
||
@onready var camera_pivot: Node3D = $CameraPivot
|
||
@onready var camera: Camera3D = $CameraPivot/Camera3D
|
||
@onready var look_loader: Node = get_node_or_null("Body")
|
||
var anim_player: AnimationPlayer = null
|
||
|
||
var free_looking: bool = false
|
||
var free_look_yaw: float = 0.0
|
||
var free_look_pitch: float = 0.0
|
||
var free_look_sensitivity: float = 0.003
|
||
var eva_peak_height: float = 0.0
|
||
|
||
var _cam_pos_target: Vector3 = Vector3(0.58, 0.38, 3.20)
|
||
var _cam_rot_target: Vector3 = Vector3(deg_to_rad(-8.0), 0.0, 0.0)
|
||
var _fov_target: float = 70.0
|
||
var _fov_kick: float = 0.0
|
||
|
||
var player_name: String = "Colonist"
|
||
var background_id: String = ""
|
||
var background_name: String = ""
|
||
var culture_id: String = ""
|
||
var culture_name: String = ""
|
||
var appearance: Dictionary = {}
|
||
|
||
var food: float = 100.0
|
||
var health: float = 100.0
|
||
var money: int = 75
|
||
var oxygen: float = 100.0
|
||
var food_decay_rate: float = 0.7
|
||
var oxygen_decay_rate: float = 0.15
|
||
var starve_damage_rate: float = 2.5
|
||
var low_oxygen_damage_rate: float = 4.0
|
||
|
||
var inventory: Array[String] = []
|
||
var inventory_open: bool = false
|
||
var interact_distance: float = 6.0
|
||
var can_interact: bool = false
|
||
var current_interactable: Node = null
|
||
|
||
@onready var needs_label: Label = get_node_or_null("/root/Node3D/HUD/NeedsLabel")
|
||
@onready var vendor_menu: Control = get_node_or_null("/root/Node3D/VendorUI/VendorMenu")
|
||
@onready var pause_menu: Control = get_node_or_null("/root/Node3D/HUD/PauseMenu")
|
||
|
||
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 EVA_SNAP_PEAK: float = 6.0
|
||
const STEP_HEIGHT: float = 0.22
|
||
const WALK_FLOORS: Array[String] = [
|
||
"Deck_",
|
||
"Stair_A_Treads", "Stair_B_Treads",
|
||
"Stair_A_Landings", "Stair_B_Landings", "Stair_LevelLandings",
|
||
"Stair_TopLandings", "Stair_HeadLanding",
|
||
"Lecture_Steps", "Lecture_Tiers", "Lecture_Seats", "School_Stage",
|
||
"Yard_Floor", "Yard_Apron", "Yard_Spoke", "Yard_Ramps",
|
||
"Yard_Bridges", "Yard_Stairs", "Yard_CollarWalk", "Yard_Piers",
|
||
"Dock_Gallery", "Dock_Threshold",
|
||
"Elev_Landings", "Fusion_Basemat", "Fusion_AmbulatoryWide",
|
||
"Live_Terraces",
|
||
]
|
||
const WALK_WALLS: Array[String] = [
|
||
"Hull_Below", "Hull_Band", "Hull_Lintel",
|
||
"Yard_Wall", "Yard_StairRail", "Yard_Rails", "Yard_RampCurbs",
|
||
"Stair_A_Rails", "Stair_B_Rails",
|
||
"NAF_Blocks",
|
||
"Fusion_Wall", "Fusion_Pane", "Fusion_Frame", "Fusion_DoorFrames",
|
||
"Fusion_Control", "Fusion_Cryoplant", "Fusion_Turbine", "Fusion_Exchanger",
|
||
"Fusion_Columns", "Fusion_ShieldHigh", "Fusion_ShieldLow",
|
||
"Fusion_CryostatHigh", "Fusion_CryostatLow",
|
||
"School_Rooms", "School_Library", "School_Bulkhead",
|
||
"School_Colonnade", "Lecture_Pillars", "Lecture_Backs", "Lecture_Curb",
|
||
"Live_Wall",
|
||
"Market_Colonnade", "Market_Stalls",
|
||
"Core_",
|
||
"Dock_Hall", "Dock_Vestibule", "Dock_Frame", "Dock_Roof", "Dock_Rail",
|
||
"Well_Curb", "Well_Beam",
|
||
"Cap_Shell",
|
||
]
|
||
const WALK_SKIP: Array[String] = [
|
||
"Light", "LightRig_", "Elev_Cab_", "Exchange_Neon_",
|
||
"Stair_Lamps", "School_RoomDoors", "School_LibraryDoor",
|
||
"Well_Seal", "Well_Planet_", "Well_Beacon_",
|
||
"Marker_",
|
||
]
|
||
|
||
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
|
||
var moon_gravity: float = 0.0
|
||
var coyote_time: float = 0.0
|
||
|
||
var pistol_drawn: bool = false
|
||
var ads: bool = false
|
||
var pistol_ammo: int = 12
|
||
var pistol_reserve: int = 36
|
||
const PISTOL_MAG: int = 12
|
||
var fire_cd: float = 0.0
|
||
const FIRE_INTERVAL: float = 0.28
|
||
var interact_cd: float = 0.0
|
||
var jump_anim_cd: float = 0.0
|
||
var _crust_on: bool = false
|
||
var _moon_colliders: Array[PhysicsBody3D] = []
|
||
|
||
|
||
func _ensure_action(action: String, keycode: Key) -> void:
|
||
if not InputMap.has_action(action):
|
||
InputMap.add_action(action)
|
||
for ev in InputMap.action_get_events(action):
|
||
if ev is InputEventKey and (ev as InputEventKey).physical_keycode == keycode:
|
||
return
|
||
var evk := InputEventKey.new()
|
||
evk.physical_keycode = keycode
|
||
InputMap.action_add_event(action, evk)
|
||
|
||
|
||
func _ensure_default_actions() -> void:
|
||
_ensure_action("interact", KEY_F)
|
||
_ensure_action("mag_boots", KEY_C)
|
||
_ensure_action("toggle_mag_boots", KEY_C)
|
||
_ensure_action("toggle_pistol", KEY_G)
|
||
_ensure_action("reload", KEY_R)
|
||
_ensure_action("toggle_inventory", KEY_TAB)
|
||
_ensure_action("toggle_first_person", KEY_V)
|
||
_ensure_action("sprint", KEY_SHIFT)
|
||
_ensure_action("eva_roll_left", KEY_Q)
|
||
_ensure_action("eva_roll_right", KEY_E)
|
||
|
||
|
||
func _ready() -> void:
|
||
_ensure_default_actions()
|
||
scale = Vector3.ONE
|
||
safe_margin = 0.05
|
||
floor_snap_length = 0.45
|
||
floor_max_angle = deg_to_rad(70.0)
|
||
motion_mode = CharacterBody3D.MOTION_MODE_GROUNDED
|
||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||
moon_gravity = gravity / 6.0
|
||
mag_boots_on = true
|
||
print("Mag Boots: ON (spawn)")
|
||
_apply_camera_mode(true)
|
||
_set_moon_surface_visible(true)
|
||
if vendor_menu:
|
||
vendor_menu.visible = false
|
||
add_to_group("player")
|
||
FloatingOrigin.set_focus(self)
|
||
var solar := get_node_or_null("/root/Node3D/SolarSystem")
|
||
if solar:
|
||
FloatingOrigin.register_root(solar)
|
||
if not GameState.scramble_begun.is_connected(_on_scramble_begun):
|
||
GameState.scramble_begun.connect(_on_scramble_begun)
|
||
if Engine.has_meta("load_save"):
|
||
Engine.remove_meta("load_save")
|
||
var data := GameState.load_game_data()
|
||
if not data.is_empty():
|
||
player_name = str(data.get("player_name", player_name))
|
||
background_id = str(data.get("background_id", ""))
|
||
background_name = str(data.get("background_name", ""))
|
||
culture_id = str(data.get("culture_id", ""))
|
||
culture_name = str(data.get("culture_name", ""))
|
||
appearance = data.get("appearance", {})
|
||
money = int(data.get("money", money))
|
||
food = float(data.get("food", food))
|
||
health = float(data.get("health", health))
|
||
oxygen = float(data.get("oxygen", oxygen))
|
||
inventory.clear()
|
||
for item in data.get("inventory", []):
|
||
inventory.append(str(item))
|
||
GameState.apply_loaded_clock(data)
|
||
print("Loaded: ", player_name, " | credits=", money)
|
||
elif Engine.has_meta("player_creation_data"):
|
||
_apply_creation_data()
|
||
else:
|
||
print("Player ready – Young Arcology")
|
||
_rebuild_look()
|
||
_hide_legacy_soldier()
|
||
call_deferred("_snap_to_spawn")
|
||
call_deferred("_snap_to_spawn") # second pass after FO / habitat owner
|
||
call_deferred("_ensure_moon_shell")
|
||
call_deferred("_ensure_habitat_collision")
|
||
|
||
|
||
func _hide_legacy_soldier() -> void:
|
||
var legacy := get_node_or_null("Body/BlueSoldier_Male")
|
||
if legacy:
|
||
legacy.visible = false
|
||
legacy.process_mode = Node.PROCESS_MODE_DISABLED
|
||
|
||
|
||
func _apply_creation_data() -> void:
|
||
var data: Dictionary = Engine.get_meta("player_creation_data")
|
||
Engine.remove_meta("player_creation_data")
|
||
player_name = str(data.get("player_name", "Colonist"))
|
||
background_id = str(data.get("background_id", ""))
|
||
background_name = str(data.get("background_name", ""))
|
||
culture_id = str(data.get("culture_id", ""))
|
||
culture_name = str(data.get("culture_name", ""))
|
||
appearance = data.get("appearance", {})
|
||
var bonuses: Dictionary = data.get("bonuses", {})
|
||
if bonuses.has("money"):
|
||
money = int(bonuses["money"])
|
||
if bonuses.has("food"):
|
||
food = clamp(food + float(bonuses["food"]), 0.0, 100.0)
|
||
if bonuses.has("health"):
|
||
health = clamp(health + float(bonuses["health"]), 0.0, 100.0)
|
||
print("Colonist: ", player_name, " | ", culture_name, " | ", background_name, " | credits=", money, " | look=", appearance)
|
||
|
||
|
||
func _rebuild_look() -> void:
|
||
if look_loader and look_loader.has_method("apply_look") and not appearance.is_empty():
|
||
anim_player = look_loader.apply_look(appearance)
|
||
if anim_player == null:
|
||
anim_player = get_node_or_null("Body/BlueSoldier_Male/AnimationPlayer") as AnimationPlayer
|
||
_reset_doll_basis()
|
||
_apply_camera_mode(true)
|
||
|
||
|
||
func _play_move_anim(clip: String, speed_scale: float = 1.0) -> void:
|
||
if look_loader and look_loader.has_method("play_clip") and look_loader.get("anim") != null:
|
||
look_loader.play_clip(clip, speed_scale)
|
||
return
|
||
if anim_player == null:
|
||
return
|
||
anim_player.speed_scale = speed_scale
|
||
var names: Array[String] = ["CharacterArmature|%s" % clip, clip]
|
||
for n in names:
|
||
if anim_player.has_animation(n):
|
||
if anim_player.current_animation != n:
|
||
anim_player.play(n)
|
||
return
|
||
|
||
|
||
func _set_body_visible(v: bool) -> void:
|
||
if look_loader and look_loader.has_method("set_meshes_visible") and look_loader.get("current") != null:
|
||
look_loader.set_meshes_visible(v)
|
||
return
|
||
if has_node("Body"):
|
||
$Body.visible = v
|
||
|
||
|
||
func _update_fp_meshes() -> void:
|
||
# look_loader._apply_keep owns outfit slots. Only tuck the kept head in 1P.
|
||
var root: Node = look_loader if look_loader else get_node_or_null("Body")
|
||
if root == null:
|
||
return
|
||
_hide_head_only(root, first_person)
|
||
|
||
|
||
func _hide_head_only(n: Node, conceal_head: bool) -> void:
|
||
if n is MeshInstance3D:
|
||
var nm: String = n.name.to_lower()
|
||
if "head" in nm or "hair" in nm or "hat" in nm or "neck" in nm:
|
||
if conceal_head:
|
||
n.visible = false
|
||
# Do not force visible=true — that stacked every creator outfit.
|
||
for c in n.get_children():
|
||
_hide_head_only(c, conceal_head)
|
||
|
||
|
||
func _collect_moon_colliders() -> void:
|
||
_moon_colliders.clear()
|
||
var moon: Node = get_node_or_null("/root/Node3D/SolarSystem/Moon")
|
||
if moon == null:
|
||
moon = get_tree().root.find_child("Moon", true, false)
|
||
if moon == null:
|
||
return
|
||
var stack: Array[Node] = [moon]
|
||
while not stack.is_empty():
|
||
var n: Node = stack.pop_back()
|
||
if n.name == "LovellAnchor" or n.name == "Station" or n.name == "Habitat" or n.name == "ShellBody":
|
||
continue
|
||
if n is PhysicsBody3D and (n.name == "MoonBody" or String(n.name).begins_with("Moon")):
|
||
_moon_colliders.append(n as PhysicsBody3D)
|
||
for c in n.get_children():
|
||
stack.append(c)
|
||
print("Player crust: moon colliders=", _moon_colliders.size())
|
||
|
||
|
||
func _inside_young() -> bool:
|
||
# Hull r=200. Jetway ~216. Pad ~232. Keep MoonBody ignored on the whole station.
|
||
var habitat: Node3D = _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:
|
||
if _moon_colliders.is_empty():
|
||
_collect_moon_colliders()
|
||
# Always show the sphere. Hiding it was the "luna deleted" bug.
|
||
_set_moon_surface_visible(true)
|
||
# Ignore MoonBody only in rooms / while parked in the ship. Pads and mare collide.
|
||
var want: bool = in_vehicle or _inside_young()
|
||
if want == _crust_on:
|
||
return
|
||
_crust_on = want
|
||
for b in _moon_colliders:
|
||
if not is_instance_valid(b):
|
||
continue
|
||
if want:
|
||
add_collision_exception_with(b)
|
||
else:
|
||
remove_collision_exception_with(b)
|
||
print("Player crust exception ", "ON" if want else "OFF", " interior=", _inside_young())
|
||
|
||
|
||
func _set_moon_surface_visible(vis: bool) -> void:
|
||
var moon: Node3D = _moon()
|
||
if moon == null:
|
||
return
|
||
moon.set_meta("hide_crust", false)
|
||
for c in moon.get_children():
|
||
var nm: String = String(c.name)
|
||
if nm == "SurfaceHigh" or nm == "SurfaceLow":
|
||
(c as Node3D).visible = true
|
||
if not vis:
|
||
pass # kept so old callers compile; we do not hide luna anymore
|
||
|
||
|
||
func _snap_to_spawn() -> void:
|
||
var spawn: Node3D = _find_l5_spawn()
|
||
if spawn == null:
|
||
push_warning("SpawnPoint not found")
|
||
return
|
||
var hab: Node3D = _habitat()
|
||
var up: Vector3 = Vector3.UP
|
||
if hab:
|
||
up = hab.global_transform.basis.y.normalized()
|
||
else:
|
||
up = _surface_up()
|
||
# Marker is often at the well (local 0). Always stand on the L5 slab.
|
||
var deck := Vector3(48.0, 1.20, 18.0)
|
||
if hab:
|
||
global_position = hab.to_global(deck)
|
||
else:
|
||
global_position = spawn.global_position + up * 1.2
|
||
velocity = Vector3.ZERO
|
||
up_direction = up
|
||
_align_walk(up)
|
||
var local_p := Vector3.ZERO
|
||
if hab != null:
|
||
local_p = hab.to_local(global_position)
|
||
print("Spawned L5 hab=", hab, " marker=", str(spawn.get_path()), " pos=", global_position, " local=", local_p)
|
||
|
||
|
||
func _find_l5_spawn() -> Node3D:
|
||
# Name match only: L5Spawn. Ignore every SpawnPoint under L5_Civic / Terminal.
|
||
var station := get_parent()
|
||
if station:
|
||
for hab_name in ["Habitat", "Habitat_Old"]:
|
||
var hab := station.get_node_or_null(hab_name)
|
||
if hab == null:
|
||
continue
|
||
var official: Node = hab.get_node_or_null("L5Spawn")
|
||
if official is Node3D:
|
||
return official as Node3D
|
||
official = hab.find_child("L5Spawn", true, false)
|
||
if official is Node3D:
|
||
return official as Node3D
|
||
var nested: Node = get_tree().root.find_child("L5Spawn", true, false)
|
||
if nested is Node3D:
|
||
return nested as Node3D
|
||
return null
|
||
|
||
|
||
func set_in_vehicle(value: bool) -> void:
|
||
in_vehicle = value
|
||
if value:
|
||
pressurized = true
|
||
_set_moon_surface_visible(true)
|
||
_update_crust_exception()
|
||
|
||
|
||
func _is_aiming() -> bool:
|
||
return pistol_drawn and ads and not in_vehicle
|
||
|
||
|
||
func _update_waist_aim() -> void:
|
||
if look_loader == null:
|
||
return
|
||
# 3P gun-out AND 1P on foot: fold spine to camera pitch.
|
||
# EVA orients the whole body, so waist stays off there.
|
||
var fold: bool = not in_vehicle and not _is_eva() and (pistol_drawn or first_person)
|
||
if look_loader.has_method("set_aim_enabled"):
|
||
look_loader.set_aim_enabled(fold)
|
||
if look_loader.has_method("set_aim_pitch"):
|
||
var pitch: float = 0.0
|
||
if fold and camera_pivot:
|
||
pitch = camera_pivot.rotation.x
|
||
look_loader.set_aim_pitch(pitch)
|
||
|
||
|
||
func _process(delta: float) -> void:
|
||
if first_person:
|
||
_cam_pos_target = _fp_cam_pos()
|
||
_update_fp_meshes()
|
||
_update_waist_aim()
|
||
if look_loader and look_loader.has_method("set_fp_ride"):
|
||
look_loader.set_fp_ride(camera, camera_pivot, first_person and not in_vehicle)
|
||
_smooth_camera(delta)
|
||
if in_vehicle:
|
||
_process_needs(delta)
|
||
_update_hud()
|
||
|
||
|
||
func _smooth_camera(delta: float) -> void:
|
||
if camera == null or camera_pivot == null:
|
||
return
|
||
if first_person and not free_looking:
|
||
camera_pivot.rotation.y = lerp_angle(camera_pivot.rotation.y, 0.0, clampf(14.0 * delta, 0.0, 1.0))
|
||
elif not first_person and not free_looking:
|
||
free_look_yaw = lerp_angle(free_look_yaw, 0.0, clampf(14.0 * delta, 0.0, 1.0))
|
||
free_look_pitch = lerp_angle(free_look_pitch, 0.0, clampf(14.0 * delta, 0.0, 1.0))
|
||
if abs(free_look_yaw) < 0.002:
|
||
free_look_yaw = 0.0
|
||
if abs(free_look_pitch) < 0.002:
|
||
free_look_pitch = 0.0
|
||
camera_pivot.rotation.y = free_look_yaw
|
||
var t: float = clampf(10.0 * delta, 0.0, 1.0)
|
||
camera.position = camera.position.lerp(_cam_pos_target, t)
|
||
camera.rotation.x = lerp_angle(camera.rotation.x, _cam_rot_target.x, t)
|
||
camera.rotation.y = lerp_angle(camera.rotation.y, _cam_rot_target.y, t)
|
||
camera.rotation.z = lerp_angle(camera.rotation.z, _cam_rot_target.z, t)
|
||
if _fov_kick > 0.0:
|
||
_fov_kick = maxf(_fov_kick - 18.0 * delta, 0.0)
|
||
camera.fov = lerpf(camera.fov, _fov_target - _fov_kick, t)
|
||
|
||
|
||
func _on_scramble_begun() -> void:
|
||
print("ALARM: People's Privateer Fleet on approach to Young")
|
||
|
||
|
||
func restore_from_vehicle() -> void:
|
||
set_physics_process(true)
|
||
set_process(true)
|
||
set_process_input(true)
|
||
set_process_unhandled_input(true)
|
||
process_mode = Node.PROCESS_MODE_INHERIT
|
||
visible = true
|
||
in_vehicle = false
|
||
var height: float = _height_above_crust()
|
||
if height < 12.0:
|
||
mag_boots_on = true
|
||
pressurized = true
|
||
_align_walk(_walk_up())
|
||
else:
|
||
mag_boots_on = false
|
||
pressurized = false
|
||
eva_peak_height = max(height, EVA_SNAP_PEAK)
|
||
_reset_camera_to_walk()
|
||
_align_walk(_walk_up())
|
||
var col = get_node_or_null("CollisionShape3D")
|
||
if col:
|
||
col.disabled = false
|
||
if camera:
|
||
camera.near = 0.05
|
||
camera.far = 1000000.0
|
||
camera.current = true
|
||
camera.make_current()
|
||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||
FloatingOrigin.set_focus(self)
|
||
|
||
|
||
func _ensure_moon_shell() -> void:
|
||
var moon := _moon()
|
||
if moon == null:
|
||
print("PlanetShell FAIL: Moon node missing")
|
||
return
|
||
if moon.has_method("build"):
|
||
moon.call("build")
|
||
print("PlanetShell: asked Moon.build()")
|
||
return
|
||
var high := moon.get_node_or_null("SurfaceHigh") as MeshInstance3D
|
||
var radius: float = 173700.0
|
||
if high and high.mesh is SphereMesh:
|
||
var sm := high.mesh as SphereMesh
|
||
radius = sm.radius
|
||
if sm.height > radius * 1.5:
|
||
radius = sm.height * 0.5
|
||
var old := moon.get_node_or_null("MoonBody") as CollisionObject3D
|
||
if old:
|
||
old.collision_layer = 1
|
||
old.collision_mask = 1
|
||
for c in old.get_children():
|
||
if c is CollisionShape3D:
|
||
(c as CollisionShape3D).disabled = false
|
||
var anchor := moon.get_node_or_null("LovellAnchor") as Node3D
|
||
var hole_dir := Vector3.ZERO
|
||
var hole_cos := 1.0
|
||
if anchor:
|
||
var to_a: Vector3 = anchor.global_position - moon.global_position
|
||
if to_a.length() > 1.0:
|
||
hole_dir = (moon.global_transform.basis.inverse() * to_a.normalized()).normalized()
|
||
hole_cos = cos(atan(205.0 / radius))
|
||
var faces := PackedVector3Array()
|
||
var segs := 40
|
||
var rgs := 24
|
||
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 := sin(t0)
|
||
var y1 := sin(t1)
|
||
var r0 := cos(t0)
|
||
var r1 := 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
|
||
for pair in [[p00, p01, p11], [p00, p11, p10]]:
|
||
var a: Vector3 = pair[0]
|
||
var b: Vector3 = pair[1]
|
||
var c: Vector3 = pair[2]
|
||
var mid: Vector3 = (a + b + c) / 3.0
|
||
if hole_dir != Vector3.ZERO and mid.length() > 0.001 and mid.normalized().dot(hole_dir) >= hole_cos:
|
||
continue
|
||
faces.append(a)
|
||
faces.append(b)
|
||
faces.append(c)
|
||
faces.append(a)
|
||
faces.append(c)
|
||
faces.append(b)
|
||
var body := moon.get_node_or_null("ShellBody") as StaticBody3D
|
||
if body == null:
|
||
body = StaticBody3D.new()
|
||
body.name = "ShellBody"
|
||
moon.add_child(body)
|
||
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)
|
||
var shape := ConcavePolygonShape3D.new()
|
||
shape.set_faces(faces)
|
||
col.shape = shape
|
||
col.disabled = false
|
||
print("PlanetShell FALLBACK radius=", radius, " faces=", faces.size() / 3, " hole=", hole_dir != Vector3.ZERO)
|
||
|
||
|
||
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:
|
||
var paths := [
|
||
NodePath("../Habitat"),
|
||
NodePath("../../Habitat"),
|
||
NodePath("/root/Node3D/SolarSystem/Moon/LovellAnchor/Station/Habitat"),
|
||
NodePath("/root/Node3D/SolarSystem/Moon/LovellAnchor/Habitat"),
|
||
]
|
||
for p in paths:
|
||
var h := get_node_or_null(p) as Node3D
|
||
if h:
|
||
return h
|
||
var any: Node = get_tree().root.find_child("Habitat", true, false)
|
||
if any is Node3D:
|
||
return any as Node3D
|
||
return null
|
||
|
||
|
||
func _soi_gravity() -> Vector3:
|
||
var moon := _moon()
|
||
if moon:
|
||
var to_m: Vector3 = moon.global_position - global_position
|
||
var dm: float = to_m.length()
|
||
if dm > 1.0 and dm < MOON_SOI:
|
||
return to_m.normalized() * MOON_G
|
||
var earth := _earth()
|
||
if earth:
|
||
var to_e: Vector3 = earth.global_position - global_position
|
||
var de: float = to_e.length()
|
||
if de > 1.0 and de < EARTH_SOI:
|
||
return to_e.normalized() * EARTH_G
|
||
return Vector3.ZERO
|
||
|
||
|
||
func _surface_up() -> Vector3:
|
||
var gvec: Vector3 = _soi_gravity()
|
||
if gvec.length() > 0.01:
|
||
return -gvec.normalized()
|
||
return Vector3.UP
|
||
|
||
|
||
func _walk_up() -> Vector3:
|
||
# Inside Young: decks are flat in Habitat space. Outside: radial from the body.
|
||
if _inside_young():
|
||
var hab := _habitat()
|
||
if hab:
|
||
return hab.global_transform.basis.y.normalized()
|
||
return _surface_up()
|
||
|
||
|
||
func _height_above_crust() -> float:
|
||
var moon := _moon()
|
||
if moon == null:
|
||
return 9999.0
|
||
return global_position.distance_to(moon.global_position) - MOON_RADIUS
|
||
|
||
|
||
func _reset_camera_to_walk() -> void:
|
||
free_looking = false
|
||
free_look_yaw = 0.0
|
||
free_look_pitch = 0.0
|
||
if camera_pivot:
|
||
camera_pivot.rotation = Vector3.ZERO
|
||
_reset_doll_basis()
|
||
_apply_camera_mode(false)
|
||
|
||
|
||
func _try_snap_boots() -> void:
|
||
if mag_boots_on or in_vehicle:
|
||
return
|
||
if not is_on_floor():
|
||
eva_peak_height = max(eva_peak_height, _height_above_crust())
|
||
return
|
||
if eva_peak_height < EVA_SNAP_PEAK:
|
||
eva_peak_height = 0.0
|
||
return
|
||
eva_peak_height = 0.0
|
||
mag_boots_on = true
|
||
pressurized = true
|
||
_snap_upright(_walk_up())
|
||
velocity.x = 0.0
|
||
velocity.z = 0.0
|
||
_reset_camera_to_walk()
|
||
print("Mag Boots: ON (landed)")
|
||
|
||
|
||
func _snap_upright(up: Vector3) -> void:
|
||
_align_walk(up)
|
||
|
||
|
||
func _is_eva() -> bool:
|
||
return (not mag_boots_on) and (not is_on_floor())
|
||
|
||
|
||
func _reset_doll_basis() -> void:
|
||
if look_loader == null:
|
||
return
|
||
look_loader.rotation = Vector3(0.0, PI, 0.0)
|
||
look_loader.position = Vector3.ZERO
|
||
look_loader.scale = Vector3.ONE
|
||
var doll = look_loader.get("current")
|
||
if doll is Node3D and is_instance_valid(doll):
|
||
doll.position = Vector3.ZERO
|
||
doll.rotation = Vector3.ZERO
|
||
|
||
|
||
func _align_walk(up: Vector3) -> void:
|
||
if up.length_squared() < 0.0001:
|
||
return
|
||
up = up.normalized()
|
||
var fwd: Vector3 = -global_transform.basis.z.slide(up)
|
||
if fwd.length_squared() < 0.0001:
|
||
fwd = global_transform.basis.x.slide(up)
|
||
if fwd.length_squared() < 0.0001:
|
||
fwd = Vector3.FORWARD.slide(up)
|
||
if fwd.length_squared() < 0.0001:
|
||
return
|
||
fwd = fwd.normalized()
|
||
var right: Vector3 = fwd.cross(up)
|
||
if right.length_squared() < 0.0001:
|
||
return
|
||
right = right.normalized()
|
||
fwd = up.cross(right).normalized()
|
||
# X = right, Y = up, -Z = forward
|
||
global_transform.basis = Basis(right, up, -fwd).orthonormalized()
|
||
|
||
|
||
func _keep_walk_upright() -> void:
|
||
if _is_eva() or in_vehicle:
|
||
return
|
||
# Align whenever we have a floor, boots or not — otherwise the highlands dump you.
|
||
if not is_on_floor() and not mag_boots_on:
|
||
return
|
||
_align_walk(_walk_up())
|
||
if camera_pivot:
|
||
camera_pivot.rotation.z = 0.0
|
||
if look_loader:
|
||
look_loader.rotation = Vector3(0.0, PI, 0.0)
|
||
look_loader.position = Vector3.ZERO
|
||
look_loader.scale = Vector3.ONE
|
||
|
||
|
||
func _aim_visual_to_look() -> void:
|
||
_reset_doll_basis()
|
||
|
||
|
||
func _set_alt_look(on: bool) -> void:
|
||
if on and camera_pivot:
|
||
free_look_yaw = camera_pivot.rotation.y
|
||
free_look_pitch = camera_pivot.rotation.x
|
||
free_looking = on
|
||
if camera and camera_pivot:
|
||
camera.rotation.z = 0.0
|
||
camera_pivot.rotation.z = 0.0
|
||
|
||
|
||
func _unhandled_input(event: InputEvent) -> void:
|
||
if in_vehicle:
|
||
return
|
||
if vendor_menu and vendor_menu.visible:
|
||
if event.is_action_pressed("ui_cancel"):
|
||
close_vendor_menu()
|
||
return
|
||
if event is InputEventKey and event.keycode == KEY_QUOTELEFT:
|
||
if event.pressed:
|
||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||
else:
|
||
if not get_tree().paused and not (vendor_menu and vendor_menu.visible):
|
||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||
return
|
||
if event is InputEventKey and event.keycode == KEY_ALT:
|
||
_set_alt_look(event.pressed)
|
||
return
|
||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
|
||
if pistol_drawn:
|
||
ads = event.pressed
|
||
_apply_camera_mode(false)
|
||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||
if pistol_drawn:
|
||
_try_fire()
|
||
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||
if _is_eva():
|
||
# Whole-body look in 1P and 3P EVA. World-Y yaw felt broken after a roll.
|
||
rotate_object_local(Vector3.UP, -event.relative.x * mouse_sensitivity)
|
||
rotate_object_local(Vector3.RIGHT, -event.relative.y * mouse_sensitivity)
|
||
if camera_pivot:
|
||
camera_pivot.rotation = Vector3.ZERO
|
||
elif first_person:
|
||
if free_looking:
|
||
# Euler SET — rotate_x+rotate_y on a pitched pivot was the warp.
|
||
free_look_yaw -= event.relative.x * mouse_sensitivity
|
||
free_look_pitch -= event.relative.y * mouse_sensitivity
|
||
free_look_pitch = clamp(free_look_pitch, deg_to_rad(-78), deg_to_rad(62))
|
||
free_look_yaw = clamp(free_look_yaw, deg_to_rad(-75), deg_to_rad(75))
|
||
camera_pivot.rotation = Vector3(free_look_pitch, free_look_yaw, 0.0)
|
||
else:
|
||
rotate_y(-event.relative.x * mouse_sensitivity)
|
||
camera_pivot.rotate_x(-event.relative.y * mouse_sensitivity)
|
||
camera_pivot.rotation.x = clamp(camera_pivot.rotation.x, deg_to_rad(-78), deg_to_rad(85))
|
||
elif free_looking:
|
||
free_look_yaw -= event.relative.x * free_look_sensitivity
|
||
free_look_pitch -= event.relative.y * free_look_sensitivity
|
||
free_look_pitch = clamp(free_look_pitch, deg_to_rad(-80), deg_to_rad(80))
|
||
if camera_pivot:
|
||
camera_pivot.rotation.y = free_look_yaw
|
||
camera_pivot.rotation.x = free_look_pitch
|
||
else:
|
||
rotate_y(-event.relative.x * mouse_sensitivity)
|
||
camera_pivot.rotate_x(-event.relative.y * mouse_sensitivity)
|
||
camera_pivot.rotation.x = clamp(camera_pivot.rotation.x, deg_to_rad(-78), deg_to_rad(55))
|
||
if event is InputEventKey and event.pressed and not event.echo:
|
||
match event.keycode:
|
||
KEY_1:
|
||
_toggle_pistol()
|
||
KEY_F3:
|
||
_debug_fp_eye()
|
||
KEY_F4:
|
||
if look_loader and look_loader.has_method("cycle_arm_axis"):
|
||
look_loader.cycle_arm_axis()
|
||
if event.is_action_pressed("interact"):
|
||
if can_interact and current_interactable:
|
||
_try_interact()
|
||
if event.is_action_pressed("mag_boots"):
|
||
mag_boots_on = not mag_boots_on
|
||
if mag_boots_on:
|
||
eva_peak_height = 0.0
|
||
velocity = velocity.slide(_walk_up()) * 0.35
|
||
_align_walk(_walk_up())
|
||
_reset_camera_to_walk()
|
||
print("Mag Boots: ", "ON" if mag_boots_on else "OFF")
|
||
if event.is_action_pressed("toggle_pistol"):
|
||
_toggle_pistol()
|
||
if event.is_action_pressed("reload"):
|
||
_reload_pistol()
|
||
if event.is_action_pressed("toggle_inventory"):
|
||
inventory_open = not inventory_open
|
||
if event.is_action_pressed("toggle_first_person"):
|
||
if in_vehicle:
|
||
return
|
||
first_person = not first_person
|
||
_set_alt_look(false)
|
||
_apply_camera_mode(false)
|
||
if event.is_action_pressed("ui_page_up"):
|
||
food = min(food + 25.0, 100.0)
|
||
if event.is_action_pressed("ui_page_down"):
|
||
money += 50
|
||
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F5:
|
||
GameState.save_game(self)
|
||
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F4:
|
||
if not GameState.scramble_started:
|
||
GameState.scramble_started = true
|
||
GameState.scramble_begun.emit()
|
||
else:
|
||
var banner := get_node_or_null("/root/Node3D/HUD/ScrambleBanner")
|
||
if banner and banner.has_method("hide_banner"):
|
||
banner.hide_banner()
|
||
|
||
|
||
func _physics_process(delta: float) -> void:
|
||
_update_crust_exception()
|
||
if fire_cd > 0.0:
|
||
fire_cd = max(fire_cd - delta, 0.0)
|
||
if interact_cd > 0.0:
|
||
interact_cd = max(interact_cd - delta, 0.0)
|
||
if jump_anim_cd > 0.0:
|
||
jump_anim_cd = max(jump_anim_cd - delta, 0.0)
|
||
if in_vehicle:
|
||
return
|
||
if vendor_menu and vendor_menu.visible:
|
||
velocity = Vector3.ZERO
|
||
return
|
||
if is_on_floor():
|
||
coyote_time = 0.14
|
||
else:
|
||
coyote_time = max(coyote_time - delta, 0.0)
|
||
_try_snap_boots()
|
||
var gvec: Vector3 = _soi_gravity()
|
||
if _is_eva():
|
||
_set_body_visible(true)
|
||
if Input.is_action_pressed("eva_roll_left") or Input.is_key_pressed(KEY_Q):
|
||
rotate_object_local(Vector3.FORWARD, -2.2 * delta)
|
||
if Input.is_action_pressed("eva_roll_right") or Input.is_key_pressed(KEY_E):
|
||
rotate_object_local(Vector3.FORWARD, 2.2 * delta)
|
||
if gvec.length() > 0.01:
|
||
velocity += gvec * delta
|
||
var input_dir_eva: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
|
||
if input_dir_eva.length() > 0.1:
|
||
var thrust: Vector3 = (
|
||
-transform.basis.z * -input_dir_eva.y + transform.basis.x * input_dir_eva.x
|
||
).normalized() * speed * 0.35
|
||
velocity += thrust * delta
|
||
move_and_slide()
|
||
_process_needs(delta)
|
||
_check_interactable()
|
||
_update_animations()
|
||
_update_hud()
|
||
return
|
||
var up: Vector3 = _walk_up()
|
||
up_direction = up
|
||
floor_max_angle = deg_to_rad(70.0)
|
||
var g_amt: float = gvec.length()
|
||
if g_amt < 0.01:
|
||
g_amt = moon_gravity if moon_gravity > 0.0 else MOON_G
|
||
# Boots on = snap-down. Boots off = 1/6 g.
|
||
if mag_boots_on:
|
||
g_amt = maxf(g_amt, MOON_G) * 6.0
|
||
else:
|
||
g_amt = maxf(g_amt, MOON_G)
|
||
velocity += -up * g_amt * delta
|
||
var input_dir: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
|
||
var direction: Vector3 = (transform.basis * Vector3(input_dir.x, 0.0, input_dir.y)).slide(up)
|
||
if direction.length() > 0.001:
|
||
direction = direction.normalized()
|
||
else:
|
||
direction = Vector3.ZERO
|
||
var want_sprint: bool = Input.is_action_pressed("sprint") and input_dir.y < -0.12
|
||
var current_speed: float = sprint_speed if want_sprint else speed
|
||
if ads and pistol_drawn:
|
||
current_speed *= ads_sprint_scale if want_sprint else ads_walk_scale
|
||
var v_up: float = velocity.dot(up)
|
||
var v_h: Vector3 = velocity.slide(up)
|
||
if is_on_floor():
|
||
if direction:
|
||
v_h = v_h.move_toward(direction * current_speed, move_accel * delta)
|
||
else:
|
||
v_h = v_h.move_toward(Vector3.ZERO, move_decel * delta)
|
||
if mag_boots_on and v_up < 0.0:
|
||
v_up = -0.12
|
||
else:
|
||
var air_control: float = 0.08 if not mag_boots_on else 0.02
|
||
if direction:
|
||
v_h = v_h.move_toward(direction * current_speed, current_speed * air_control)
|
||
if mag_boots_on:
|
||
v_h = v_h.move_toward(Vector3.ZERO, 1.2 * delta)
|
||
velocity = v_h + up * v_up
|
||
# Jump LAST so floor rebuild cannot eat or replace the impulse.
|
||
var can_jump: bool = is_on_floor() or coyote_time > 0.0
|
||
if Input.is_action_just_pressed("ui_accept") and can_jump:
|
||
coyote_time = 0.0
|
||
jump_anim_cd = 0.45
|
||
var jv: float = 1.7 if mag_boots_on else 6.4
|
||
velocity = velocity.slide(up) + up * jv
|
||
print("JUMP boots=", mag_boots_on, " jv=", jv, " g=", g_amt, " floor=", is_on_floor())
|
||
if is_on_floor() and not Input.is_action_just_pressed("ui_accept"):
|
||
_try_step(up)
|
||
move_and_slide()
|
||
_keep_walk_upright()
|
||
_process_needs(delta)
|
||
_check_interactable()
|
||
_update_animations()
|
||
_update_hud()
|
||
|
||
|
||
func _look_down_amt() -> float:
|
||
if camera_pivot == null:
|
||
return 0.0
|
||
return clampf(-camera_pivot.rotation.x / deg_to_rad(78.0), 0.0, 1.0)
|
||
|
||
|
||
func _fp_cam_pos() -> Vector3:
|
||
# Same seat as gun-out hip. Unarmed sits a few cm back so the chest
|
||
# fills the hole the pistol otherwise occupies. G should barely jump.
|
||
if ads and pistol_drawn:
|
||
return Vector3(0.05, 0.05, -0.06)
|
||
return Vector3(0.03, 0.04, -0.04)
|
||
|
||
|
||
func _apply_camera_mode(snap: bool = false) -> void:
|
||
if first_person:
|
||
_cam_pos_target = _fp_cam_pos()
|
||
_cam_rot_target = Vector3.ZERO
|
||
_fov_target = 62.0 if (ads and pistol_drawn) else 72.0
|
||
if camera_pivot:
|
||
# 3P look-down must not come with us or 1P stares at the hoodie.
|
||
camera_pivot.rotation.x = clampf(camera_pivot.rotation.x, deg_to_rad(-18.0), deg_to_rad(20.0))
|
||
camera_pivot.rotation.z = 0.0
|
||
if look_loader and look_loader.has_method("set_first_person"):
|
||
look_loader.set_first_person(true)
|
||
_update_fp_meshes()
|
||
else:
|
||
camera_pivot.position = Vector3(0, 2.40, 0)
|
||
if ads and pistol_drawn:
|
||
_cam_pos_target = Vector3(0.62, 0.22, 2.15)
|
||
_cam_rot_target = Vector3(deg_to_rad(-6.0), 0.0, 0.0)
|
||
_fov_target = 58.0
|
||
else:
|
||
_cam_pos_target = Vector3(0.58, 0.38, 3.20)
|
||
_cam_rot_target = Vector3(deg_to_rad(-8.0), 0.0, 0.0)
|
||
_fov_target = 70.0
|
||
if look_loader and look_loader.has_method("set_first_person"):
|
||
look_loader.set_first_person(false)
|
||
_update_fp_meshes()
|
||
_set_body_visible(true)
|
||
if camera:
|
||
camera.near = 0.08 if first_person else 0.06
|
||
camera.far = 1000000.0
|
||
if snap:
|
||
camera.position = _cam_pos_target
|
||
camera.rotation = _cam_rot_target
|
||
camera.fov = _fov_target
|
||
if not in_vehicle:
|
||
camera.current = true
|
||
|
||
|
||
func _process_needs(delta: float) -> void:
|
||
if food > 0.0:
|
||
food -= food_decay_rate * delta
|
||
food = max(food, 0.0)
|
||
else:
|
||
health -= starve_damage_rate * delta
|
||
if not pressurized and not in_vehicle:
|
||
if oxygen > 0.0:
|
||
oxygen -= oxygen_decay_rate * delta
|
||
oxygen = max(oxygen, 0.0)
|
||
else:
|
||
health -= low_oxygen_damage_rate * delta
|
||
health = clamp(health, 0.0, 100.0)
|
||
|
||
|
||
func _query_interactable(origin: Vector3, end: Vector3) -> Dictionary:
|
||
var space = get_world_3d().direct_space_state
|
||
var query = PhysicsRayQueryParameters3D.create(origin, end)
|
||
query.collide_with_areas = true
|
||
query.collide_with_bodies = true
|
||
query.exclude = [get_rid()]
|
||
return space.intersect_ray(query)
|
||
|
||
|
||
func _check_interactable() -> void:
|
||
can_interact = false
|
||
current_interactable = null
|
||
if camera == null:
|
||
return
|
||
var hits: Array[Dictionary] = []
|
||
var cam_origin := camera.global_position
|
||
var cam_end := cam_origin + (-camera.global_transform.basis.z * interact_distance)
|
||
hits.append(_query_interactable(cam_origin, cam_end))
|
||
var chest := global_position + Vector3(0, 1.1, 0)
|
||
var look := -global_transform.basis.z
|
||
hits.append(_query_interactable(chest, chest + look * 3.5 + Vector3(0, -1.4, 0)))
|
||
hits.append(_query_interactable(chest, chest + Vector3(0, -2.2, 0)))
|
||
var feet := global_position + Vector3(0, 0.35, 0)
|
||
hits.append(_query_interactable(feet, feet + look * 2.4))
|
||
for result in hits:
|
||
if result and result.collider and result.collider.is_in_group("interactable"):
|
||
can_interact = true
|
||
current_interactable = result.collider
|
||
return
|
||
|
||
|
||
func _try_interact() -> void:
|
||
if current_interactable and current_interactable.has_method("on_interact"):
|
||
current_interactable.on_interact(self)
|
||
interact_cd = 0.55
|
||
_play_move_anim("Interact")
|
||
|
||
|
||
func open_vendor_menu() -> void:
|
||
if vendor_menu and vendor_menu.has_method("open_menu"):
|
||
vendor_menu.open_menu(self)
|
||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||
|
||
|
||
func close_vendor_menu() -> void:
|
||
if vendor_menu:
|
||
vendor_menu.visible = false
|
||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||
|
||
|
||
func _update_animations() -> void:
|
||
var dt: float = get_physics_process_delta_time()
|
||
if interact_cd > 0.0:
|
||
return
|
||
if jump_anim_cd > 0.0:
|
||
_play_move_anim("Jump")
|
||
return
|
||
if _is_eva() or (not mag_boots_on and not is_on_floor()):
|
||
if pistol_drawn and ads:
|
||
_play_move_anim("AimIdle")
|
||
elif pistol_drawn:
|
||
_play_move_anim("Idle_Gun")
|
||
else:
|
||
_play_move_anim("Idle")
|
||
return
|
||
if not is_on_floor():
|
||
_play_move_anim("Idle")
|
||
return
|
||
# Stick, not velocity — accel must not pick the clip.
|
||
var stick: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
|
||
var strafe: float = clampf(stick.x, -1.0, 1.0)
|
||
var back: float = clampf(stick.y, -1.0, 1.0)
|
||
var want_sprint: bool = Input.is_action_pressed("sprint") and stick.y < -0.12
|
||
var run_amt: float = 1.0 if want_sprint else 0.0
|
||
if look_loader and look_loader.has_method("set_loco"):
|
||
look_loader.set_loco(strafe, back, run_amt, dt, _is_aiming())
|
||
return
|
||
if _is_aiming():
|
||
_play_move_anim("AimIdle")
|
||
elif pistol_drawn:
|
||
_play_move_anim("Idle_Gun")
|
||
else:
|
||
_play_move_anim("Idle")
|
||
|
||
|
||
func _update_hud() -> void:
|
||
if needs_label:
|
||
var interact_text: String = " [F] Use" if can_interact else ""
|
||
var gun_text: String = ""
|
||
if pistol_drawn:
|
||
gun_text = " | PISTOL %d/%d%s" % [pistol_ammo, pistol_reserve, " ADS" if ads else ""]
|
||
var inv_text: String = ""
|
||
if inventory_open:
|
||
if inventory.size() == 0:
|
||
inv_text = "\nInventory: (empty)"
|
||
else:
|
||
inv_text = "\nInventory: " + ", ".join(inventory)
|
||
var boot_text: String = " | BOOTS ON" if mag_boots_on else " | BOOTS OFF"
|
||
needs_label.text = "%s | %s | Food: %.0f | Health: %.0f | O₂: %.0f | Credits: %d%s%s%s%s" % [
|
||
player_name, GameState.get_time_string(),
|
||
food, health, oxygen, money, boot_text, interact_text, gun_text, inv_text
|
||
]
|
||
|
||
|
||
func _toggle_pistol() -> void:
|
||
pistol_drawn = not pistol_drawn
|
||
if not pistol_drawn:
|
||
ads = false
|
||
if look_loader and look_loader.has_method("set_pistol_drawn"):
|
||
look_loader.set_pistol_drawn(pistol_drawn)
|
||
_apply_camera_mode(false)
|
||
if pistol_drawn:
|
||
_play_move_anim("Idle_Gun")
|
||
else:
|
||
_play_move_anim("Idle")
|
||
print("Pistol: ", "DRAWN" if pistol_drawn else "HOLSTERED")
|
||
|
||
|
||
func _reload_pistol() -> void:
|
||
if not pistol_drawn:
|
||
return
|
||
var need: int = PISTOL_MAG - pistol_ammo
|
||
if need <= 0 or pistol_reserve <= 0:
|
||
return
|
||
var take: int = mini(need, pistol_reserve)
|
||
pistol_reserve -= take
|
||
pistol_ammo += take
|
||
interact_cd = 0.45
|
||
_play_move_anim("Interact")
|
||
print("Reload ", pistol_ammo, "/", pistol_reserve)
|
||
|
||
|
||
func _try_fire() -> void:
|
||
if fire_cd > 0.0 or interact_cd > 0.0 or in_vehicle:
|
||
return
|
||
if pistol_ammo <= 0:
|
||
_reload_pistol()
|
||
return
|
||
pistol_ammo -= 1
|
||
fire_cd = FIRE_INTERVAL
|
||
if look_loader and look_loader.has_method("play_fire"):
|
||
look_loader.play_fire(ads)
|
||
else:
|
||
_play_move_anim("Idle_Gun_Shoot" if ads else "Gun_Shoot")
|
||
_fov_kick = 3.2 if ads else 2.2
|
||
if camera == null:
|
||
return
|
||
var space := get_world_3d().direct_space_state
|
||
var origin := camera.global_position
|
||
var end := origin + (-camera.global_transform.basis.z * 80.0)
|
||
var query := PhysicsRayQueryParameters3D.create(origin, end)
|
||
query.exclude = [get_rid()]
|
||
var hit := space.intersect_ray(query)
|
||
if hit:
|
||
print("Shot ", hit.collider.name, " @ ", hit.position)
|
||
else:
|
||
print("Shot (miss)")
|
||
|
||
|
||
func add_food(amount: float) -> void:
|
||
food = clamp(food + amount, 0.0, 100.0)
|
||
|
||
|
||
func add_health(amount: float) -> void:
|
||
health = clamp(health + amount, 0.0, 100.0)
|
||
|
||
|
||
func add_oxygen(amount: float) -> void:
|
||
oxygen = clamp(oxygen + amount, 0.0, 100.0)
|
||
|
||
|
||
func _debug_fp_eye() -> void:
|
||
var head := Vector3.INF
|
||
if look_loader and look_loader.has_method("get_head_global_pos"):
|
||
head = look_loader.get_head_global_pos()
|
||
print("FP-EYE fp=", first_person,
|
||
" pitch=", snappedf(rad_to_deg(camera_pivot.rotation.x), 0.1) if camera_pivot else 0.0,
|
||
" pivot=", camera_pivot.global_position if camera_pivot else Vector3.ZERO,
|
||
" cam=", camera.global_position if camera else Vector3.ZERO,
|
||
" head=", head,
|
||
" local=", camera.position if camera else Vector3.ZERO)
|
||
|
||
|
||
func add_money(amount: int) -> void:
|
||
money += amount
|
||
|
||
|
||
func add_item(item_name: String) -> void:
|
||
inventory.append(item_name)
|
||
print("Picked up: ", item_name)
|
||
|
||
|
||
func remove_item(item_name: String) -> bool:
|
||
var index: int = inventory.find(item_name)
|
||
if index != -1:
|
||
inventory.remove_at(index)
|
||
return true
|
||
return false
|
||
|
||
|
||
func _ensure_habitat_collision() -> void:
|
||
var hab := _habitat()
|
||
if hab == null:
|
||
push_warning("Habitat collision: Habitat node not found")
|
||
return
|
||
var made := _add_walk_collision(hab)
|
||
print("Habitat collision shapes=", made)
|
||
|
||
|
||
func _add_walk_collision(n: Node) -> int:
|
||
var made := 0
|
||
for c in n.get_children():
|
||
made += _add_walk_collision(c)
|
||
if n is MeshInstance3D and _walkable_mesh(n.name) and (n as MeshInstance3D).mesh:
|
||
var mesh := n as MeshInstance3D
|
||
var body: StaticBody3D = null
|
||
for c in mesh.get_children():
|
||
if c is StaticBody3D:
|
||
body = c as StaticBody3D
|
||
break
|
||
if body == null:
|
||
mesh.create_trimesh_collision()
|
||
for c in mesh.get_children():
|
||
if c is StaticBody3D:
|
||
body = c as StaticBody3D
|
||
break
|
||
if body == null:
|
||
return made
|
||
body.collision_layer = 1
|
||
body.collision_mask = 0
|
||
for s in body.get_children():
|
||
if s is CollisionShape3D and (s as CollisionShape3D).shape is ConcavePolygonShape3D:
|
||
((s as CollisionShape3D).shape as ConcavePolygonShape3D).backface_collision = true
|
||
made += 1
|
||
return made
|
||
|
||
|
||
func _walkable_mesh(mesh_name: String) -> bool:
|
||
for s in WALK_SKIP:
|
||
if mesh_name.begins_with(s):
|
||
return false
|
||
for prefix in WALK_FLOORS:
|
||
if mesh_name.begins_with(prefix):
|
||
return true
|
||
for prefix in WALK_WALLS:
|
||
if mesh_name.begins_with(prefix):
|
||
return true
|
||
return false
|
||
|
||
|
||
func _feet_pos(up: Vector3) -> Vector3:
|
||
var col := get_node_or_null("CollisionShape3D") as CollisionShape3D
|
||
if col == null or col.shape == null:
|
||
return global_position
|
||
var half := 0.9
|
||
var shape := col.shape
|
||
if shape is CapsuleShape3D:
|
||
half = (shape as CapsuleShape3D).height * 0.5
|
||
elif shape is CylinderShape3D:
|
||
half = (shape as CylinderShape3D).height * 0.5
|
||
elif shape is BoxShape3D:
|
||
half = (shape as BoxShape3D).size.y * 0.5
|
||
return col.global_position - up * half
|
||
|
||
|
||
func _try_step(up: Vector3) -> void:
|
||
var dir := velocity.slide(up)
|
||
if dir.length() < 0.15:
|
||
return
|
||
dir = dir.normalized()
|
||
var feet := _feet_pos(up)
|
||
var space := get_world_3d().direct_space_state
|
||
var low_from := feet + up * 0.08
|
||
var low := PhysicsRayQueryParameters3D.create(low_from, low_from + dir * 0.55)
|
||
low.exclude = [get_rid()]
|
||
low.collision_mask = collision_mask
|
||
if space.intersect_ray(low).is_empty():
|
||
return
|
||
var high_from := feet + up * (STEP_HEIGHT + 0.05)
|
||
var high := PhysicsRayQueryParameters3D.create(high_from, high_from + dir * 0.6)
|
||
high.exclude = [get_rid()]
|
||
high.collision_mask = collision_mask
|
||
if not space.intersect_ray(high).is_empty():
|
||
return
|
||
var down_from := high_from + dir * 0.35
|
||
var down := PhysicsRayQueryParameters3D.create(down_from, down_from - up * (STEP_HEIGHT + 0.08))
|
||
down.exclude = [get_rid()]
|
||
down.collision_mask = collision_mask
|
||
var hit := space.intersect_ray(down)
|
||
if hit.is_empty():
|
||
return
|
||
var hit_pos: Vector3 = hit.position
|
||
var lift: float = (hit_pos - feet).dot(up)
|
||
if lift < 0.04 or lift > STEP_HEIGHT:
|
||
return
|
||
global_position += up * (lift + 0.02)
|
||
|