feat: establish mech extraction prototype with bilingual UI
Track Unity source, scenes, editable assets, licenses, tools and QA evidence. Exclude generated caches, Windows builds and raw runtime logs.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Ironspore/Chassis")]
|
||||
public sealed class ChassisDefinition : ScriptableObject
|
||||
{
|
||||
public string id;
|
||||
public string displayName;
|
||||
public string description;
|
||||
public string LocalizedName => GameLocalization.Text("chassis." + id + ".name");
|
||||
public string LocalizedDescription => GameLocalization.Text("chassis." + id + ".description");
|
||||
public float speed = 6;
|
||||
public float boostSpeed = 18;
|
||||
public float boostDuration = .35f;
|
||||
public float lift;
|
||||
public float armor = 150;
|
||||
public float massCapacity = 20;
|
||||
public float powerCapacity = 18;
|
||||
public Color accent = Color.cyan;
|
||||
public Vector3 modelScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fc4fc94898f2bf4eb53e5103857c330
|
||||
@@ -0,0 +1,219 @@
|
||||
using UnityEngine;
|
||||
|
||||
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;
|
||||
}
|
||||
GameSession game;
|
||||
CharacterController motor;
|
||||
Camera view;
|
||||
Transform visual;
|
||||
GameObject cockpit;
|
||||
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<CharacterController>();
|
||||
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<Transform>())
|
||||
t.gameObject.layer = 8;
|
||||
var cam = new GameObject("PilotCamera");
|
||||
cam.transform.SetParent(transform);
|
||||
view = cam.AddComponent<Camera>();
|
||||
view.nearClipPlane = .08f;
|
||||
view.farClipPlane = 250;
|
||||
view.fieldOfView = 70;
|
||||
cam.AddComponent<AudioListener>();
|
||||
cockpit = WorldFactory.Cockpit(view.transform);
|
||||
Health = game.Run.armor;
|
||||
Energy = game.Run.energy;
|
||||
ApplyLoadout();
|
||||
}
|
||||
public void ApplyLoadout()
|
||||
{
|
||||
visual.localScale = game.chassis[game.Run.chassis].modelScale;
|
||||
}
|
||||
public void RequireFreshTrigger()
|
||||
{
|
||||
freshTrigger = true;
|
||||
}
|
||||
void Update()
|
||||
{
|
||||
if (!game.InField || game.Paused)
|
||||
return;
|
||||
var frame = game.chassis[game.Run.chassis];
|
||||
float dt = Time.deltaTime;
|
||||
boostCooldown -= dt;
|
||||
shotCooldown -= dt;
|
||||
sideCooldown -= dt;
|
||||
Guard = Mathf.Max(0, Guard - dt);
|
||||
Vector2 movement = game.MenuOpen ? Vector2.zero : game.Action("Move").ReadValue<Vector2>();
|
||||
bool usingEnergy = false;
|
||||
if (!game.MenuOpen)
|
||||
{
|
||||
Vector2 look = game.Action("Look").ReadValue<Vector2>();
|
||||
yaw += look.x * .105f;
|
||||
pitch = Mathf.Clamp(pitch - look.y * .105f, -72, 75);
|
||||
if (game.Action("View").WasPressedThisFrame())
|
||||
FirstPerson = !FirstPerson;
|
||||
if (game.Action("Guard").WasPressedThisFrame() && Energy >= 20)
|
||||
{
|
||||
Energy -= 20;
|
||||
Guard = 2.5f;
|
||||
}
|
||||
if (game.Action("Boost").WasPressedThisFrame() && 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;
|
||||
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 (!freshTrigger && game.Action("Fire").IsPressed() && shotCooldown <= 0)
|
||||
Shoot(false);
|
||||
if (game.Action("Secondary").WasPressedThisFrame() && 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);
|
||||
visual.gameObject.SetActive(!FirstPerson);
|
||||
cockpit.SetActive(FirstPerson);
|
||||
}
|
||||
void Shoot(bool secondary)
|
||||
{
|
||||
var part = game.Part(secondary ? 1 : 0, game.Run.equipment[secondary ? 1 : 0]);
|
||||
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<EnemyUnit>();
|
||||
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<EnemyUnit>();
|
||||
if (enemy)
|
||||
{
|
||||
enemy.Damage(part.damage * (1 + game.Run.upgrades * .15f));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab3c33b089c192f438cd78bec647dc13
|
||||
Reference in New Issue
Block a user