Files
unity/Assets/Game/Scripts/Runs/TrainingCourse.cs
RCiBot eb819c6688 feat: improve map lighting and expand the playable training range
Add real-time lighting and shadows, training courses and targets, and animated salvage crates. Include bilingual assets and curated verification results.
2026-11-12 18:03:04 +08:00

216 lines
9.7 KiB
C#

using UnityEngine;
namespace Ironspore
{
public sealed class TrainingCourse : MonoBehaviour
{
static readonly Color Steel = new Color(.19f, .28f, .32f);
static readonly Color Amber = new Color(.95f, .48f, .12f);
static readonly Color Green = new Color(.16f, .92f, .65f);
GameSession game;
Renderer firstLamp, upperLamp;
RangeTarget plate, movingPlate;
Vector3 previousPosition;
int gateCount, hits;
float startedAt, finishSeconds;
static readonly Vector3 StartGate = new Vector3(19, 0, -15);
static readonly Vector3 UpperGate = new Vector3(19, 4.3f, 4);
static readonly Vector3 Landing = new Vector3(19, 4.3f, 22);
public string Instruction
{
get
{
Vector3 position = game.World.Player.transform.position;
if (game.World.TransportReady) return GameLocalization.Text("range.board");
if (position.x < -16 && (position.z > 6 || Mathf.Abs(position.z + 8) < 4))
return GameLocalization.Text(game.Run.podUsed ? "range.recoverySent" : "range.recoveryGuide");
if (position.x < -7) return GameLocalization.Text("range.targetGuide", hits);
if (gateCount == 3) return GameLocalization.Text("range.finish", finishSeconds);
if (gateCount == 2) return GameLocalization.Text("range.brake");
if (gateCount == 1) return GameLocalization.Text("range.gate");
return GameLocalization.Text("range.choose");
}
}
public void Build(GameSession session)
{
game = session;
previousPosition = game.World.Player.transform.position;
firstLamp = Gate(StartGate);
upperLamp = Gate(UpperGate);
WorldFactory.Label("range.start", StartGate + Vector3.up * 5.95f, Green, transform);
WorldFactory.Label("range.upper", UpperGate + Vector3.up * 5.95f, Green, transform);
WorldFactory.Label("range.landing", Landing + Vector3.up * 1.7f, Green, transform);
for (int i = -1; i <= 1; i++)
WorldFactory.Box("LandingStripe", Landing + new Vector3(0,.02f,i * 1.35f), new Vector3(4.7f,.035f,.17f), Amber, transform, false);
plate = Target(new Vector3(-13, 0, -2), false);
movingPlate = Target(new Vector3(-25, 0, -2), true);
WorldFactory.Box("TraverseRail", new Vector3(-25,.12f,-2), new Vector3(9,.24f,.8f), Steel, transform);
foreach (int side in new[] { -1, 1 })
WorldFactory.Box("RailStop", new Vector3(-25 + side * 4.5f,.4f,-2), new Vector3(.35f,.8f,1.3f), Amber, transform);
WorldFactory.Box("FiringLine", new Vector3(-19,.025f,-15), new Vector3(12,.04f,.15f), Amber, transform, false);
WorldFactory.Label("range.gallery", new Vector3(-19,4.6f,-2), Amber, transform);
WorldFactory.Label("range.salvage", new Vector3(-24,4,14), Green, transform);
var desk = WorldFactory.Box("RangeResetDesk", new Vector3(-12, .85f, -15), new Vector3(1.4f, 1.7f, .85f), Steel, transform);
WorldFactory.Box("ResetSwitch", desk.transform.position + Vector3.up * .92f, new Vector3(.8f,.14f,.5f), Green, transform, false, true);
var reset = desk.AddComponent<RangeReset>();
reset.kind = 4;
reset.course = this;
WorldFactory.Label("range.reset", desk.transform.position + Vector3.up * 1.6f, Green, transform);
}
Renderer Gate(Vector3 at)
{
foreach (int side in new[] { -1, 1 })
{
Vector3 foot = at + Vector3.right * side * 3.1f;
WorldFactory.Box("GateFoot", foot + Vector3.up * .14f, new Vector3(.8f,.28f,1.2f), Steel, transform);
WorldFactory.Box("GatePost", foot + Vector3.up * 2.65f, new Vector3(.35f,5.3f,.5f), Steel, transform);
WorldFactory.Box("GateStripe", foot + Vector3.up * 2, new Vector3(.38f,.8f,.53f), Amber, transform, false);
}
return WorldFactory.Box("GateLamp", at + Vector3.up * 5.35f, new Vector3(6.6f,.26f,.45f), Amber, transform, false, true).GetComponent<Renderer>();
}
RangeTarget Target(Vector3 at, bool moves)
{
var carriage = new GameObject(moves ? "TraversingTarget" : "FixedTarget").transform;
carriage.SetParent(transform, false);
carriage.position = at;
var stand = WorldFactory.Box("TargetStand", at + Vector3.up * .55f, new Vector3(2.2f,1.1f,.85f), Steel, carriage);
if (moves) stand.layer = 10;
var pivot = new GameObject("RangeTarget").transform;
pivot.SetParent(carriage, false);
pivot.position = at + Vector3.up * 1.15f;
var panel = WorldFactory.Box("StrikePlate", pivot.position + Vector3.up * 1.15f, new Vector3(2.1f,2.3f,.22f), Amber, pivot);
var bull = WorldFactory.Primitive(PrimitiveType.Cylinder, "Bullseye", panel.transform.position + Vector3.back * .14f, new Vector3(1.25f,.025f,1.25f), Steel, pivot, false, false);
bull.transform.rotation = Quaternion.Euler(90,0,0);
var lamp = WorldFactory.Box("TargetLamp", bull.transform.position + Vector3.back * .04f, new Vector3(.52f,.52f,.05f), Amber, pivot, false, true);
foreach (Transform part in pivot.GetComponentsInChildren<Transform>()) part.gameObject.layer = 10;
var target = pivot.gameObject.AddComponent<RangeTarget>();
target.Initialize(this, panel.GetComponent<Collider>(), lamp.GetComponent<Renderer>(), moves);
return target;
}
void Update()
{
if (!game.Pilot || game.Paused) return;
Vector3 position = game.Pilot.transform.position;
if ((gateCount == 0 || gateCount == 3) && Crossed(previousPosition, position, StartGate))
{
gateCount = 1;
startedAt = Time.time;
SetLamp(firstLamp, true);
SetLamp(upperLamp, false);
game.Notify("range.gate");
}
else if (gateCount == 1 && Crossed(previousPosition, position, UpperGate))
{
gateCount = 2;
SetLamp(upperLamp, true);
game.Notify("range.brake");
}
else if (gateCount == 2 && Mathf.Abs(position.x - Landing.x) < 2.65f &&
Mathf.Abs(position.z - Landing.z) < 2.4f && Mathf.Abs(position.y - Landing.y) < .6f &&
game.Pilot.Grounded && game.Pilot.HorizontalSpeed < 1)
{
gateCount = 3;
finishSeconds = Time.time - startedAt;
game.Notify("range.finish", finishSeconds);
GameAudio.Cue(620,.2f);
}
previousPosition = position;
}
static bool Crossed(Vector3 from, Vector3 to, Vector3 gate)
{
if (from.z >= gate.z || to.z < gate.z) return false;
Vector3 crossing = Vector3.Lerp(from, to, (gate.z - from.z) / (to.z - from.z));
return Mathf.Abs(crossing.x - gate.x) < 2.9f && crossing.y >= gate.y - .5f && crossing.y < gate.y + 5;
}
internal static void SetLamp(Renderer renderer, bool on)
{
renderer.sharedMaterial = WorldFactory.Material(on ? Green : Amber, true);
}
internal void Hit()
{
game.Notify("range.hit", ++hits);
GameAudio.Cue(880, .12f);
}
public void ResetPractice()
{
hits = gateCount = 0;
previousPosition = game.Pilot.transform.position;
SetLamp(firstLamp, false);
SetLamp(upperLamp, false);
plate.ResetTarget();
movingPlate.ResetTarget();
game.Notify("range.ready");
}
}
public sealed class RangeTarget : MonoBehaviour
{
TrainingCourse course;
Collider strike;
Renderer lamp;
bool down, moving;
float travelTime, holdTime;
Vector3 origin;
internal void Initialize(TrainingCourse owner, Collider collider, Renderer indicator, bool moves)
{
course = owner;
strike = collider;
lamp = indicator;
moving = moves;
origin = transform.parent.localPosition;
}
public bool Hit()
{
if (down || holdTime > 0) return false;
down = !moving;
if (moving) holdTime = 1f;
else strike.enabled = false;
TrainingCourse.SetLamp(lamp, true);
course.Hit();
return true;
}
void Update()
{
if (down) transform.localRotation = Quaternion.RotateTowards(transform.localRotation, Quaternion.Euler(87,0,0), 240 * Time.deltaTime);
if (!moving) return;
if (holdTime > 0)
{
holdTime -= Time.deltaTime;
if (holdTime <= 0) TrainingCourse.SetLamp(lamp, false);
return;
}
travelTime += Time.deltaTime;
transform.parent.localPosition = origin + Vector3.right * (3.1f * Mathf.Sin(travelTime * .85f));
}
internal void ResetTarget()
{
down = false;
holdTime = travelTime = 0;
transform.parent.localPosition = origin;
strike.enabled = true;
transform.localRotation = Quaternion.identity;
TrainingCourse.SetLamp(lamp, false);
}
}
public sealed class RangeReset : WorldInteractable
{
public TrainingCourse course;
public override string Prompt => GameLocalization.Text("range.reset");
public override void Activate() => course.ResetPractice();
}
}