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.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user