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:
@@ -4,6 +4,8 @@ namespace Ironspore
|
||||
{
|
||||
public sealed class EnemyUnit : MonoBehaviour
|
||||
{
|
||||
// DynamicProps keeps its original physical obstruction semantics after render-layer separation.
|
||||
const int WorldObstructionMask = (1 << 0) | (1 << 10);
|
||||
GameSession game;
|
||||
float health, cooldown, age;
|
||||
int type;
|
||||
@@ -41,7 +43,7 @@ namespace Ironspore
|
||||
if (type != 1 && distance > 10)
|
||||
{
|
||||
Vector3 step = flat.normalized * Time.deltaTime * (type == 3 ? 1.4f : type == 0 ? 2.5f : 1.2f);
|
||||
if (!Physics.Raycast(transform.position + Vector3.up, step.normalized, 1.1f, 1, QueryTriggerInteraction.Ignore))
|
||||
if (!Physics.Raycast(transform.position + Vector3.up, step.normalized, 1.1f, WorldObstructionMask, QueryTriggerInteraction.Ignore))
|
||||
transform.position += step;
|
||||
}
|
||||
if (type == 0)
|
||||
@@ -54,7 +56,7 @@ namespace Ironspore
|
||||
if (cooldown <= 0 && distance < 42)
|
||||
{
|
||||
Vector3 origin = transform.position + Vector3.up * (type == 3 ? 3.5f : 2);
|
||||
if (!Physics.Linecast(origin, target, 1, QueryTriggerInteraction.Ignore))
|
||||
if (!Physics.Linecast(origin, target, WorldObstructionMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
var bolt = WorldFactory.Primitive(PrimitiveType.Sphere, "EnemyBolt", origin, Vector3.one * .3f, new Color(1, .35f, .08f), null, false, true);
|
||||
bolt.AddComponent<EnemyBolt>().Initialize((target - origin).normalized, type == 3 ? 23 : 12);
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
public static class WorldFactory
|
||||
{
|
||||
static readonly Dictionary<string, Material> materials = new Dictionary<string, Material>();
|
||||
public static Material Material(Color color, bool glow = false)
|
||||
public enum Surface { Painted, Concrete, BareMetal }
|
||||
public static Material Material(Color color, bool glow = false, Surface surface = Surface.Painted)
|
||||
{
|
||||
string key = ColorUtility.ToHtmlStringRGB(color) + (glow ? "g" : "");
|
||||
string key = ColorUtility.ToHtmlStringRGB(color) + (glow ? "g" : "") + (int)surface;
|
||||
if (materials.TryGetValue(key, out var cached) && cached)
|
||||
return cached;
|
||||
var mat = new Material(Shader.Find("Standard"));
|
||||
mat.color = color;
|
||||
mat.SetFloat("_Metallic", .3f);
|
||||
mat.SetFloat("_Glossiness", .35f);
|
||||
mat.SetFloat("_Metallic", surface == Surface.BareMetal ? .78f : surface == Surface.Concrete ? 0 : .04f);
|
||||
mat.SetFloat("_Glossiness", surface == Surface.Concrete ? .15f : surface == Surface.BareMetal ? .48f : .32f);
|
||||
mat.enableInstancing = true;
|
||||
if (glow)
|
||||
{
|
||||
mat.EnableKeyword("_EMISSION");
|
||||
mat.SetColor("_EmissionColor", color * 1.8f);
|
||||
mat.SetColor("_EmissionColor", color * 1.25f);
|
||||
}
|
||||
materials[key] = mat;
|
||||
return mat;
|
||||
@@ -38,7 +41,13 @@ namespace Ironspore
|
||||
obj.transform.SetParent(parent);
|
||||
obj.transform.position = position;
|
||||
obj.transform.localScale = scale;
|
||||
obj.GetComponent<Renderer>().sharedMaterial = Material(color, glow);
|
||||
var renderer = obj.GetComponent<Renderer>();
|
||||
var surface = name == "Ground" || name == "AccessRamp" ? Surface.Concrete : name == "ContainerRib" || name == "CanopyRail" ? Surface.BareMetal : Surface.Painted;
|
||||
renderer.sharedMaterial = Material(color, glow, surface);
|
||||
bool detail = name == "FloorSeam" || name == "AmberStripe" || name == "CrateLatch" || name == "RelayScreen" || name == "LandingLight" || name == "CanopyIndicator" || name.EndsWith("WorkFixture");
|
||||
renderer.shadowCastingMode = detail || name == "EnemyBolt" ? ShadowCastingMode.Off : ShadowCastingMode.On;
|
||||
renderer.receiveShadows = !glow;
|
||||
if (name == "EnemyBolt") obj.layer = 10;
|
||||
if (!collision)
|
||||
Object.Destroy(obj.GetComponent<Collider>());
|
||||
return obj;
|
||||
@@ -59,6 +68,12 @@ namespace Ironspore
|
||||
dashboard.transform.localPosition = new Vector3(0, -.42f, .75f);
|
||||
foreach (var child in root.GetComponentsInChildren<Transform>())
|
||||
child.gameObject.layer = 8;
|
||||
foreach (var renderer in root.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
||||
renderer.lightProbeUsage = LightProbeUsage.Off;
|
||||
renderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
|
||||
}
|
||||
root.SetActive(false);
|
||||
return root;
|
||||
}
|
||||
@@ -67,11 +82,17 @@ namespace Ironspore
|
||||
var obj = new GameObject("Sign");
|
||||
obj.transform.SetParent(parent);
|
||||
obj.transform.position = position;
|
||||
obj.layer = 11;
|
||||
var label = obj.AddComponent<TextMesh>();
|
||||
label.fontSize = 40;
|
||||
label.characterSize = .09f;
|
||||
label.anchor = TextAnchor.MiddleCenter;
|
||||
label.color = color;
|
||||
var renderer = obj.GetComponent<Renderer>();
|
||||
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
||||
renderer.receiveShadows = false;
|
||||
renderer.lightProbeUsage = LightProbeUsage.Off;
|
||||
renderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
|
||||
obj.AddComponent<LocalizedWorldLabel>().Initialize(key);
|
||||
obj.AddComponent<FaceCamera>();
|
||||
}
|
||||
@@ -79,6 +100,11 @@ namespace Ironspore
|
||||
{
|
||||
var obj = new GameObject("ShotTrace");
|
||||
var line = obj.AddComponent<LineRenderer>();
|
||||
obj.layer = 11;
|
||||
line.shadowCastingMode = ShadowCastingMode.Off;
|
||||
line.receiveShadows = false;
|
||||
line.lightProbeUsage = LightProbeUsage.Off;
|
||||
line.reflectionProbeUsage = ReflectionProbeUsage.Off;
|
||||
line.positionCount = 2;
|
||||
line.SetPosition(0, from);
|
||||
line.SetPosition(1, to);
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
// Opt-in player QA. Normal launches never instantiate this component.
|
||||
public sealed class LightingValidation : MonoBehaviour
|
||||
{
|
||||
const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||
static readonly FieldInfo Yaw = typeof(MechPilot).GetField("yaw", Private);
|
||||
static readonly FieldInfo Pitch = typeof(MechPilot).GetField("pitch", Private);
|
||||
static readonly PropertyInfo View = typeof(MechPilot).GetProperty("FirstPerson");
|
||||
static readonly Vector3[] Route = { new Vector3(3, 0, -18), new Vector3(26, 0, -18), new Vector3(26, 0, 38), new Vector3(3, 0, 38), new Vector3(3, 0, -18), new Vector3(-26, 0, -18), new Vector3(-26, 0, 38), new Vector3(3, 0, 38) };
|
||||
readonly FrameTiming[] timing = new FrameTiming[1];
|
||||
readonly List<Sample> samples = new List<Sample>(40000);
|
||||
readonly Report report = new Report();
|
||||
GameSession game;
|
||||
Keyboard keyboard;
|
||||
Mouse mouse;
|
||||
Camera benchmarkCamera;
|
||||
int renderedFrames;
|
||||
string folder;
|
||||
bool measuring;
|
||||
double previous;
|
||||
ulong lastTiming;
|
||||
int waypoint;
|
||||
int visitedWaypoints, segmentWaypoints;
|
||||
float travelled, segmentTravelled;
|
||||
bool touring;
|
||||
Color32[] captured;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void Launch()
|
||||
{
|
||||
if (!Debug.isDebugBuild || Array.IndexOf(Environment.GetCommandLineArgs(), "-lighting-qa") < 0)
|
||||
return;
|
||||
new GameObject("LightingValidation").AddComponent<LightingValidation>();
|
||||
}
|
||||
|
||||
static string Arg(string name, string fallback)
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
int index = Array.IndexOf(args, name);
|
||||
return index >= 0 && index + 1 < args.Length ? args[index + 1] : fallback;
|
||||
}
|
||||
|
||||
IEnumerator Start()
|
||||
{
|
||||
folder = Path.GetFullPath(Arg("-lighting-output", Path.Combine(Application.persistentDataPath, "LightingQA")));
|
||||
Directory.CreateDirectory(folder);
|
||||
Directory.CreateDirectory(Path.Combine(folder, "Screenshots"));
|
||||
if (!Application.productName.Contains("Lighting tree3"))
|
||||
throw new InvalidOperationException("Lighting QA requires the isolated tree3 product namespace.");
|
||||
Application.runInBackground = true;
|
||||
Screen.SetResolution(1920, 1080, FullScreenMode.FullScreenWindow);
|
||||
InputSystem.settings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus;
|
||||
while (!GameSession.Active)
|
||||
yield return null;
|
||||
game = GameSession.Active;
|
||||
Camera.onPostRender += CameraRendered;
|
||||
yield return null;
|
||||
string quality = Arg("-lighting-quality", "Medium");
|
||||
int level = Array.IndexOf(QualitySettings.names, quality);
|
||||
if (level < 0)
|
||||
throw new ArgumentException("Unknown quality: " + quality);
|
||||
QualitySettings.SetQualityLevel(level, true);
|
||||
game.World.BuildHangar();
|
||||
QualitySettings.vSyncCount = 0;
|
||||
Application.targetFrameRate = int.Parse(Arg("-lighting-cap", "-1"), CultureInfo.InvariantCulture);
|
||||
report.tag = Arg("-lighting-tag", "optimized");
|
||||
report.branch = int.Parse(Arg("-lighting-branch", "0"), CultureInfo.InvariantCulture);
|
||||
report.routeVersion = "perimeter-and-center-v2";
|
||||
report.startedUtc = DateTime.UtcNow.ToString("O");
|
||||
report.gpu = SystemInfo.graphicsDeviceName;
|
||||
report.api = SystemInfo.graphicsDeviceType.ToString();
|
||||
report.cpu = SystemInfo.processorType;
|
||||
report.gpuMemoryMiB = SystemInfo.graphicsMemorySize;
|
||||
report.systemMemoryMiB = SystemInfo.systemMemorySize;
|
||||
report.colorSpace = QualitySettings.activeColorSpace.ToString();
|
||||
report.quality = QualitySettings.names[level];
|
||||
report.shadowDistance = QualitySettings.shadowDistance;
|
||||
report.cascades = QualitySettings.shadowCascades;
|
||||
report.vSync = QualitySettings.vSyncCount;
|
||||
report.frameCap = Application.targetFrameRate;
|
||||
report.persistentDataPath = Application.persistentDataPath;
|
||||
report.frameTimingEnabled = FrameTimingManager.IsFeatureEnabled();
|
||||
keyboard = InputSystem.AddDevice<Keyboard>("LightingQaKeyboard");
|
||||
mouse = InputSystem.AddDevice<Mouse>("LightingQaMouse");
|
||||
game.Input.devices = new InputDevice[] { keyboard, mouse };
|
||||
yield return new WaitForSecondsRealtime(2);
|
||||
yield return CaptureScreen("hangar");
|
||||
game.StartTrial();
|
||||
yield return new WaitForSecondsRealtime(2);
|
||||
report.dynamicResolution = game.Pilot.GetComponentInChildren<Camera>().allowDynamicResolution;
|
||||
report.renderWidthScale = ScalableBufferManager.widthScaleFactor;
|
||||
report.renderHeightScale = ScalableBufferManager.heightScaleFactor;
|
||||
report.sunResolution = GameObject.Find("Sun").GetComponent<Light>().shadowCustomResolution;
|
||||
yield return VisualChecks();
|
||||
if (Array.IndexOf(Environment.GetCommandLineArgs(), "-lighting-visual-only") < 0)
|
||||
{
|
||||
// Use the actual combat world, including the final-stage enemy population.
|
||||
game.EndSortie(true);
|
||||
game.BeginSortie();
|
||||
game.Store.Commit(save => { save.run.seed = 1739; save.run.stage = 4; save.run.branch = report.branch; save.run.armor = 100000; return true; });
|
||||
game.Resume();
|
||||
benchmarkCamera = game.Pilot.GetComponentInChildren<Camera>();
|
||||
report.cameraWidth = benchmarkCamera.pixelWidth;
|
||||
report.cameraHeight = benchmarkCamera.pixelHeight;
|
||||
yield return new WaitForSecondsRealtime(2);
|
||||
foreach (var enemy in FindObjectsByType<EnemyUnit>())
|
||||
// Keep the same combatants alive throughout every performance sample.
|
||||
typeof(EnemyUnit).GetField("health", Private).SetValue(enemy, 1000000f);
|
||||
game.World.CallExtraction();
|
||||
touring = true;
|
||||
waypoint = 0;
|
||||
double warmup = double.Parse(Arg("-lighting-warmup", "60"), CultureInfo.InvariantCulture);
|
||||
double duration = double.Parse(Arg("-lighting-seconds", "120"), CultureInfo.InvariantCulture);
|
||||
int repeats = int.Parse(Arg("-lighting-repeats", "3"), CultureInfo.InvariantCulture);
|
||||
report.warmupSeconds = warmup;
|
||||
report.sampleSeconds = duration;
|
||||
Debug.Log("Lighting QA warmup: " + warmup + " seconds");
|
||||
yield return Wait(warmup);
|
||||
for (int repeat = 0; repeat < repeats; repeat++)
|
||||
{
|
||||
for (int view = 0; view < 2; view++)
|
||||
{
|
||||
View.SetValue(game.Pilot, view == 1);
|
||||
yield return Wait(2);
|
||||
samples.Clear();
|
||||
renderedFrames = 0;
|
||||
segmentWaypoints = visitedWaypoints;
|
||||
segmentTravelled = travelled;
|
||||
previous = Time.realtimeSinceStartupAsDouble;
|
||||
measuring = true;
|
||||
yield return Wait(duration);
|
||||
measuring = false;
|
||||
string name = (view == 0 ? "shoulder" : "cockpit") + "-" + (repeat + 1);
|
||||
WriteSegment(name);
|
||||
yield return CaptureScreen(name);
|
||||
Debug.Log("Lighting QA completed " + name);
|
||||
}
|
||||
}
|
||||
touring = false;
|
||||
}
|
||||
report.width = Screen.width;
|
||||
report.height = Screen.height;
|
||||
report.finishedUtc = DateTime.UtcNow.ToString("O");
|
||||
report.completed = true;
|
||||
report.routeCompleted = game.InField && game.Run.routeCompleted;
|
||||
SaveReport();
|
||||
if (game.InField) game.EndSortie(true);
|
||||
Debug.Log("Lighting QA finished: " + Path.Combine(folder, "report.json"));
|
||||
if (!Application.isEditor)
|
||||
Application.Quit();
|
||||
}
|
||||
|
||||
static IEnumerator Wait(double seconds)
|
||||
{
|
||||
double until = Time.realtimeSinceStartupAsDouble + seconds;
|
||||
while (Time.realtimeSinceStartupAsDouble < until)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
IEnumerator VisualChecks()
|
||||
{
|
||||
var pilot = game.Pilot;
|
||||
var visual = pilot.transform.Find("MechVisual");
|
||||
var renderers = visual.GetComponentsInChildren<Renderer>(true);
|
||||
var sun = GameObject.Find("Sun").GetComponent<Light>();
|
||||
Pitch.SetValue(pilot, 25f);
|
||||
Yaw.SetValue(pilot, 0f);
|
||||
View.SetValue(pilot, false);
|
||||
yield return new WaitForSecondsRealtime(.25f);
|
||||
yield return CaptureScreen("shoulder");
|
||||
View.SetValue(pilot, true);
|
||||
yield return new WaitForSecondsRealtime(.25f);
|
||||
yield return CaptureScreen("cockpit");
|
||||
int activeCasters = 0;
|
||||
foreach (var renderer in renderers)
|
||||
if (renderer.enabled && renderer.gameObject.activeInHierarchy && renderer.shadowCastingMode == ShadowCastingMode.ShadowsOnly)
|
||||
activeCasters++;
|
||||
report.firstPersonShadowOnlyRenderers = activeCasters;
|
||||
// Hold the real pilot in first-person mode. The observer sees its shadow only.
|
||||
pilot.enabled = false;
|
||||
var cockpit = (GameObject)typeof(MechPilot).GetField("cockpit", Private).GetValue(pilot);
|
||||
cockpit.SetActive(false);
|
||||
var observerObject = new GameObject("LightingQaObserver");
|
||||
var observer = observerObject.AddComponent<Camera>();
|
||||
observer.enabled = false;
|
||||
observer.cullingMask = ~(1 << 11);
|
||||
observer.fieldOfView = 52;
|
||||
observer.nearClipPlane = .1f;
|
||||
observer.farClipPlane = 140;
|
||||
observer.transform.position = new Vector3(0, 9, -23);
|
||||
observer.transform.LookAt(new Vector3(0, 0, -15));
|
||||
yield return null;
|
||||
CaptureCamera(observer, "shadow-before-move");
|
||||
var before = captured;
|
||||
var controller = pilot.GetComponent<CharacterController>();
|
||||
controller.enabled = false;
|
||||
pilot.transform.position += Vector3.right * 2;
|
||||
controller.enabled = true;
|
||||
yield return null;
|
||||
CaptureCamera(observer, "shadow-after-move");
|
||||
report.movingShadowPixels = DifferentPixels(before, captured);
|
||||
var oldShadows = sun.shadows;
|
||||
sun.shadows = LightShadows.None;
|
||||
yield return null;
|
||||
CaptureCamera(observer, "sun-shadows-disabled");
|
||||
var noShadows = captured;
|
||||
sun.shadows = oldShadows;
|
||||
yield return null;
|
||||
CaptureCamera(observer, "sun-shadows-enabled");
|
||||
report.sunShadowPixels = DifferentPixels(noShadows, captured);
|
||||
var localObject = GameObject.Find("NavigationWorkLight");
|
||||
if (localObject)
|
||||
{
|
||||
var local = localObject.GetComponent<Light>();
|
||||
controller.enabled = false;
|
||||
pilot.transform.position = new Vector3(0, .1f, 18);
|
||||
controller.enabled = true;
|
||||
observer.transform.position = new Vector3(7, 10, 13);
|
||||
observer.transform.LookAt(new Vector3(0, .3f, 21));
|
||||
yield return new WaitForSecondsRealtime(.25f);
|
||||
local.enabled = false;
|
||||
var manager = local.GetComponentInParent<MonoBehaviour>();
|
||||
if (manager && manager.GetType().Name == "WorldLighting")
|
||||
manager.enabled = false;
|
||||
CaptureCamera(observer, "local-off");
|
||||
var off = captured;
|
||||
local.enabled = true;
|
||||
CaptureCamera(observer, "local-on");
|
||||
var on = captured;
|
||||
var blocker = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
blocker.name = "LightingQaTemporaryBlocker";
|
||||
blocker.transform.position = local.transform.position + local.transform.forward * 1.1f;
|
||||
blocker.transform.localScale = new Vector3(1.4f, .15f, 1.4f);
|
||||
blocker.GetComponent<Collider>().enabled = false;
|
||||
local.enabled = false;
|
||||
yield return null;
|
||||
CaptureCamera(observer, "local-blocked-off");
|
||||
var blockedOff = captured;
|
||||
local.enabled = true;
|
||||
CaptureCamera(observer, "local-blocked-on");
|
||||
int lit = 0, blocked = 0;
|
||||
for (int i = 0; i < on.Length; i++)
|
||||
{
|
||||
int direct = Brightness(on[i]) - Brightness(off[i]);
|
||||
int obstructed = Brightness(captured[i]) - Brightness(blockedOff[i]);
|
||||
if (direct > 30) { lit++; if (obstructed < direct * .45f) blocked++; }
|
||||
}
|
||||
report.localLitPixels = lit;
|
||||
report.localBlockedPixels = blocked;
|
||||
report.localLightCastsShadows = local.shadows != LightShadows.None;
|
||||
Destroy(blocker);
|
||||
if (manager) manager.enabled = true;
|
||||
}
|
||||
Destroy(observerObject);
|
||||
pilot.enabled = true;
|
||||
report.visualPassed = activeCasters > 0 && report.movingShadowPixels > 50 && report.sunShadowPixels > 50 && report.localLightCastsShadows && report.localBlockedPixels > 50;
|
||||
SaveReport();
|
||||
}
|
||||
|
||||
static int Brightness(Color32 color) => color.r + color.g + color.b;
|
||||
static int DifferentPixels(Color32[] first, Color32[] second)
|
||||
{
|
||||
int changed = 0;
|
||||
for (int i = 0; i < first.Length; i++)
|
||||
if (Math.Abs(first[i].r - second[i].r) + Math.Abs(first[i].g - second[i].g) + Math.Abs(first[i].b - second[i].b) > 30)
|
||||
changed++;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void CaptureCamera(Camera camera, string name)
|
||||
{
|
||||
var target = RenderTexture.GetTemporary(1920, 1080, 24, RenderTextureFormat.ARGB32);
|
||||
var oldTarget = camera.targetTexture;
|
||||
var oldActive = RenderTexture.active;
|
||||
camera.targetTexture = target;
|
||||
camera.Render();
|
||||
RenderTexture.active = target;
|
||||
var image = new Texture2D(1920, 1080, TextureFormat.RGB24, false);
|
||||
image.ReadPixels(new Rect(0, 0, 1920, 1080), 0, 0);
|
||||
image.Apply();
|
||||
captured = image.GetPixels32();
|
||||
File.WriteAllBytes(Path.Combine(folder, "Screenshots", name + ".png"), image.EncodeToPNG());
|
||||
camera.targetTexture = oldTarget;
|
||||
RenderTexture.active = oldActive;
|
||||
RenderTexture.ReleaseTemporary(target);
|
||||
Destroy(image);
|
||||
}
|
||||
|
||||
IEnumerator CaptureScreen(string name)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
var image = ScreenCapture.CaptureScreenshotAsTexture();
|
||||
File.WriteAllBytes(Path.Combine(folder, "Screenshots", name + ".png"), image.EncodeToPNG());
|
||||
Destroy(image);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!touring || !game || !game.InField)
|
||||
return;
|
||||
var pilot = game.Pilot;
|
||||
var goal = Route[waypoint];
|
||||
var offset = goal - pilot.transform.position;
|
||||
offset.y = 0;
|
||||
if (offset.magnitude < 1.2f) { visitedWaypoints++; waypoint = (waypoint + 1) % Route.Length; offset = Route[waypoint] - pilot.transform.position; offset.y = 0; }
|
||||
Yaw.SetValue(pilot, Mathf.Atan2(offset.x, offset.z) * Mathf.Rad2Deg);
|
||||
Pitch.SetValue(pilot, 12f);
|
||||
var beforeMove = pilot.transform.position;
|
||||
pilot.GetComponent<CharacterController>().Move(offset.normalized * (3.5f * Time.deltaTime));
|
||||
travelled += Vector3.Distance(beforeMove, pilot.transform.position);
|
||||
var keys = new KeyboardState();
|
||||
if ((int)Time.realtimeSinceStartup % 9 == 0) keys.Press(Key.Space);
|
||||
InputSystem.QueueStateEvent(keyboard, keys);
|
||||
var buttons = new MouseState();
|
||||
if ((int)Time.realtimeSinceStartup % 3 == 0) buttons = buttons.WithButton(MouseButton.Left);
|
||||
InputSystem.QueueStateEvent(mouse, buttons);
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
FrameTimingManager.CaptureFrameTimings();
|
||||
if (!measuring)
|
||||
return;
|
||||
double now = Time.realtimeSinceStartupAsDouble;
|
||||
var sample = new Sample { wallMs = (now - previous) * 1000 };
|
||||
previous = now;
|
||||
if (FrameTimingManager.GetLatestTimings(1, timing) > 0 && timing[0].frameStartTimestamp != lastTiming)
|
||||
{
|
||||
lastTiming = timing[0].frameStartTimestamp;
|
||||
sample.cpuMs = timing[0].cpuFrameTime;
|
||||
sample.gpuMs = timing[0].gpuFrameTime;
|
||||
sample.mainMs = timing[0].cpuMainThreadFrameTime;
|
||||
sample.renderMs = timing[0].cpuRenderThreadFrameTime;
|
||||
sample.presentWaitMs = timing[0].cpuMainThreadPresentWaitTime;
|
||||
}
|
||||
samples.Add(sample);
|
||||
}
|
||||
|
||||
void CameraRendered(Camera camera)
|
||||
{
|
||||
if (measuring && camera == benchmarkCamera) renderedFrames++;
|
||||
}
|
||||
|
||||
void WriteSegment(string name)
|
||||
{
|
||||
var wall = new List<double>(samples.Count);
|
||||
var gpu = new List<double>(samples.Count);
|
||||
var cpu = new List<double>(samples.Count);
|
||||
var csv = new StringBuilder("wallMs,cpuMs,gpuMs,mainMs,renderMs,presentWaitMs\n");
|
||||
double total = 0;
|
||||
foreach (var s in samples)
|
||||
{
|
||||
wall.Add(s.wallMs); total += s.wallMs;
|
||||
if (s.gpuMs > 0) gpu.Add(s.gpuMs);
|
||||
if (s.cpuMs > 0) cpu.Add(s.cpuMs);
|
||||
csv.AppendFormat(CultureInfo.InvariantCulture, "{0:F5},{1:F5},{2:F5},{3:F5},{4:F5},{5:F5}\n", s.wallMs, s.cpuMs, s.gpuMs, s.mainMs, s.renderMs, s.presentWaitMs);
|
||||
}
|
||||
wall.Sort(); gpu.Sort(); cpu.Sort();
|
||||
File.WriteAllText(Path.Combine(folder, name + ".csv"), csv.ToString());
|
||||
report.segments.Add(new Segment { name = name, frames = samples.Count, cameraRenderFrames = renderedFrames, meanFps = samples.Count * 1000.0 / total, p50Ms = Percentile(wall, .5), p95Ms = Percentile(wall, .95), p99Ms = Percentile(wall, .99), gpuP95Ms = Percentile(gpu, .95), cpuP95Ms = Percentile(cpu, .95), gpuSamples = gpu.Count, cpuSamples = cpu.Count, allocatedMiB = Profiler.GetTotalAllocatedMemoryLong() / 1048576.0, enemyCount = FindObjectsByType<EnemyUnit>().Length, width = Screen.width, height = Screen.height, waypointsVisited = visitedWaypoints - segmentWaypoints, distanceTravelled = travelled - segmentTravelled, passed = Screen.width == 1920 && Screen.height == 1080 && Percentile(wall, .95) <= 16.7 && visitedWaypoints - segmentWaypoints >= 4 && renderedFrames >= samples.Count * .98 });
|
||||
SaveReport();
|
||||
}
|
||||
|
||||
static double Percentile(List<double> sorted, double fraction) => sorted.Count == 0 ? 0 : sorted[Math.Max(0, (int)Math.Ceiling(sorted.Count * fraction) - 1)];
|
||||
void SaveReport() => File.WriteAllText(Path.Combine(folder, "report.json"), JsonUtility.ToJson(report, true));
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
Camera.onPostRender -= CameraRendered;
|
||||
if (keyboard != null && keyboard.added) InputSystem.RemoveDevice(keyboard);
|
||||
if (mouse != null && mouse.added) InputSystem.RemoveDevice(mouse);
|
||||
}
|
||||
|
||||
[Serializable] struct Sample { public double wallMs, cpuMs, gpuMs, mainMs, renderMs, presentWaitMs; }
|
||||
[Serializable] sealed class Segment
|
||||
{
|
||||
public string name;
|
||||
public int frames, width, height, gpuSamples, cpuSamples, enemyCount, waypointsVisited, cameraRenderFrames;
|
||||
public double meanFps, p50Ms, p95Ms, p99Ms, gpuP95Ms, cpuP95Ms, allocatedMiB, distanceTravelled;
|
||||
public bool passed;
|
||||
}
|
||||
[Serializable] sealed class Report
|
||||
{
|
||||
public string tag, startedUtc, finishedUtc, gpu, api, cpu, colorSpace, quality, persistentDataPath, routeVersion;
|
||||
public int width, height, gpuMemoryMiB, systemMemoryMiB, cascades, vSync, frameCap, firstPersonShadowOnlyRenderers, movingShadowPixels, sunShadowPixels, localLitPixels, localBlockedPixels, sunResolution, cameraWidth, cameraHeight, branch;
|
||||
public float shadowDistance, renderWidthScale, renderHeightScale;
|
||||
public double warmupSeconds, sampleSeconds;
|
||||
public bool completed, frameTimingEnabled, localLightCastsShadows, visualPassed, dynamicResolution, routeCompleted;
|
||||
public List<Segment> segments = new List<Segment>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 833538f031bef3b448f712042795cb10
|
||||
@@ -1,4 +1,5 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
@@ -31,6 +32,9 @@ namespace Ironspore
|
||||
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;
|
||||
@@ -53,7 +57,12 @@ namespace Ironspore
|
||||
visual.localPosition = Vector3.zero;
|
||||
foreach (var t in visual.GetComponentsInChildren<Transform>())
|
||||
t.gameObject.layer = 8;
|
||||
worldRenderers = visual.GetComponentsInChildren<Renderer>(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<Camera>();
|
||||
view.nearClipPlane = .08f;
|
||||
@@ -165,8 +174,14 @@ namespace Ironspore
|
||||
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);
|
||||
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)
|
||||
{
|
||||
@@ -201,6 +216,8 @@ namespace Ironspore
|
||||
enemy.Damage(part.damage * (1 + game.Run.upgrades * .15f));
|
||||
lastHitTime = Time.time;
|
||||
}
|
||||
else if (game.Trial && hit.collider.GetComponentInParent<RangeTarget>() 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);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f503296d8ae461f41a8229186bbad63e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,178 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
public sealed class WorldLighting : MonoBehaviour
|
||||
{
|
||||
readonly List<Light> fixtures = new List<Light>(2);
|
||||
WorldLightingProfile profile;
|
||||
Material sky;
|
||||
ReflectionProbe environment;
|
||||
Camera observer;
|
||||
bool ownsProfile;
|
||||
int shadowBudget;
|
||||
bool captureQueued, capturing;
|
||||
float captureAfter;
|
||||
public bool ReflectionReady { get; private set; }
|
||||
|
||||
public void Initialize(WorldLightingProfile settings, bool hangar)
|
||||
{
|
||||
profile = settings;
|
||||
if (!profile)
|
||||
{
|
||||
profile = ScriptableObject.CreateInstance<WorldLightingProfile>();
|
||||
ownsProfile = true;
|
||||
}
|
||||
int level = QualitySettings.GetQualityLevel();
|
||||
shadowBudget = level <= 1 ? 0 : level >= 4 ? 2 : 1;
|
||||
RenderSettings.ambientMode = AmbientMode.Trilight;
|
||||
RenderSettings.ambientSkyColor = profile.ambientSky;
|
||||
RenderSettings.ambientEquatorColor = profile.ambientEquator;
|
||||
RenderSettings.ambientGroundColor = profile.ambientGround;
|
||||
RenderSettings.fog = !hangar;
|
||||
RenderSettings.fogMode = FogMode.Linear;
|
||||
RenderSettings.fogColor = profile.fogColor;
|
||||
RenderSettings.fogStartDistance = profile.fogStart;
|
||||
RenderSettings.fogEndDistance = profile.fogEnd;
|
||||
RenderSettings.reflectionIntensity = profile.reflectionIntensity;
|
||||
var sourceSky = RenderSettings.skybox;
|
||||
sky = sourceSky ? new Material(sourceSky) : new Material(Shader.Find("Skybox/Procedural"));
|
||||
sky.name = "RuntimeDaySky";
|
||||
if (sky.HasProperty("_Exposure")) sky.SetFloat("_Exposure", profile.skyExposure);
|
||||
RenderSettings.skybox = sky;
|
||||
var sunObject = new GameObject("Sun");
|
||||
sunObject.transform.SetParent(transform, false);
|
||||
sunObject.transform.rotation = Quaternion.Euler(profile.sunEuler);
|
||||
var sun = sunObject.AddComponent<Light>();
|
||||
sun.type = LightType.Directional;
|
||||
sun.color = profile.sunColor;
|
||||
sun.intensity = hangar ? profile.hangarSunIntensity : profile.sunIntensity;
|
||||
sun.renderMode = LightRenderMode.ForcePixel;
|
||||
sun.shadows = level <= 1 ? LightShadows.Hard : LightShadows.Soft;
|
||||
sun.shadowStrength = 1;
|
||||
sun.shadowBias = profile.bias;
|
||||
sun.shadowNormalBias = profile.normalBias;
|
||||
sun.shadowNearPlane = profile.shadowNearPlane;
|
||||
sun.shadowCustomResolution = level <= 1 ? 1024 : level >= 4 ? 4096 : 2048;
|
||||
RenderSettings.sun = sun;
|
||||
}
|
||||
|
||||
public void CompleteEnvironment(Camera camera, bool hangar)
|
||||
{
|
||||
observer = camera;
|
||||
if (hangar)
|
||||
{
|
||||
WorldFactory.Box("HangarWorkFixture", new Vector3(2, 8.2f, 1), new Vector3(2, .18f, .65f), new Color(.8f, .87f, 1), transform, false, true);
|
||||
AddSpot("HangarWorkLight", new Vector3(2, 8, 1), new Vector3(2, 1, 2), new Color(.82f, .9f, 1), profile.hangarIntensity, 17, 84);
|
||||
}
|
||||
else
|
||||
{
|
||||
WorldFactory.Box("NavigationWorkFixture", new Vector3(0, 3.4f, 22.7f), new Vector3(1.2f, .16f, .4f), new Color(.55f, 1, .85f), transform, false, true);
|
||||
AddSpot("NavigationWorkLight", new Vector3(0, 3.25f, 22.3f), new Vector3(0, 0, 20.6f), new Color(.65f, 1, .85f), profile.navigationIntensity, 11, 78);
|
||||
WorldFactory.Box("ExtractionLampPost", new Vector3(-22.4f, 3.1f, -8), new Vector3(.16f, 6.2f, .16f), new Color(.2f, .25f, .28f), transform, false);
|
||||
WorldFactory.Box("ExtractionLampArm", new Vector3(-20.7f, 6.1f, -8), new Vector3(3.5f, .15f, .18f), new Color(.2f, .25f, .28f), transform, false);
|
||||
WorldFactory.Box("ExtractionWorkFixture", new Vector3(-19, 6, -8), new Vector3(1, .18f, .6f), new Color(1, .77f, .45f), transform, false, true);
|
||||
AddSpot("ExtractionWorkLight", new Vector3(-19, 5.8f, -8), new Vector3(-19, 0, -8), new Color(1, .8f, .55f), profile.extractionIntensity, 13, 76);
|
||||
}
|
||||
var probeObject = new GameObject("EnvironmentReflection");
|
||||
probeObject.transform.SetParent(transform, false);
|
||||
probeObject.transform.position = hangar ? new Vector3(2, 3, 2) : new Vector3(0, 3, 8);
|
||||
environment = probeObject.AddComponent<ReflectionProbe>();
|
||||
environment.mode = ReflectionProbeMode.Realtime;
|
||||
environment.refreshMode = ReflectionProbeRefreshMode.ViaScripting;
|
||||
environment.timeSlicingMode = ReflectionProbeTimeSlicingMode.IndividualFaces;
|
||||
environment.resolution = QualitySettings.GetQualityLevel() >= 4 ? 256 : 128;
|
||||
environment.hdr = true;
|
||||
environment.boxProjection = true;
|
||||
environment.blendDistance = 5;
|
||||
environment.size = hangar ? new Vector3(66, 18, 45) : new Vector3(74, 20, 104);
|
||||
// The captured environment must not freeze actors, transient props or world labels.
|
||||
environment.cullingMask = ~((1 << 8) | (1 << 9) | (1 << 10) | (1 << 11));
|
||||
environment.nearClipPlane = .2f;
|
||||
environment.farClipPlane = 150;
|
||||
environment.intensity = profile.reflectionIntensity;
|
||||
SelectLights();
|
||||
StartCoroutine(CaptureEnvironment());
|
||||
}
|
||||
|
||||
void AddSpot(string name, Vector3 position, Vector3 target, Color color, float intensity, float range, float angle)
|
||||
{
|
||||
var obj = new GameObject(name);
|
||||
obj.transform.SetParent(transform, false);
|
||||
obj.transform.position = position;
|
||||
obj.transform.LookAt(target);
|
||||
var light = obj.AddComponent<Light>();
|
||||
light.type = LightType.Spot;
|
||||
light.renderMode = LightRenderMode.ForcePixel;
|
||||
light.color = color;
|
||||
light.intensity = intensity;
|
||||
light.range = range;
|
||||
light.spotAngle = angle;
|
||||
light.innerSpotAngle = angle * .65f;
|
||||
light.shadows = LightShadows.Soft;
|
||||
light.shadowCustomResolution = 1024;
|
||||
light.shadowBias = profile.bias;
|
||||
light.shadowNormalBias = profile.normalBias;
|
||||
light.shadowNearPlane = profile.shadowNearPlane;
|
||||
light.enabled = false;
|
||||
fixtures.Add(light);
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
SelectLights();
|
||||
if (environment && captureQueued && !capturing && Time.unscaledTime >= captureAfter)
|
||||
StartCoroutine(CaptureEnvironment());
|
||||
}
|
||||
|
||||
void SelectLights()
|
||||
{
|
||||
if (!observer)
|
||||
return;
|
||||
Vector3 position = observer.transform.position;
|
||||
float limit = profile.localLightCullDistance * profile.localLightCullDistance;
|
||||
Light nearest = null;
|
||||
float distance = limit;
|
||||
foreach (var light in fixtures)
|
||||
{
|
||||
float candidate = (light.transform.position - position).sqrMagnitude;
|
||||
if (candidate < distance) { distance = candidate; nearest = light; }
|
||||
}
|
||||
foreach (var light in fixtures)
|
||||
{
|
||||
bool active = shadowBudget > 0 && (light == nearest || shadowBudget > 1 && (light.transform.position - position).sqrMagnitude < limit);
|
||||
if (light.enabled == active)
|
||||
continue;
|
||||
light.enabled = active;
|
||||
captureQueued = true;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator CaptureEnvironment()
|
||||
{
|
||||
capturing = true;
|
||||
captureQueued = false;
|
||||
yield return null;
|
||||
DynamicGI.UpdateEnvironment();
|
||||
int render = environment.RenderProbe();
|
||||
while (environment && !environment.IsFinishedRendering(render))
|
||||
yield return null;
|
||||
ReflectionReady = environment != null;
|
||||
capturing = false;
|
||||
captureAfter = Time.unscaledTime + 8;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (sky)
|
||||
{
|
||||
if (RenderSettings.skybox == sky) RenderSettings.skybox = null;
|
||||
Destroy(sky);
|
||||
}
|
||||
if (ownsProfile && profile) Destroy(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f71c474a8bca074aa2274b7a4054680
|
||||
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Ironspore/World Lighting Profile")]
|
||||
public sealed class WorldLightingProfile : ScriptableObject
|
||||
{
|
||||
[Header("Daylight")]
|
||||
public Vector3 sunEuler = new Vector3(48, -35, 0);
|
||||
public Color sunColor = new Color(1, .94f, .84f);
|
||||
[Min(0)] public float sunIntensity = 1.2f;
|
||||
[Min(0)] public float hangarSunIntensity = .8f;
|
||||
public Color ambientSky = new Color(.24f, .30f, .38f);
|
||||
public Color ambientEquator = new Color(.12f, .16f, .19f);
|
||||
public Color ambientGround = new Color(.045f, .055f, .065f);
|
||||
public Color fogColor = new Color(.15f, .21f, .26f);
|
||||
public float fogStart = 38;
|
||||
public float fogEnd = 145;
|
||||
[Range(0, 2)] public float skyExposure = .85f;
|
||||
[Range(0, 1)] public float reflectionIntensity = .7f;
|
||||
[Header("Shadows")]
|
||||
[Range(0, 2)] public float bias = .08f;
|
||||
[Range(0, 2)] public float normalBias = .4f;
|
||||
[Min(.01f)] public float shadowNearPlane = .2f;
|
||||
[Header("Industrial fixtures")]
|
||||
[Min(0)] public float navigationIntensity = 6;
|
||||
[Min(0)] public float extractionIntensity = 6;
|
||||
[Min(0)] public float hangarIntensity = 5;
|
||||
[Min(1)] public float localLightCullDistance = 50;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90562a900c2114d48bd80969332d5ec6
|
||||
@@ -19,6 +19,7 @@ namespace Ironspore
|
||||
public UnityEngine.UIElements.StyleSheet uiStyle;
|
||||
public UnityEngine.UIElements.PanelSettings uiPanel;
|
||||
public LocalizationCatalog localization;
|
||||
public WorldLightingProfile lightingProfile;
|
||||
public bool startInRange;
|
||||
public ProfileStore Store
|
||||
{
|
||||
@@ -75,7 +76,7 @@ namespace Ironspore
|
||||
{
|
||||
Active = this;
|
||||
GameLocalization.Initialize(localization);
|
||||
Application.targetFrameRate = 120;
|
||||
Application.targetFrameRate = 60;
|
||||
mainStore = new ProfileStore(Path.Combine(Application.persistentDataPath, "Ironspore", "profile.json"));
|
||||
Store = mainStore;
|
||||
Input = Instantiate(inputTemplate);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0607d512ea805f459c7d25872379957
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
public sealed class TrainingCrate : WorldInteractable
|
||||
{
|
||||
Transform hinge;
|
||||
Transform latch;
|
||||
const float UnlatchSeconds = .16f;
|
||||
const float LiftSeconds = .55f;
|
||||
bool started;
|
||||
public void Build(Transform world, Vector3 basePosition, bool part, bool showLabel)
|
||||
{
|
||||
Color accent = part ? new Color(.98f,.53f,.17f) : new Color(.16f,.85f,.67f);
|
||||
hinge = new GameObject("CaseHinge").transform;
|
||||
hinge.SetParent(world, false);
|
||||
hinge.position = basePosition + new Vector3(0,1.36f,.71f);
|
||||
var lid = WorldFactory.Box("CaseLid", hinge.position + Vector3.back * .72f, new Vector3(1.96f,.16f,1.49f), accent, hinge, false);
|
||||
lid.layer = 10;
|
||||
foreach (int side in new[] { -1, 1 })
|
||||
WorldFactory.Box("CaseStrap", lid.transform.position + new Vector3(side * .66f,.1f,0), new Vector3(.14f,.07f,1.42f), Color.gray, hinge, false).layer = 10;
|
||||
latch = new GameObject("CaseLatchPivot").transform;
|
||||
latch.SetParent(hinge, false);
|
||||
latch.localPosition = new Vector3(0, 0, -1.49f);
|
||||
WorldFactory.Box("CaseLatch", latch.position + Vector3.down * .13f, new Vector3(.38f, .26f, .09f), Color.gray, latch, false).layer = 10;
|
||||
WorldFactory.Box("CaseLiner", basePosition + Vector3.up * 1.30f, new Vector3(1.58f, .02f, 1.18f), new Color(.07f, .10f, .11f), world, false).layer = 10;
|
||||
if (showLabel)
|
||||
WorldFactory.Label(part ? "range.parts" : "range.material", basePosition + Vector3.up * 2.4f, accent, world);
|
||||
}
|
||||
|
||||
public override void RefreshLabel()
|
||||
{
|
||||
if (collected && !started)
|
||||
{
|
||||
started = true;
|
||||
StartCoroutine(OpenLid());
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator OpenLid()
|
||||
{
|
||||
float elapsed = 0;
|
||||
while (elapsed < UnlatchSeconds + LiftSeconds)
|
||||
{
|
||||
yield return null;
|
||||
elapsed += Time.deltaTime;
|
||||
latch.localRotation = Quaternion.Euler(Mathf.SmoothStep(0, 78, elapsed / UnlatchSeconds), 0, 0);
|
||||
hinge.localRotation = Quaternion.Euler(Mathf.SmoothStep(0, 92, (elapsed - UnlatchSeconds) / LiftSeconds), 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 165d2c6b7407be447abcb69a251294c5
|
||||
@@ -23,10 +23,12 @@ namespace Ironspore
|
||||
}
|
||||
Transform root;
|
||||
GameSession game;
|
||||
WorldLighting lighting;
|
||||
public TrainingCourse Course { get; private set; }
|
||||
Vector3 extractionPosition = new Vector3(-19, 0, -8);
|
||||
GameObject transport;
|
||||
|
||||
void ResetWorld()
|
||||
void ResetWorld(bool hangar)
|
||||
{
|
||||
game = GetComponent<GameSession>();
|
||||
if (root)
|
||||
@@ -38,19 +40,10 @@ namespace Ironspore
|
||||
ExtractionRemaining = -1;
|
||||
TransportReady = false;
|
||||
Focused = null;
|
||||
Course = null;
|
||||
Escort = null;
|
||||
RenderSettings.ambientLight = new Color(.27f, .34f, .38f);
|
||||
RenderSettings.fog = true;
|
||||
RenderSettings.fogColor = new Color(.09f, .14f, .18f);
|
||||
RenderSettings.fogDensity = .009f;
|
||||
var sun = new GameObject("Sun");
|
||||
sun.transform.SetParent(root);
|
||||
sun.transform.rotation = Quaternion.Euler(48, -35, 0);
|
||||
var light = sun.AddComponent<Light>();
|
||||
light.type = LightType.Directional;
|
||||
light.intensity = 1.7f;
|
||||
light.color = new Color(1, .87f, .72f);
|
||||
light.shadows = LightShadows.Soft;
|
||||
lighting = root.gameObject.AddComponent<WorldLighting>();
|
||||
lighting.Initialize(game.lightingProfile, hangar);
|
||||
WorldFactory.Box("Ground", new Vector3(0, -.3f, 8), new Vector3(72, .6f, 100), new Color(.11f, .16f, .18f), root);
|
||||
for (int x = -30; x <= 30; x += 5)
|
||||
WorldFactory.Box("FloorSeam", new Vector3(x, .008f, 8), new Vector3(.035f, .015f, 96), new Color(.18f, .24f, .26f), root, false);
|
||||
@@ -68,7 +61,7 @@ namespace Ironspore
|
||||
}
|
||||
public void BuildHangar()
|
||||
{
|
||||
ResetWorld();
|
||||
ResetWorld(true);
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var model = Instantiate(game.mechModel, root);
|
||||
@@ -79,16 +72,18 @@ namespace Ironspore
|
||||
WorldFactory.Cylinder("DisplayPad", new Vector3(i * 4 - 2, .05f, 2), new Vector3(3, .1f, 3), game.chassis[i].accent, root, false);
|
||||
}
|
||||
var camera = new GameObject("HangarCamera");
|
||||
camera.tag = "MainCamera";
|
||||
camera.transform.SetParent(root);
|
||||
camera.AddComponent<Camera>().fieldOfView = 47;
|
||||
camera.transform.position = new Vector3(11, 4.3f, -11);
|
||||
camera.transform.LookAt(new Vector3(2, 1.5f, 2));
|
||||
camera.AddComponent<AudioListener>();
|
||||
WorldFactory.Box("RoofBeam", new Vector3(0, 9, 4), new Vector3(62, 1.3f, 2), new Color(.16f, .22f, .24f), root);
|
||||
lighting.CompleteEnvironment(camera.GetComponent<Camera>(), true);
|
||||
}
|
||||
public void BuildWarzone()
|
||||
{
|
||||
ResetWorld();
|
||||
ResetWorld(false);
|
||||
var run = game.Run;
|
||||
var avatar = new GameObject("PlayerMech");
|
||||
avatar.transform.SetParent(root);
|
||||
@@ -111,13 +106,14 @@ namespace Ironspore
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
Vector3 pos = new Vector3(i % 2 == 0 ? -8 : 9, 0, -6 + i * 6);
|
||||
bool demo = game.Trial && (i == 1 || i == 2);
|
||||
if (demo) pos = new Vector3(-24, 0, i == 1 ? 9 : 18);
|
||||
string key = "zone" + run.stage + "_crate" + i;
|
||||
if (run.searched.Contains(key))
|
||||
continue;
|
||||
string item = i == 0 ? "blueprint_boost_efficient" : i == 3 ? "blueprint_coil_capacitor" : i == 5 ? "blueprint_reactive_guard" : (i + run.biome) % 2 == 0 ? "alloy" : "cells";
|
||||
var crate = WorldFactory.Box("SalvageCrate", pos + Vector3.up * .65f, new Vector3(1.8f, 1.3f, 1.4f), new Color(.24f, .42f, .39f), root);
|
||||
WorldFactory.Box("CrateLatch", pos + new Vector3(0, 1.32f, 0), new Vector3(1.4f, .08f, .85f), new Color(.15f, .95f, .73f), root, false, true);
|
||||
var interaction = crate.AddComponent<WorldInteractable>();
|
||||
var interaction = crate.AddComponent<TrainingCrate>();
|
||||
interaction.kind = 0;
|
||||
interaction.key = key;
|
||||
interaction.itemId = item;
|
||||
@@ -125,6 +121,7 @@ namespace Ironspore
|
||||
interaction.offersPart = i == 2 || i == 4;
|
||||
interaction.partSlot = i == 2 ? 2 : 0;
|
||||
interaction.partIndex = run.stage % 2 == 0 ? 1 : 2;
|
||||
interaction.Build(root, pos, interaction.offersPart, demo);
|
||||
interaction.RefreshLabel();
|
||||
}
|
||||
var objective = WorldFactory.Box("NavigationRelay", new Vector3(0, 1.5f, 23), new Vector3(2.2f, 3, 1.6f), new Color(.21f, .33f, .36f), root);
|
||||
@@ -149,7 +146,7 @@ namespace Ironspore
|
||||
WorldFactory.Label("world.relay", new Vector3(0, 5, 23), new Color(.25f, 1, .85f), root);
|
||||
int count = game.Trial ? 2 : 3 + run.stage;
|
||||
for (int i = 0; i < count; i++)
|
||||
SpawnEnemy(new Vector3((i % 2 == 0 ? -1 : 1) * (4 + i % 3 * 3), .1f, 6 + i * 5), i % 3, i);
|
||||
SpawnEnemy(new Vector3((i % 2 == 0 ? -1 : 1) * (4 + i % 3 * 3), .1f, (game.Trial ? 39 : 6) + i * 5), i % 3, i);
|
||||
if (!game.Trial && run.stage == 4)
|
||||
{
|
||||
objective.SetActive(false);
|
||||
@@ -160,8 +157,15 @@ namespace Ironspore
|
||||
var cart = WorldFactory.Box("EscortEngineer", new Vector3(0, 1.3f, 0), new Vector3(3, 2.6f, 4), new Color(.2f, .55f, .5f), root);
|
||||
Escort = cart.AddComponent<EscortUnit>();
|
||||
Escort.Initialize(game);
|
||||
cart.layer = 10;
|
||||
}
|
||||
}
|
||||
if (game.Trial)
|
||||
{
|
||||
Course = root.gameObject.AddComponent<TrainingCourse>();
|
||||
Course.Build(game);
|
||||
}
|
||||
lighting.CompleteEnvironment(Player.GetComponentInChildren<Camera>(), false);
|
||||
}
|
||||
void SpawnEnemy(Vector3 pos, int type, int number)
|
||||
{
|
||||
@@ -198,7 +202,8 @@ namespace Ironspore
|
||||
{
|
||||
TransportReady = true;
|
||||
transport = WorldFactory.Box("RecoveryTransport", extractionPosition + new Vector3(0, 5.5f, 0), new Vector3(7, 1.8f, 10), new Color(.24f, .3f, .32f), root, false);
|
||||
WorldFactory.Box("TransportWing", transport.transform.position, new Vector3(14, .5f, 3), new Color(.32f, .4f, .41f), root, false);
|
||||
transport.layer = 10;
|
||||
WorldFactory.Box("TransportWing", transport.transform.position, new Vector3(14, .5f, 3), new Color(.32f, .4f, .41f), root, false).layer = 10;
|
||||
game.Notify("notice.transportReady");
|
||||
GameAudio.Cue(950, .3f);
|
||||
}
|
||||
@@ -220,18 +225,19 @@ namespace Ironspore
|
||||
public void LaunchPod(Vector3 position)
|
||||
{
|
||||
var pod = WorldFactory.Box("CargoPod", position + Vector3.up * 2, new Vector3(1.4f, 1, 1.4f), new Color(.1f, .8f, .6f), root, false, true);
|
||||
pod.layer = 10;
|
||||
pod.AddComponent<PodFlight>();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WorldInteractable : MonoBehaviour
|
||||
public class WorldInteractable : MonoBehaviour
|
||||
{
|
||||
public int kind;
|
||||
public string key, itemId;
|
||||
public int amount;
|
||||
public bool collected, offersPart;
|
||||
public int partSlot, partIndex;
|
||||
public string Prompt
|
||||
public virtual string Prompt
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -245,12 +251,12 @@ namespace Ironspore
|
||||
return GameLocalization.Text(g.World.TransportReady ? "prompt.board" : g.World.ExtractionRemaining >= 0 ? "prompt.inbound" : "prompt.call");
|
||||
}
|
||||
}
|
||||
public void RefreshLabel()
|
||||
public virtual void RefreshLabel()
|
||||
{
|
||||
if (collected)
|
||||
transform.localScale = Vector3.one * .5f;
|
||||
}
|
||||
public void Activate()
|
||||
public virtual void Activate()
|
||||
{
|
||||
var g = GameSession.Active;
|
||||
if (kind == 0 && !collected)
|
||||
|
||||
@@ -385,7 +385,8 @@ namespace Ironspore
|
||||
if (status != null)
|
||||
status.text = L("ui.hud.status", game.Trial ? L("ui.home.range") : L("ui.hud.zone", run.stage + 1), TimeSpan.FromSeconds(run.elapsed).ToString(@"mm\:ss"));
|
||||
if (mission != null)
|
||||
mission.text = L(run.routeCompleted ? "ui.hud.complete" : run.stage == 4 && !game.Trial ? run.branch == 0 ? "ui.hud.boss" : "ui.hud.escort" : run.objectiveDone ? "ui.hud.routeOpen" : "ui.hud.objective");
|
||||
mission.text = game.World.Course ? game.World.Course.Instruction :
|
||||
L(run.routeCompleted ? "ui.hud.complete" : run.stage == 4 && !game.Trial ? run.branch == 0 ? "ui.hud.boss" : "ui.hud.escort" : run.objectiveDone ? "ui.hud.routeOpen" : "ui.hud.objective");
|
||||
if (armor != null)
|
||||
{
|
||||
armor.value = p.Health / game.chassis[run.chassis].armor * 100;
|
||||
|
||||
Reference in New Issue
Block a user