Files

279 lines
13 KiB
GDScript
Raw Permalink Normal View History

2026-09-12 11:01:37 +02:00
class_name OperatorModel
extends Node3D
# Visual rig only. Hitboxes, movement and the network capsule belong to Fighter.
# All proportions are in metres; the operator faces -Z.
const G = preload("res://scripts/weapon_geometry.gd")
static var palette: Dictionary = {}
static var models: Dictionary = {}
var upper: Node3D
var head: Node3D
var weapon_mount: Node3D
var hips: Array[Node3D] = []
var knees: Array[Node3D] = []
var stride := 0.0
var movement := 0.0
var imported: ImportedOperator
func setup(team: int, variant: String = "steve", use_imported: bool = true) -> void:
name = "OperatorModel"
if use_imported:
imported = ImportedOperator.new()
add_child(imported)
imported.setup(variant)
weapon_mount = imported.weapon_mount
return
if models.has(team):
add_child(models[team].instantiate())
else:
var rig := Node3D.new()
rig.name = "Rig"
add_child(rig)
build(rig, team)
WeaponModels.assign_owner(rig, rig)
var packed := PackedScene.new()
packed.pack(rig)
models[team] = packed
var root := get_node("Rig")
upper = root.get_node("Upper")
head = upper.get_node("Head")
weapon_mount = upper.get_node("WeaponMount")
for side in ["L", "R"]:
var hip: Node3D = root.get_node("Hip" + side)
hips.append(hip)
knees.append(hip.get_node("Knee"))
static func material(key: String) -> StandardMaterial3D:
if palette.has(key):
return palette[key]
var colors := {"fabric": "54594f", "fabric_dark": "3c423b", "webbing": "797864",
"armor": "363d3c", "rubber": "202927", "leather": "404139", "lens": "293d41",
"edge": "8a948c", "teal": "3eb6b3", "orange": "d98354", "thread": "a5a28b", "skin": "b88768", "lips": "795447", "eyes": "ded8ca"}
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(colors.get(key, "54594f"))
mat.roughness = 0.84
if key == "lens":
mat.metallic = 0.58
mat.roughness = 0.17
elif key == "edge":
mat.metallic = 0.55
mat.roughness = 0.4
elif key in ["fabric", "fabric_dark", "webbing", "leather"]:
var image := Image.create(64, 64, false, Image.FORMAT_RGB8)
for y in 64:
for x in 64:
var grain := 0.79 + 0.13 * float((x * 31 + y * 17) % 11) / 10.0
var weave := 0.08 if (x % 4 < 2) != (y % 4 < 2) else 0.0
image.set_pixel(x, y, Color.WHITE * (grain + weave))
image.generate_mipmaps()
mat.albedo_texture = ImageTexture.create_from_image(image)
mat.uv1_triplanar = true
mat.uv1_scale = Vector3.ONE * 16
palette[key] = mat
return mat
static func attach(parent: Node3D, mesh: Mesh, pos: Vector3, key: String) -> MeshInstance3D:
var node := MeshInstance3D.new()
node.mesh = mesh
node.position = pos
node.material_override = material(key)
parent.add_child(node)
return node
static func oval(parent: Node3D, pos: Vector3, size: Vector3, key: String) -> MeshInstance3D:
var mesh := SphereMesh.new()
mesh.radius = 0.5
mesh.height = 1.0
mesh.radial_segments = 16
mesh.rings = 10
var node := attach(parent, mesh, pos, key)
node.scale = size
return node
static func panel(parent: Node3D, pos: Vector3, size: Vector3, key: String, rot := Vector3.ZERO) -> MeshInstance3D:
var node := G.block(parent, pos, size, "polymer", rot)
node.material_override = material(key)
return node
# Contoured elliptical rings form fabric volume and tapered anatomy, with smooth
# analytic normals rather than sharp box faces. Vector4: Y, radius X/Z, offset Z.
static func loft(parent: Node3D, rings: Array[Vector4], pos: Vector3, key: String, sides := 16) -> MeshInstance3D:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var uvs := PackedVector2Array()
var indices := PackedInt32Array()
for r in rings.size():
var ring := rings[r]
var previous := rings[maxi(0, r - 1)]
var next := rings[mini(rings.size() - 1, r + 1)]
for i in sides + 1:
var angle := float(i) * TAU / sides
var sx := sin(angle)
var cz := cos(angle)
vertices.append(Vector3(sx * ring.y, ring.x, cz * ring.z + ring.w))
var tangent := Vector3(cz * ring.y, 0, -sx * ring.z)
var vertical := Vector3(sx * (next.y - previous.y), next.x - previous.x,
cz * (next.z - previous.z) + next.w - previous.w)
normals.append(tangent.cross(vertical).normalized())
uvs.append(Vector2(float(i) / sides, ring.x))
for r in rings.size() - 1:
for i in sides:
var a := r * (sides + 1) + i
var b := a + sides + 1
indices.append_array(PackedInt32Array([a, b, b + 1, a, b + 1, a + 1]))
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return attach(parent, mesh, pos, key)
static func limb(parent: Node3D, start: Vector3, finish: Vector3, radius: float, tip: float, key: String) -> MeshInstance3D:
var length := start.distance_to(finish)
var mesh := loft(parent, [Vector4(0, radius * 0.45, radius * 0.45, 0),
Vector4(length * 0.1, radius * 0.94, radius * 0.86, 0),
Vector4(length * 0.3, radius, radius * 0.95, 0),
Vector4(length * 0.58, lerpf(radius, tip, 0.55), lerpf(radius, tip, 0.55) * 0.91, 0),
Vector4(length * 0.87, tip, tip * 0.9, 0),
Vector4(length, tip * 0.68, tip * 0.68, 0)], start, key)
mesh.quaternion = Quaternion(Vector3.UP, (finish - start).normalized())
return mesh
static func bone(parent: Node3D, label: String, pos: Vector3) -> Node3D:
var node := Node3D.new()
node.name = label
node.position = pos
parent.add_child(node)
return node
static func build(rig: Node3D, team: int) -> void:
var accent := "teal" if team == 0 else "orange"
var body := bone(rig, "Upper", Vector3.ZERO)
# Contoured jacket: hips, waist, rib cage, shoulders and collar.
loft(body, [Vector4(0.81, 0.17, 0.10, 0.02), Vector4(0.89, 0.21, 0.135, 0.02),
Vector4(1.01, 0.19, 0.13, 0.015), Vector4(1.13, 0.215, 0.145, 0),
Vector4(1.31, 0.24, 0.14, 0), Vector4(1.41, 0.245, 0.12, 0.015),
Vector4(1.465, 0.15, 0.10, 0.025), Vector4(1.49, 0.08, 0.07, 0.02)], Vector3.ZERO, "fabric")
# Rounded armor carrier plates, side closures, shoulder harness and MOLLE.
panel(body, Vector3(0, 1.245, -0.129), Vector3(0.37, 0.36, 0.075), "armor")
panel(body, Vector3(0, 1.26, -0.176), Vector3(0.31, 0.265, 0.035), "fabric_dark")
panel(body, Vector3(0, 1.12, 0.149), Vector3(0.325, 0.33, 0.11), "webbing")
oval(body, Vector3(0, 1.34, 0.156), Vector3(0.31, 0.23, 0.13), "fabric_dark")
for side in [-1, 1]:
panel(body, Vector3(side * 0.157, 1.414, 0.006), Vector3(0.063, 0.045, 0.29), "webbing", Vector3(0, 0, side * -0.24))
panel(body, Vector3(side * 0.171, 1.365, -0.14), Vector3(0.066, 0.06, 0.029), "rubber")
panel(body, Vector3(side * 0.198, 1.05, 0), Vector3(0.07, 0.12, 0.27), "webbing")
panel(body, Vector3(side * 0.15, 1.075, -0.17), Vector3(0.072, 0.17, 0.052), "webbing")
for row in 3:
panel(body, Vector3(side * 0.085, 1.20 + row * 0.044, -0.2), Vector3(0.085, 0.015, 0.009), "webbing")
for row in 3:
panel(body, Vector3(side * 0.18, 1.03 + row * 0.09, 0.21), Vector3(0.05, 0.045, 0.02), "armor")
for x in [-0.085, 0.0, 0.085]:
panel(body, Vector3(x, 1.074, -0.21), Vector3(0.073, 0.155, 0.06), "webbing")
panel(body, Vector3(x, 1.137, -0.244), Vector3(0.065, 0.043, 0.012), "fabric_dark")
panel(body, Vector3(0, 1.358, -0.196), Vector3(0.108, 0.038, 0.008), accent)
for x in [-0.027, 0, 0.027]:
panel(body, Vector3(x, 1.36, -0.202), Vector3(0.008, 0.019, 0.003), "thread")
# Belt and radio, with subdued material break-up on the back.
loft(body, [Vector4(0.885, 0.215, 0.145, 0.015), Vector4(0.94, 0.21, 0.142, 0.015)], Vector3.ZERO, "rubber")
panel(body, Vector3(0, 0.917, -0.136), Vector3(0.065, 0.038, 0.018), "edge")
panel(body, Vector3(-0.233, 1.19, 0.025), Vector3(0.065, 0.12, 0.06), "rubber")
limb(body, Vector3(-0.25, 1.24, 0.025), Vector3(-0.24, 1.45, 0.025), 0.004, 0.003, "rubber")
limb(body, Vector3(0, 1.44, 0.015), Vector3(0, 1.56, 0.015), 0.069, 0.065, "fabric_dark")
make_head(bone(body, "Head", Vector3(0, 1.615, 0.005)), accent)
make_arms(body, accent)
bone(body, "WeaponMount", Vector3(0.2, 1.25, -0.26))
G.batch(body)
for side in [-1, 1]:
make_leg(rig, side)
static func make_head(node: Node3D, accent: String) -> void:
oval(node, Vector3(0, -0.012, -0.014), Vector3(0.235, 0.296, 0.224), "skin")
# The helmet has a full dome, rolled rim and side-mounted headset cups.
loft(node, [Vector4(-0.012, 0.133, 0.136, 0.01), Vector4(0.035, 0.143, 0.146, 0.015),
Vector4(0.094, 0.128, 0.133, 0.018), Vector4(0.145, 0.086, 0.09, 0.019),
Vector4(0.166, 0.001, 0.001, 0.02)], Vector3.ZERO, "armor", 24)
loft(node, [Vector4(-0.018, 0.135, 0.14, 0.01), Vector4(0.002, 0.143, 0.147, 0.01),
Vector4(0.011, 0.141, 0.145, 0.01)], Vector3.ZERO, "rubber", 24)
# Exposed face beneath helmet: eyes, brows and nose instead of opaque goggles.
oval(node, Vector3(0, -0.036, -0.135), Vector3(0.039, 0.063, 0.051), "skin")
for side in [-1, 1]:
oval(node, Vector3(side * 0.046, -0.012, -0.122), Vector3(0.041, 0.017, 0.018), "eyes")
oval(node, Vector3(side * 0.046, -0.012, -0.133), Vector3(0.012, 0.014, 0.006), "rubber")
panel(node, Vector3(side * 0.047, 0.009, -0.126), Vector3(0.047, 0.009, 0.012), "fabric_dark")
oval(node, Vector3(side * 0.128, -0.034, 0.017), Vector3(0.067, 0.123, 0.087), "rubber")
panel(node, Vector3(side * 0.148, 0.011, 0.048), Vector3(0.022, 0.023, 0.095), "edge")
panel(node, Vector3(side * 0.139, 0.057, 0.006), Vector3(0.018, 0.048, 0.081), accent)
limb(node, Vector3(side * 0.109, -0.051, -0.01), Vector3(side * 0.061, -0.122, -0.071), 0.011, 0.009, "webbing")
oval(node, Vector3(0, -0.079, -0.111), Vector3(0.061, 0.013, 0.021), "lips")
panel(node, Vector3(0, 0.072, -0.128), Vector3(0.045, 0.058, 0.024), "rubber")
panel(node, Vector3(0, 0.086, -0.144), Vector3(0.029, 0.021, 0.011), "edge")
limb(node, Vector3(-0.148, -0.036, -0.01), Vector3(-0.08, -0.098, -0.137), 0.005, 0.004, "rubber")
oval(node, Vector3(-0.07, -0.101, -0.145), Vector3(0.031, 0.014, 0.017), "rubber")
G.batch(node)
static func make_arms(body: Node3D, accent: String) -> void:
for side in [-1, 1]:
var shoulder := Vector3(side * 0.245, 1.387, 0.012)
var elbow := Vector3(0.353, 1.083, -0.025) if side == 1 else Vector3(-0.287, 1.105, -0.206)
var wrist := Vector3(0.218, 1.131, -0.185) if side == 1 else Vector3(0.167, 1.192, -0.49)
limb(body, shoulder, elbow, 0.092, 0.065, "fabric")
oval(body, shoulder, Vector3(0.20, 0.185, 0.203), "fabric")
oval(body, elbow, Vector3(0.127, 0.11, 0.131), "armor")
limb(body, elbow, wrist, 0.074, 0.046, "fabric")
var patch := oval(body, shoulder + Vector3(side * 0.081, -0.038, -0.006), Vector3(0.048, 0.129, 0.13), accent)
patch.rotation.z = side * -0.22
for fold in [0.24, 0.48, 0.72]:
var at := elbow.lerp(wrist, fold)
var crease := oval(body, at, Vector3(0.124 - fold * 0.03, 0.025, 0.12 - fold * 0.025), "fabric_dark")
crease.quaternion = Quaternion(Vector3.UP, (wrist - elbow).normalized())
oval(body, wrist + Vector3(0, -0.006, -0.029), Vector3(0.093, 0.095, 0.121), "leather")
for finger in 4:
oval(body, wrist + Vector3(-0.025 + finger * 0.017, 0.029, -0.067), Vector3(0.019, 0.04, 0.05), "rubber")
static func make_leg(rig: Node3D, side: int) -> void:
var hip := bone(rig, "HipL" if side < 0 else "HipR", Vector3(side * 0.132, 0.884, 0.016))
limb(hip, Vector3.ZERO, Vector3(side * 0.018, -0.408, -0.009), 0.123, 0.089, "fabric")
oval(hip, Vector3(side * 0.083, -0.19, 0.014), Vector3(0.092, 0.185, 0.17), "fabric_dark")
panel(hip, Vector3(side * 0.103, -0.141, -0.014), Vector3(0.043, 0.053, 0.153), "webbing")
var knee := bone(hip, "Knee", Vector3(side * 0.018, -0.408, -0.009))
oval(knee, Vector3(0, 0.01, -0.062), Vector3(0.15, 0.15, 0.086), "armor")
oval(knee, Vector3(0, 0.012, -0.098), Vector3(0.104, 0.104, 0.028), "rubber")
limb(knee, Vector3(0, -0.015, 0), Vector3(0, -0.321, 0.019), 0.09, 0.059, "fabric")
for y in [-0.103, -0.219]:
oval(knee, Vector3(0, y, 0.017), Vector3(0.16, 0.043, 0.148), "fabric_dark")
oval(knee, Vector3(0, -0.332, 0.016), Vector3(0.146, 0.19, 0.161), "leather")
oval(knee, Vector3(0, -0.392, -0.047), Vector3(0.157, 0.128, 0.276), "leather")
panel(knee, Vector3(0, -0.431, -0.043), Vector3(0.163, 0.043, 0.281), "rubber")
oval(knee, Vector3(0, -0.389, -0.125), Vector3(0.15, 0.073, 0.104), "rubber")
for row in 4:
panel(knee, Vector3(0, -0.335 - row * 0.011, -0.043 - row * 0.018), Vector3(0.067, 0.007, 0.007), "webbing", Vector3(0, (1 if row % 2 else -1) * 0.19, 0))
G.batch(hip)
G.batch(knee)
func animate(dt: float, speed: float, view_pitch: float, crouching: bool) -> void:
if imported != null:
imported.animate_state(dt, {"velocity": Vector3(0, 0, -speed), "pitch": view_pitch, "crouch": crouching})
return
movement = move_toward(movement, clampf(speed / 6.0, 0, 1.35), dt * 8)
stride += dt * lerpf(4.0, 11.5, minf(movement, 1.0))
var amount := movement * (0.3 if crouching else 0.53)
for index in 2:
var phase := stride + index * PI
hips[index].rotation.x = sin(phase) * amount
knees[index].rotation.x = maxf(0, -sin(phase)) * amount * 1.05
upper.position.y = absf(cos(stride)) * movement * 0.018
upper.rotation.z = sin(stride) * movement * 0.015
upper.rotation.x = -movement * 0.025
head.rotation.x = clampf(view_pitch * 0.6, -0.48, 0.42)
func animate_state(dt: float, state: Dictionary) -> void:
if imported != null:
imported.animate_state(dt, state)
else:
var velocity: Vector3 = state.get("velocity", Vector3.ZERO)
animate(dt, Vector2(velocity.x, velocity.z).length(), float(state.get("pitch", 0)), bool(state.get("crouch", false)))