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)))