UNIT-77 has a body: modular part rig, third person, skins

Four farm-generated parts (chassis/wheel/head/arm, light-grey enamel,
decimated 25k) assembled on a socket rig: wheels spin with movement and
visibly vanish when donated, PORT 00 is a real socket ring on the chassis
that lights amber when the crystal installs, charged = cyan emission,
wet = darkened shell. V toggles third/first person (SpringArm cam),
B cycles six repaint skins (GC/Heliotrope/VerdantCo/Comms/Executive/
Void) via per-droid material tints. Hold point moved from camera to
chassis. Capsule fallback kept. Integration passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-04 21:56:30 +10:00
parent f8aa19d06e
commit aa1efea167
26 changed files with 696 additions and 13 deletions

BIN
assets/models/droid_arm.glb Normal file

Binary file not shown.

View File

@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bs1u7n2jb2rte"
path="res://.godot/imported/droid_arm.glb-87692637a4de3117c0c9d4fa49343974.scn"
[deps]
source_file="res://assets/models/droid_arm.glb"
dest_files=["res://.godot/imported/droid_arm.glb-87692637a4de3117c0c9d4fa49343974.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://byos5d5uaxjwg"
path="res://.godot/imported/droid_arm_Image_0.jpg-04c72430e4fb9d5d3b8f1fff5e74ebdb.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "2f983e6ffbe30961ef69dcad55fd56f0"
}
[deps]
source_file="res://assets/models/droid_arm_Image_0.jpg"
dest_files=["res://.godot/imported/droid_arm_Image_0.jpg-04c72430e4fb9d5d3b8f1fff5e74ebdb.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbndggsykq4fr"
path="res://.godot/imported/droid_arm_Image_1.jpg-33e2003a3cb5aef640e28fe7df632ab0.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "80c90542f76a7f141cb8e0b547bde5b6"
}
[deps]
source_file="res://assets/models/droid_arm_Image_1.jpg"
dest_files=["res://.godot/imported/droid_arm_Image_1.jpg-33e2003a3cb5aef640e28fe7df632ab0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View File

@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://em1dpxjs40t"
path="res://.godot/imported/droid_chassis.glb-6faef3d6dfdace4e5a68eb3b59035f57.scn"
[deps]
source_file="res://assets/models/droid_chassis.glb"
dest_files=["res://.godot/imported/droid_chassis.glb-6faef3d6dfdace4e5a68eb3b59035f57.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c86qoudfeafbu"
path="res://.godot/imported/droid_chassis_Image_0.jpg-3e1f5a9bdd0be320dc859c0bc796868a.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "9ffef4e9f2041c31238bf3c748b421a0"
}
[deps]
source_file="res://assets/models/droid_chassis_Image_0.jpg"
dest_files=["res://.godot/imported/droid_chassis_Image_0.jpg-3e1f5a9bdd0be320dc859c0bc796868a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cupqwmqj4tqpm"
path="res://.godot/imported/droid_chassis_Image_1.jpg-b22dc2642b4cdcbe7c0fe892691e6627.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "70b1cd8dc32c27d7feacc2253bdb7d7a"
}
[deps]
source_file="res://assets/models/droid_chassis_Image_1.jpg"
dest_files=["res://.godot/imported/droid_chassis_Image_1.jpg-b22dc2642b4cdcbe7c0fe892691e6627.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View File

@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://efktwyjm6t26"
path="res://.godot/imported/droid_head.glb-9631f3e3801007fab1d8f33bcec0312b.scn"
[deps]
source_file="res://assets/models/droid_head.glb"
dest_files=["res://.godot/imported/droid_head.glb-9631f3e3801007fab1d8f33bcec0312b.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://s1lrx7ulmeoj"
path="res://.godot/imported/droid_head_Image_0.jpg-2f9ad1b68d59c503083b639ff2b0e599.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "6fdb37196ea3b1f603d8df9bdfd29b0c"
}
[deps]
source_file="res://assets/models/droid_head_Image_0.jpg"
dest_files=["res://.godot/imported/droid_head_Image_0.jpg-2f9ad1b68d59c503083b639ff2b0e599.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dgtmfi527rkfo"
path="res://.godot/imported/droid_head_Image_1.jpg-8c77c664416a070e487d7326827087a1.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "6ae6632dbfd7b1e7d75097a8b21adb8c"
}
[deps]
source_file="res://assets/models/droid_head_Image_1.jpg"
dest_files=["res://.godot/imported/droid_head_Image_1.jpg-8c77c664416a070e487d7326827087a1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

View File

@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://df84docixowys"
path="res://.godot/imported/droid_wheel.glb-89418c7f822eb7303917e3d789641f0c.scn"
[deps]
source_file="res://assets/models/droid_wheel.glb"
dest_files=["res://.godot/imported/droid_wheel.glb-89418c7f822eb7303917e3d789641f0c.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bmbx5cjq4dsiw"
path="res://.godot/imported/droid_wheel_Image_0.jpg-2e55d81137fd18d53f788a27d09e641d.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "841cd301389f493c6741aa30673692d7"
}
[deps]
source_file="res://assets/models/droid_wheel_Image_0.jpg"
dest_files=["res://.godot/imported/droid_wheel_Image_0.jpg-2e55d81137fd18d53f788a27d09e641d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://eamk6dhayp65"
path="res://.godot/imported/droid_wheel_Image_1.jpg-a499d35ddd40e67eb78063803344eb8a.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "17d7df751f7ffc54d778bce4c1fea036"
}
[deps]
source_file="res://assets/models/droid_wheel_Image_1.jpg"
dest_files=["res://.godot/imported/droid_wheel_Image_1.jpg-a499d35ddd40e67eb78063803344eb8a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -3,10 +3,24 @@ extends CharacterBody3D
## UNIT-77. Its shell and core are Exposables — the same class as every wall
## component (GDD pillar 4: symmetric exposure). Charged core = mobile
## electricity ElementField (be the wire). Wet + charged = self-shock reboot.
## The body is assembled from generated part GLBs on a socket rig: wheels
## spin, a donated wheel visibly disappears, PORT 00 is a real socket on the
## chassis, and skins are repaints of the whole part set (B). V toggles
## third/first person.
const BASE_SPEED := 5.0
const SLOW_SPEED := 2.4
const MOUSE_SENS := 0.0025
const WHEEL_R := 0.26
const SKINS := [
["GC STANDARD", Color(1.0, 0.62, 0.25)],
["HELIOTROPE", Color(1.0, 0.38, 0.30)],
["VERDANTCO", Color(0.66, 1.0, 0.62)],
["COMMS BLUE", Color(0.55, 0.72, 1.0)],
["EXECUTIVE", Color(1.0, 0.92, 0.72)],
["VOID PLATE", Color(0.38, 0.38, 0.44)],
]
var wheels := 2
var spare_wheels := 0
@ -14,12 +28,23 @@ var held: Node3D = null
var stun := 0.0
var toast_text := ""
var toast_t := 0.0
var skin := 0
var third_person := true
var shell: Exposable
var core: Exposable
var emitter: ElementField
var cam: Camera3D
var spring: SpringArm3D
var hold_point: Node3D
var body_root: Node3D = null
var wheel_l: Node3D
var wheel_r: Node3D
var _capsule_mi: MeshInstance3D
var _body_mats: Array = []
var _port_mat: StandardMaterial3D
var _fx := ""
var _pitch := 0.0
@ -32,25 +57,28 @@ func _ready() -> void:
col.position.y = 0.85
add_child(col)
var mi := MeshInstance3D.new()
_capsule_mi = MeshInstance3D.new()
var m := CapsuleMesh.new()
m.radius = 0.45
m.height = 1.5
mi.mesh = m
mi.position.y = 0.85
_capsule_mi.mesh = m
_capsule_mi.position.y = 0.85
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.9, 0.55, 0.15)
mi.material_override = mat
add_child(mi)
_capsule_mi.material_override = mat
add_child(_capsule_mi)
spring = SpringArm3D.new()
spring.position = Vector3(0, 1.75, 0)
spring.spring_length = 3.0
spring.rotation.x = -0.22
spring.add_excluded_object(get_rid())
add_child(spring)
cam = Camera3D.new()
cam.position = Vector3(0, 1.45, 0)
add_child(cam)
cam.current = true
hold_point = Node3D.new()
hold_point.position = Vector3(0.4, -0.35, -0.9)
cam.add_child(hold_point)
hold_point.position = Vector3(0.5, 0.95, -0.78)
add_child(hold_point)
shell = Exposable.new().setup("UNIT-77 SHELL", "dry", [
{"from": "dry", "to": "wet", "element": "water",
@ -70,9 +98,118 @@ func _ready() -> void:
emitter.position.y = 0.9
add_child(emitter)
_build_body()
_apply_cam_mode()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# ---------- modular body ----------
func _part(parent: Node3D, file: String, pos: Vector3, target_h: float, center := false, rot_y := 0.0) -> bool:
var path := "res://assets/models/" + file
if not FileAccess.file_exists(path):
return false
var p: Node3D = (load(path) as PackedScene).instantiate()
parent.add_child(p)
p.position = Vector3.ZERO
p.rotation.y = rot_y
var inv := parent.global_transform.affine_inverse()
var aabb := AABB()
var first := true
for mi in p.find_children("*", "MeshInstance3D", true, false):
var b: AABB = (inv * mi.global_transform) * mi.get_aabb()
aabb = b if first else aabb.merge(b)
first = false
if first:
return true
var sc := target_h / maxf(aabb.size.y, 0.01)
p.scale = Vector3.ONE * sc
var c := aabb.get_center()
if center:
p.position = pos - c * sc
else:
p.position = pos + Vector3(-c.x * sc, -aabb.position.y * sc, -c.z * sc)
return true
func _build_body() -> void:
body_root = Node3D.new()
add_child(body_root)
if not _part(body_root, "droid_chassis.glb", Vector3(0, 0.40, 0), 0.85):
body_root.queue_free()
body_root = null
return # capsule fallback until parts exist
_part(body_root, "droid_head.glb", Vector3(0, 1.30, 0.02), 0.42, false, PI)
wheel_l = Node3D.new()
wheel_l.position = Vector3(-0.40, WHEEL_R, 0)
body_root.add_child(wheel_l)
wheel_r = Node3D.new()
wheel_r.position = Vector3(0.40, WHEEL_R, 0)
body_root.add_child(wheel_r)
_part(wheel_l, "droid_wheel.glb", Vector3.ZERO, WHEEL_R * 2.0, true)
_part(wheel_r, "droid_wheel.glb", Vector3.ZERO, WHEEL_R * 2.0, true)
_part(body_root, "droid_arm.glb", Vector3(0.44, 0.82, -0.22), 0.5, false, PI)
# PORT 00 — the socket nothing fits. Front of chassis, off-center.
var ring := MeshInstance3D.new()
var t := TorusMesh.new()
t.inner_radius = 0.05
t.outer_radius = 0.09
ring.mesh = t
ring.rotation.x = PI / 2.0
ring.position = Vector3(-0.2, 0.95, -0.42)
_port_mat = StandardMaterial3D.new()
_port_mat.albedo_color = Color(0.08, 0.08, 0.1)
ring.material_override = _port_mat
body_root.add_child(ring)
# per-droid unique materials so skins/FX don't leak to world props
for mi in body_root.find_children("*", "MeshInstance3D", true, false):
if mi == ring or not mi.mesh:
continue
for si in mi.mesh.get_surface_count():
var mt = mi.mesh.surface_get_material(si)
if mt is BaseMaterial3D:
var dup: BaseMaterial3D = mt.duplicate()
dup.metallic = minf(dup.metallic, 0.2)
mi.set_surface_override_material(si, dup)
_body_mats.append(dup)
_apply_skin()
func _apply_skin() -> void:
var tint: Color = SKINS[skin][1]
var wet := shell != null and shell.state == "wet"
var charged := core != null and core.state == "charged"
for mt in _body_mats:
mt.albedo_color = tint * (Color(0.55, 0.6, 0.78) if wet else Color.WHITE)
mt.emission_enabled = charged
if charged:
mt.emission = Color(0.2, 0.9, 1.0)
mt.emission_energy_multiplier = 0.45
func _apply_cam_mode() -> void:
if cam.get_parent():
cam.get_parent().remove_child(cam)
if third_person:
spring.add_child(cam)
cam.position = Vector3.ZERO
cam.rotation = Vector3.ZERO
spring.rotation.x = clampf(_pitch, -1.2, 0.5)
else:
add_child(cam)
cam.position = Vector3(0, 1.45, 0)
cam.rotation.x = _pitch
cam.current = true
var show_body := third_person
if body_root:
body_root.visible = show_body
_capsule_mi.visible = false
else:
_capsule_mi.visible = show_body
func speed() -> float:
return BASE_SPEED if wheels >= 2 else SLOW_SPEED
@ -86,11 +223,21 @@ func _unhandled_input(e: InputEvent) -> void:
if e is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
rotate_y(-e.relative.x * MOUSE_SENS)
_pitch = clampf(_pitch - e.relative.y * MOUSE_SENS, -1.25, 1.25)
cam.rotation.x = _pitch
if third_person:
spring.rotation.x = clampf(_pitch, -1.2, 0.5)
else:
cam.rotation.x = _pitch
elif e.is_action_pressed("interact"):
var t := best_target(["interactable"], 3.0, 0.7)
if t and t.has_method("interact"):
t.interact(self)
elif e.is_action_pressed("toggle_cam"):
third_person = not third_person
_apply_cam_mode()
elif e.is_action_pressed("repaint"):
skin = (skin + 1) % SKINS.size()
_apply_skin()
toast("REPAINT APPLIED: %s. GC CHARGES THIS TO YOUR UNIT." % SKINS[skin][0])
elif e.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
@ -108,7 +255,7 @@ func best_target(groups: Array, max_d: float, min_dot := 0.9) -> Node3D:
continue
var to_n: Vector3 = n.global_position - cam.global_position
var d := to_n.length()
if d > max_d or d < 0.01:
if d > max_d + (spring.spring_length if third_person else 0.0) or d < 0.01:
continue
var dot := fwd.dot(to_n / d)
if dot > best_dot:
@ -157,6 +304,30 @@ func _physics_process(delta: float) -> void:
velocity.y = -0.5
emitter.enabled = core.state == "charged"
# body FX + wheels
if body_root:
var fx := "%s|%s|%d" % [shell.state, core.state, skin]
if fx != _fx:
_fx = fx
_apply_skin()
if wheel_r:
wheel_r.visible = wheels >= 2
if wheel_l:
wheel_l.visible = wheels >= 1
var fwd_speed := velocity.dot(-transform.basis.z)
if wheel_l and wheel_l.visible:
wheel_l.rotate_object_local(Vector3.RIGHT, -fwd_speed * delta / WHEEL_R)
if wheel_r and wheel_r.visible:
wheel_r.rotate_object_local(Vector3.RIGHT, -fwd_speed * delta / WHEEL_R)
if _port_mat:
var free: bool = get_parent() and get_parent().get("free_will")
if free and not _port_mat.emission_enabled:
_port_mat.emission_enabled = true
_port_mat.emission = Color(1.0, 0.75, 0.2)
_port_mat.emission_energy_multiplier = 1.6
_port_mat.albedo_color = Color(0.9, 0.7, 0.3)
if toast_t > 0.0:
toast_t -= delta
move_and_slide()

View File

@ -107,7 +107,7 @@ func _init_input() -> void:
var keys := {
"move_forward": KEY_W, "move_back": KEY_S,
"move_left": KEY_A, "move_right": KEY_D,
"interact": KEY_E,
"interact": KEY_E, "toggle_cam": KEY_V, "repaint": KEY_B,
}
for action in keys:
if not InputMap.has_action(action):