using UnityEngine; using UnityEngine.Rendering; namespace Ironspore { public sealed class MechPilot : MonoBehaviour { public float Health { get; private set; } public float Energy { get; private set; } public float HorizontalSpeed { get; private set; } public int LastDamageFrame { get; private set; } = -1; public bool FirstPerson { get; private set; } public bool Grounded => motor && motor.isGrounded; public float Guard { get; private set; } public int Rounds { get; private set; } public float ReloadRemaining { get; private set; } public float SecondaryCooldown => Mathf.Max(0, sideCooldown); public float Heading => yaw; public EquipmentDefinition Primary { get; private set; } GameSession game; CharacterController motor; Camera view; Transform visual; GameObject cockpit; Renderer[] worldRenderers; ShadowCastingMode[] worldShadowModes; bool displayedFirstPerson; float yaw, pitch, vertical, boostRemaining, boostCooldown, shotCooldown, sideCooldown; Vector3 boostDirection; bool freshTrigger = true; float lastHitTime; public void Initialize(GameSession session, GameObject model) { game = session; motor = gameObject.AddComponent(); motor.height = 3.15f; motor.radius = .65f; motor.center = new Vector3(0, 1.6f, 0); motor.stepOffset = .45f; motor.slopeLimit = 55; gameObject.layer = 8; visual = new GameObject("MechVisual").transform; visual.SetParent(transform, false); var modelInstance = Instantiate(model, visual); modelInstance.transform.localScale = Vector3.one; visual.localPosition = Vector3.zero; foreach (var t in visual.GetComponentsInChildren()) t.gameObject.layer = 8; worldRenderers = visual.GetComponentsInChildren(true); worldShadowModes = new ShadowCastingMode[worldRenderers.Length]; for (int i = 0; i < worldRenderers.Length; i++) worldShadowModes[i] = worldRenderers[i].shadowCastingMode; var cam = new GameObject("PilotCamera"); cam.tag = "MainCamera"; cam.transform.SetParent(transform); view = cam.AddComponent(); view.nearClipPlane = .08f; view.farClipPlane = 250; view.fieldOfView = 70; cam.AddComponent(); cockpit = WorldFactory.Cockpit(view.transform); Health = game.Run.armor; Energy = game.Run.energy; ApplyLoadout(); } public void ApplyLoadout() { var primary = game.Part(0, game.Run.equipment[0]); if (Primary != primary) { Primary = primary; Rounds = primary ? primary.magazineSize : 0; ReloadRemaining = 0; } visual.localScale = game.chassis[game.Run.chassis].modelScale; } public void RequireFreshTrigger() { freshTrigger = true; } void Update() { if (!game.InField || game.Paused || Health <= 0) return; var frame = game.chassis[game.Run.chassis]; float dt = Time.deltaTime; if (ReloadRemaining > 0) { ReloadRemaining = Mathf.Max(0, ReloadRemaining - dt); if (ReloadRemaining == 0) Rounds = Primary.magazineSize; } boostCooldown -= dt; shotCooldown -= dt; sideCooldown -= dt; Guard = Mathf.Max(0, Guard - dt); Vector2 movement = game.MenuOpen ? Vector2.zero : game.Action("Move").ReadValue(); bool usingEnergy = false; if (!game.MenuOpen) { Vector2 look = game.Action("Look").ReadValue(); float sensitivity = .105f * game.Settings.LookSensitivity * (game.Action("Aim").IsPressed() ? game.Settings.AimSensitivity : 1); yaw += look.x * sensitivity; pitch = Mathf.Clamp(pitch - look.y * sensitivity, -72, 75); if (game.Action("View").WasPressedThisFrame()) FirstPerson = !FirstPerson; if (game.Action("Guard").WasPressedThisFrame() && game.Run.equipment[3] >= 0 && Energy >= 20) { Energy -= 20; Guard = 2.5f; } if (game.Action("Boost").WasPressedThisFrame() && game.Run.equipment[2] >= 0 && boostCooldown <= 0 && Energy >= 18) { boostDirection = Quaternion.Euler(0, yaw, 0) * new Vector3(movement.x, 0, movement.y == 0 && movement.x == 0 ? 1 : movement.y); boostDirection.Normalize(); boostRemaining = frame.boostDuration; boostCooldown = .65f; Energy -= 18; GameAudio.Cue(170, .1f); } if (motor.isGrounded && game.Action("Jump").WasPressedThisFrame()) vertical = 7; if (frame.lift > 0 && game.Action("Jump").IsPressed() && Energy > 0 && !motor.isGrounded) { vertical = Mathf.MoveTowards(vertical, frame.lift, dt * 18); Energy = Mathf.Max(0, Energy - dt * 23); usingEnergy = true; } } Vector3 velocity = Quaternion.Euler(0, yaw, 0) * new Vector3(movement.x, 0, movement.y); float speed = frame.speed * (game.Part(2, game.Run.equipment[2])?.speedFactor ?? 1f); velocity = Vector3.ClampMagnitude(velocity, 1) * speed; if (boostRemaining > 0) { boostRemaining -= dt; velocity = boostDirection * frame.boostSpeed; usingEnergy = true; } if (motor.isGrounded && vertical < 0) vertical = -2; vertical -= dt * 20; velocity.y = vertical; motor.Move(velocity * dt); HorizontalSpeed = new Vector2(motor.velocity.x, motor.velocity.z).magnitude; visual.localRotation = Quaternion.Euler(0, Mathf.LerpAngle(visual.localEulerAngles.y, yaw, dt * 9), 0); if (!usingEnergy && Guard <= 0) Energy = Mathf.Min(100, Energy + dt * 18); if (transform.position.y < -20) Damage(1000); if (!game.MenuOpen) { if (!game.Action("Fire").IsPressed()) freshTrigger = false; if (Primary && game.Action("Reload").WasPressedThisFrame() && Rounds < Primary.magazineSize && ReloadRemaining == 0) ReloadRemaining = Primary.reloadSeconds; if (Primary && !freshTrigger && game.Action("Fire").IsPressed() && shotCooldown <= 0 && ReloadRemaining == 0 && Rounds > 0) { Shoot(false); if (--Rounds == 0) ReloadRemaining = Primary.reloadSeconds; } if (game.Action("Secondary").WasPressedThisFrame() && game.Run.equipment[1] >= 0 && sideCooldown <= 0 && Energy >= 15) { Energy -= 15; Shoot(true); sideCooldown = 2.5f; } } } void LateUpdate() { if (!view) return; Quaternion rotation = Quaternion.Euler(pitch, yaw, 0); Vector3 pivot = transform.position + Vector3.up * 2.7f; Vector3 desired = FirstPerson ? pivot + rotation * new Vector3(0, 0, .25f) : pivot + rotation * new Vector3(1.25f, .25f, -6); if (!FirstPerson) { Vector3 offset = desired - pivot; if (Physics.SphereCast(pivot, .25f, offset.normalized, out var obstruction, offset.magnitude, ~(1 << 8), QueryTriggerInteraction.Ignore)) desired = pivot + offset.normalized * Mathf.Max(.5f, obstruction.distance - .15f); } view.transform.SetPositionAndRotation(desired, rotation); float fov = game.MenuOpen ? 70 : game.Action("Aim").IsPressed() ? 50 : FirstPerson ? 78 : 70; view.fieldOfView = Mathf.Lerp(view.fieldOfView, fov, Time.unscaledDeltaTime * 12); if (displayedFirstPerson != FirstPerson) { displayedFirstPerson = FirstPerson; for (int i = 0; i < worldRenderers.Length; i++) if (worldRenderers[i]) worldRenderers[i].shadowCastingMode = FirstPerson ? ShadowCastingMode.ShadowsOnly : worldShadowModes[i]; cockpit.SetActive(FirstPerson); } } void Shoot(bool secondary) { var part = game.Part(secondary ? 1 : 0, game.Run.equipment[secondary ? 1 : 0]); if (!part) return; shotCooldown = secondary ? shotCooldown : 1 / part.fireRate; Ray ray = view.ViewportPointToRay(new Vector3(.5f, .5f)); Vector3 target = ray.GetPoint(180); if (Physics.Raycast(ray, out var aim, 180, ~(1 << 8), QueryTriggerInteraction.Ignore)) target = aim.point; Vector3 muzzle = transform.position + Vector3.up * 2 + Quaternion.Euler(0, yaw, 0) * new Vector3(.85f, 0, 1); if (secondary && part.guided) { var enemies = FindObjectsByType(); float nearest = 50; foreach (var enemy in enemies) { float d = Vector3.Distance(muzzle, enemy.transform.position); if (d < nearest) { nearest = d; target = enemy.transform.position + Vector3.up; } } } Vector3 direction = (target - muzzle).normalized; if (Physics.Raycast(muzzle, direction, out var hit, 180, ~(1 << 8), QueryTriggerInteraction.Ignore)) { target = hit.point; var enemy = hit.collider.GetComponentInParent(); if (enemy) { enemy.Damage(part.damage * (1 + game.Run.upgrades * .15f)); lastHitTime = Time.time; } else if (game.Trial && hit.collider.GetComponentInParent() is RangeTarget plate && plate.Hit()) lastHitTime = Time.time; } WorldFactory.Tracer(muzzle, target, secondary ? new Color(1, .65f, .2f) : new Color(.2f, 1, .85f)); GameAudio.Cue(secondary ? 140 : 240, .06f, .07f); } public bool RecentHit => Time.time - lastHitTime < .15f; public void Damage(float amount) { if (!game.InField || Health <= 0) return; LastDamageFrame = Time.frameCount; float armorFactor = game.Part(3, game.Run.equipment[3])?.id == "reactive_guard" ? .72f : 1; Health = Mathf.Max(0, Health - amount * armorFactor * (Guard > 0 ? .2f : 1)); game.Run.armor = Health; } } }